pkg/webhook/server.go (159 lines of code) (raw):

// Copyright (c) 2021, 2023, Oracle and/or its affiliates. // // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ package webhook import ( "context" "crypto/tls" "encoding/json" "flag" "fmt" "io" "net/http" "os" "strings" "github.com/mysql/ndb-operator/pkg/controllers" "github.com/mysql/ndb-operator/pkg/helpers" admissionv1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" klog "k8s.io/klog/v2" ) const ( webHookServerAddr = ":9443" // Note: when the operator is installed using OLM, OLM will install the // certificate in the below mentioned hardcoded path certFile = "tmp/k8s-webhook-server/serving-certs/tls.crt" keyFile = "tmp/k8s-webhook-server/serving-certs/tls.key" ) // tlsData holds the pem encoded certificate and privateKey type tlsData struct { certificate, privateKey []byte } // sendAdmissionResponse sends an AdmissionResponse wrapped in a AdmissionReview back to the caller func sendAdmissionResponse(w http.ResponseWriter, response *admissionv1.AdmissionResponse) { klog.V(2).Infof("Sending response: %s", response.String()) // wrap it in a AdmissionReview and send it back responseAdmissionReview := admissionv1.AdmissionReview{ TypeMeta: metav1.TypeMeta{ Kind: "AdmissionReview", APIVersion: "admission.k8s.io/v1", }, Response: response, } w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(responseAdmissionReview) if err != nil { klog.Error(err) } } func sendAdmissionResponsePathNotFound(w http.ResponseWriter) { sendAdmissionResponse(w, requestDeniedBad("", "requested URL path not found")) } // serve handles the http portion of a request and then validates the review request func serve(w http.ResponseWriter, r *http.Request, ac admissionController, executorFunc requestExecutor) { // verify the content type is accurate contentType := r.Header.Get("Content-Type") if contentType != "application/json" { errMessage := fmt.Sprintf("Expected 'application/json' contentType but got '%s'", contentType) sendAdmissionResponse(w, requestDeniedBad("", errMessage)) return } // Read all the request body content bodyBytes, err := io.ReadAll(r.Body) if err != nil { sendAdmissionResponse(w, requestDeniedBad("", err.Error())) return } klog.V(5).Infof("Received body : %s", string(bodyBytes)) // Decode the request body content into the AdmissionReview struct requestedAdmissionReview := admissionv1.AdmissionReview{} _, _, err = scheme.Codecs.UniversalDeserializer().Decode(bodyBytes, nil, &requestedAdmissionReview) if err != nil { sendAdmissionResponse(w, requestDeniedBad("", err.Error())) return } klog.V(5).Infof("Received review request : %s", requestedAdmissionReview.String()) if requestedAdmissionReview.APIVersion != "admission.k8s.io/v1" { // Only v1 is supported errMessage := fmt.Sprintf( "Webhook can only handle API version 'admission.k8s.io/v1' but got '%s'", requestedAdmissionReview.APIVersion) sendAdmissionResponse(w, requestDeniedBad("", errMessage)) return } request := requestedAdmissionReview.Request if request == nil { sendAdmissionResponse(w, requestDeniedBad("", "Received an empty(nil) AdmissionRequest")) return } klog.Infof("Serving request with UID '%s'", request.UID) // Review the request and reply response := executorFunc(request, ac) sendAdmissionResponse(w, response) } // initWebhookServer sets up the handler and initializes the server func initWebhookServer(ws *http.Server) { // set server address ws.Addr = webHookServerAddr // setup the handlers http.HandleFunc("/health", func(writer http.ResponseWriter, request *http.Request) { // Handle readiness probe klog.V(2).Infof("Replying OK to health probe") writer.WriteHeader(http.StatusOK) }) // pattern to admissionController mapping admissionControllers := map[string]admissionController{ "ndb": newNdbAdmissionController(), } // allowed admissionController requestTypes validRequestTypes := map[string]requestExecutor{ "validate": validate, "mutate": mutate, } http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { urlTokens := strings.Split(request.URL.Path[1:], "/") if len(urlTokens) == 2 { ac := admissionControllers[urlTokens[0]] executeFunc, isValidRequestType := validRequestTypes[urlTokens[1]] if ac != nil && isValidRequestType { // Found an admissionController for the pattern serve(writer, request, ac, executeFunc) return } } // No admissionController found or invalid requestType // Handle error klog.V(2).Infof("URL path not found '%s'", request.URL.Path) sendAdmissionResponsePathNotFound(writer) }) } // setWebhookServerTLSCerts configures the server to use the TLS certificates func setWebhookServerTLSCerts(ctx context.Context, ws *http.Server) { namespace, err := helpers.GetCurrentNamespace() if err != nil { klog.Fatalf("Could not get current namespace : %s", err) } var cert tls.Certificate // Check if the certificate and key files exist _, certErr := os.Stat(certFile) _, keyErr := os.Stat(keyFile) if certErr != nil || keyErr != nil { // Either certificate or key file is missing, generate new certificate td := createCertificate(config.serviceName, namespace) // Get k8s clientset clientset := getK8sClientset() if clientset == nil { klog.Fatal("Failed to create k8s clientset") } // Update the validating webhook config with the certificate vwcInterface := controllers.NewValidatingWebhookConfigController(clientset) if !vwcInterface.UpdateWebhookConfigCertificate( ctx, "webhook-server="+namespace+"-"+config.serviceName, td.certificate) { klog.Fatal("Failed to update validating webhook configs with the new certificate") } // Update the mutating webhook config with the certificate mwcInterface := controllers.NewMutatingWebhookConfigController(clientset) if !mwcInterface.UpdateWebhookConfigCertificate( ctx, "webhook-server="+namespace+"-"+config.serviceName, td.certificate) { klog.Fatal("Failed to update mutating webhook configs with the new certificate") } // Load the TLS certificate and key cert, err = tls.X509KeyPair(td.certificate, td.privateKey) if err != nil { klog.Fatal(err) } } else { // Load the TLS certificate and key files cert, err = tls.LoadX509KeyPair(certFile, keyFile) if err != nil { klog.Fatal("Failed to load TLS certificate and key:", err) } } // Add certificate to server config ws.TLSConfig = &tls.Config{ Certificates: []tls.Certificate{cert}, } } // Run sets up a webhook server and handles incoming connections over TLS // This is the main function of this package func Run() { // Parse the arguments flag.Parse() validateCommandLineArgs() // init the server ws := &http.Server{} initWebhookServer(ws) // Setup TLS certificates setWebhookServerTLSCerts(context.Background(), ws) klog.Info("Webhook server : setup successful") // start the server klog.Info("Starting to serve...") if err := ws.ListenAndServeTLS("", ""); err != http.ErrServerClosed { klog.Errorf("Error during listen and server : %v", err) } else { klog.Info("Webhook server : closed") } }