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 }