mutating-webhook/cmd/webhook/httpServer.go
2023-03-25 15:06:58 -05:00

199 lines
5.8 KiB
Go

package main
import (
"fmt"
"io"
"log"
"strconv"
"time"
"crypto/tls"
"encoding/json"
"net/http"
"mutating-webhook/internal/config"
"mutating-webhook/internal/operations"
admission "k8s.io/api/admission/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
const InvalidMethod string = "Invalid http method."
func httpAccessLog(req *http.Request) {
log.Printf("[TRACE] %s - %s - %s\n", req.Method, req.RemoteAddr, req.RequestURI)
}
func crossSiteOrigin(w http.ResponseWriter) {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Add("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization, X-API-Token")
}
func strictTransport(w http.ResponseWriter) {
w.Header().Add("Strict-Transport-Security", "max-age=63072000")
}
func httpServer(cfg *config.Config) {
serverCertificate, _ := tls.X509KeyPair(append([]byte(cfg.CertCert), []byte(cfg.CACert)...), []byte(cfg.CertPrivateKey))
path := http.NewServeMux()
connection := &http.Server{
Addr: cfg.WebServerIP + ":" + strconv.FormatInt(int64(cfg.WebServerPort), 10),
Handler: path,
ReadTimeout: time.Duration(cfg.WebServerReadTimeout) * time.Second,
WriteTimeout: time.Duration(cfg.WebServerWriteTimeout) * time.Second,
IdleTimeout: time.Duration(cfg.WebServerIdleTimeout) * time.Second,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
Certificates: []tls.Certificate{
serverCertificate,
},
},
}
ah := &admissionHandler{
decoder: serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer(),
config: cfg,
}
// pod admission
path.HandleFunc("/api/v1/admit/pod", ah.ahServe(operations.PodsValidation()))
// deployment admission
path.HandleFunc("/api/v1/admit/deployment", ah.ahServe(operations.DeploymentsValidation()))
// pod mutation
path.HandleFunc("/api/v1/mutate/pod", ah.ahServe(operations.PodsMutation()))
// web root
path.HandleFunc("/", webServe())
if err := connection.ListenAndServeTLS("", ""); err != nil {
log.Fatalf("[ERROR] %s\n", err)
}
}
func webServe() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
httpAccessLog(r)
crossSiteOrigin(w)
strictTransport(w)
switch {
case r.Method != http.MethodGet:
msg := fmt.Sprintf("incorrect method: got request type %s, expected request type %s", r.Method, http.MethodPost)
log.Printf("[DEBUG] %s", msg)
tmpltError(w, http.StatusMethodNotAllowed, msg)
case r.URL.Path == "/api/v1/admin":
tmpltAdminToggle(w, r.URL.Query())
case r.URL.Path == "/healthcheck":
tmpltHealthCheck(w)
case r.URL.Path == "/":
tmpltWebRoot(w)
default:
msg := fmt.Sprintf("Unable to locate requested path: '%s'", r.URL.Path)
log.Printf("[DEBUG] %s", msg)
tmpltError(w, http.StatusNotFound, msg)
}
}
}
type admissionHandler struct {
decoder runtime.Decoder
config *config.Config
}
func (h *admissionHandler) ahServe(hook operations.Hook) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
httpAccessLog(r)
crossSiteOrigin(w)
strictTransport(w)
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
msg := fmt.Sprintf("incorrect method: got request type %s, expected request type %s", r.Method, http.MethodPost)
log.Printf("[DEBUG] %s", msg)
tmpltError(w, http.StatusMethodNotAllowed, msg)
return
}
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
msg := "only content type 'application/json' is supported"
log.Printf("[DEBUG] %s", msg)
tmpltError(w, http.StatusBadRequest, msg)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
msg := fmt.Sprintf("could not read request body: %v", err)
log.Printf("[DEBUG] %s", msg)
tmpltError(w, http.StatusBadRequest, msg)
return
}
var review admission.AdmissionReview
if _, _, err := h.decoder.Decode(body, nil, &review); err != nil {
msg := fmt.Sprintf("could not deserialize request: %v", err)
log.Printf("[DEBUG] %s", msg)
tmpltError(w, http.StatusBadRequest, msg)
return
}
if review.Request == nil {
msg := "malformed admission review: request is nil"
log.Printf("[DEBUG] %s", msg)
tmpltError(w, http.StatusBadRequest, msg)
return
}
result, err := hook.Execute(review.Request, &cfg)
if err != nil {
msg := err.Error()
log.Printf("[ERROR] Internal Server Error: %s", msg)
tmpltError(w, http.StatusInternalServerError, msg)
return
}
admissionResponse := admission.AdmissionReview{
Response: &admission.AdmissionResponse{
UID: review.Request.UID,
Allowed: result.Allowed,
Result: &meta.Status{Message: result.Msg},
},
}
// set the patch operations for mutating admission
if len(result.PatchOps) > 0 {
patchBytes, err := json.Marshal(result.PatchOps)
if err != nil {
msg := fmt.Sprintf("could not marshal JSON patch: %v", err)
log.Printf("[ERROR] %s", msg)
tmpltError(w, http.StatusInternalServerError, msg)
}
admissionResponse.Response.Patch = patchBytes
}
res, err := json.Marshal(admissionResponse)
if err != nil {
msg := fmt.Sprintf("could not marshal response: %v", err)
log.Printf("[ERROR] %s", msg)
tmpltError(w, http.StatusInternalServerError, msg)
return
}
log.Printf("[DEBUG] Webhook [%s] - Allowed: %t", review.Request.Operation, result.Allowed)
w.WriteHeader(http.StatusOK)
w.Write(res)
}
}