github.com/k8snetworkplumbingwg/sriov-network-operator@v1.2.1-0.20240408194816-2d2e5a45d453/cmd/webhook/start.go (about)

     1  package main
     2  
     3  import (
     4  	"crypto/tls"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"reflect"
    10  
    11  	"github.com/fsnotify/fsnotify"
    12  	"github.com/spf13/cobra"
    13  	v1 "k8s.io/api/admission/v1"
    14  	"k8s.io/apimachinery/pkg/runtime"
    15  	"sigs.k8s.io/controller-runtime/pkg/log"
    16  
    17  	snolog "github.com/k8snetworkplumbingwg/sriov-network-operator/pkg/log"
    18  	"github.com/k8snetworkplumbingwg/sriov-network-operator/pkg/webhook"
    19  )
    20  
    21  var (
    22  	certFile    string
    23  	keyFile     string
    24  	port        int
    25  	enableHTTP2 bool
    26  )
    27  
    28  var (
    29  	startCmd = &cobra.Command{
    30  		Use:   "start",
    31  		Short: "Starts Webhook Daemon",
    32  		Long:  "Starts Webhook Daemon",
    33  		Run:   runStartCmd,
    34  	}
    35  )
    36  
    37  // admitv1Func handles a v1 admission
    38  type admitv1Func func(v1.AdmissionReview) *v1.AdmissionResponse
    39  
    40  // admitHandler is a handler, for both validators and mutators, that supports multiple admission review versions
    41  type admitHandler struct {
    42  	v1 admitv1Func
    43  }
    44  
    45  func init() {
    46  	rootCmd.AddCommand(startCmd)
    47  
    48  	startCmd.Flags().StringVar(&certFile, "tls-cert-file", "",
    49  		"File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated after server cert).")
    50  	startCmd.Flags().StringVar(&keyFile, "tls-private-key-file", "",
    51  		"File containing the default x509 private key matching --tls-cert-file.")
    52  	startCmd.Flags().IntVar(&port, "port", 443,
    53  		"Secure port that the webhook listens on")
    54  	startCmd.Flags().BoolVar(&enableHTTP2, "enable-http2", false, "If HTTP/2 should be enabled for the metrics and webhook servers.")
    55  }
    56  
    57  // serve handles the http portion of a request prior to handing to an admit
    58  // function
    59  func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) {
    60  	serveLog := log.Log.WithName("serve")
    61  
    62  	var body []byte
    63  	if r.Body != nil {
    64  		if data, err := io.ReadAll(r.Body); err == nil {
    65  			body = data
    66  		}
    67  	}
    68  
    69  	// verify the content type is accurate
    70  	contentType := r.Header.Get("Content-Type")
    71  	if contentType != "application/json" {
    72  		serveLog.Error(fmt.Errorf("unexpected content type"),
    73  			"expect Content-Type application/json", "Content-Type", contentType)
    74  		return
    75  	}
    76  
    77  	serveLog.V(2).Info("handling request", "request-body", string(body))
    78  
    79  	deserializer := webhook.Codecs.UniversalDeserializer()
    80  	obj, gvk, err := deserializer.Decode(body, nil, nil)
    81  	if err != nil {
    82  		msg := fmt.Sprintf("Request could not be decoded: %v", err)
    83  		serveLog.Error(nil, msg)
    84  		http.Error(w, msg, http.StatusBadRequest)
    85  		return
    86  	}
    87  
    88  	var responseObj runtime.Object
    89  	switch *gvk {
    90  	case v1.SchemeGroupVersion.WithKind("AdmissionReview"):
    91  		requestedAdmissionReview, ok := obj.(*v1.AdmissionReview)
    92  		if !ok {
    93  			err := fmt.Errorf("unexpected object")
    94  			serveLog.Error(err, "Expected v1.AdmissionReview", "Actual-Type", reflect.TypeOf(obj).String())
    95  			return
    96  		}
    97  		responseAdmissionReview := &v1.AdmissionReview{}
    98  		responseAdmissionReview.SetGroupVersionKind(*gvk)
    99  		responseAdmissionReview.Response = admit.v1(*requestedAdmissionReview)
   100  		responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
   101  		responseObj = responseAdmissionReview
   102  	default:
   103  		msg := fmt.Sprintf("Unsupported group version kind: %v", gvk)
   104  		serveLog.Error(nil, msg)
   105  		http.Error(w, msg, http.StatusBadRequest)
   106  		return
   107  	}
   108  
   109  	respBytes, err := json.Marshal(responseObj)
   110  	if err != nil {
   111  		serveLog.Error(err, "failed to marshal response object")
   112  		http.Error(w, err.Error(), http.StatusInternalServerError)
   113  		return
   114  	}
   115  	serveLog.V(2).Info("sending response", "response", string(respBytes[:]))
   116  	w.Header().Set("Content-Type", "application/json")
   117  	if _, err := w.Write(respBytes); err != nil {
   118  		serveLog.Error(err, "failed to write response")
   119  	}
   120  }
   121  
   122  func serveMutateCustomResource(w http.ResponseWriter, r *http.Request) {
   123  	serve(w, r, newDelegateToV1AdmitHandler(webhook.MutateCustomResource))
   124  }
   125  
   126  func serveValidateCustomResource(w http.ResponseWriter, r *http.Request) {
   127  	serve(w, r, newDelegateToV1AdmitHandler(webhook.ValidateCustomResource))
   128  }
   129  
   130  func newDelegateToV1AdmitHandler(f admitv1Func) admitHandler {
   131  	return admitHandler{
   132  		v1: f,
   133  	}
   134  }
   135  
   136  func runStartCmd(cmd *cobra.Command, args []string) {
   137  	// init logger
   138  	snolog.InitLog()
   139  	setupLog := log.Log.WithName("sriov-network-operator-webhook")
   140  
   141  	setupLog.Info("Run sriov-network-operator-webhook")
   142  
   143  	if err := webhook.SetupInClusterClient(); err != nil {
   144  		setupLog.Error(err, "failed to setup in-cluster client")
   145  		panic(err)
   146  	}
   147  
   148  	if err := webhook.RetriveSupportedNics(); err != nil {
   149  		setupLog.Error(err, "failed to retrieve supported NICs")
   150  		panic(err)
   151  	}
   152  
   153  	keyPair, err := webhook.NewTLSKeypairReloader(certFile, keyFile)
   154  	if err != nil {
   155  		setupLog.Error(err, "failed to load certificates", "cert-file", certFile, "key-file", keyFile)
   156  		panic(err)
   157  	}
   158  
   159  	http.HandleFunc("/mutating-custom-resource", serveMutateCustomResource)
   160  	http.HandleFunc("/validating-custom-resource", serveValidateCustomResource)
   161  	http.HandleFunc("/readyz", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("ok")) })
   162  
   163  	go func() {
   164  		setupLog.Info("start server")
   165  		server := &http.Server{
   166  			Addr: fmt.Sprintf(":%d", port),
   167  			TLSConfig: &tls.Config{
   168  				GetCertificate: keyPair.GetCertificateFunc(),
   169  			},
   170  			// CVE-2023-39325 https://github.com/golang/go/issues/63417
   171  			TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
   172  		}
   173  		if enableHTTP2 {
   174  			server.TLSNextProto = nil
   175  		}
   176  		err := server.ListenAndServeTLS("", "")
   177  		if err != nil {
   178  			setupLog.Error(err, "failed to listen for requests")
   179  			panic(err)
   180  		}
   181  	}()
   182  	/* watch the cert file and restart http sever if the file updated. */
   183  	watcher, err := fsnotify.NewWatcher()
   184  	if err != nil {
   185  		setupLog.Error(err, "error starting fsnotify watcher")
   186  		panic(err)
   187  	}
   188  	defer watcher.Close()
   189  
   190  	certUpdated := false
   191  	keyUpdated := false
   192  
   193  	for {
   194  		watcher.Add(certFile)
   195  		watcher.Add(keyFile)
   196  
   197  		select {
   198  		case event, ok := <-watcher.Events:
   199  			if !ok {
   200  				continue
   201  			}
   202  			setupLog.Info("watcher event", "event", event)
   203  			mask := fsnotify.Create | fsnotify.Rename | fsnotify.Remove |
   204  				fsnotify.Write | fsnotify.Chmod
   205  			if (event.Op & mask) != 0 {
   206  				setupLog.Info("modified file", "name", event.Name)
   207  				if event.Name == certFile {
   208  					certUpdated = true
   209  				}
   210  				if event.Name == keyFile {
   211  					keyUpdated = true
   212  				}
   213  				if keyUpdated && certUpdated {
   214  					if err := keyPair.Reload(); err != nil {
   215  						setupLog.Error(err, "failed to reload certificate")
   216  						panic("failed to reload certificate")
   217  					}
   218  					certUpdated = false
   219  					keyUpdated = false
   220  				}
   221  			}
   222  		case err, ok := <-watcher.Errors:
   223  			if !ok {
   224  				continue
   225  			}
   226  			setupLog.Error(err, "watcher error")
   227  		}
   228  	}
   229  }