sigs.k8s.io/controller-runtime@v0.18.2/pkg/webhook/server.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package webhook 18 19 import ( 20 "context" 21 "crypto/tls" 22 "crypto/x509" 23 "fmt" 24 "net" 25 "net/http" 26 "os" 27 "path/filepath" 28 "strconv" 29 "sync" 30 "time" 31 32 "sigs.k8s.io/controller-runtime/pkg/certwatcher" 33 "sigs.k8s.io/controller-runtime/pkg/healthz" 34 "sigs.k8s.io/controller-runtime/pkg/internal/httpserver" 35 "sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics" 36 ) 37 38 // DefaultPort is the default port that the webhook server serves. 39 var DefaultPort = 9443 40 41 // Server is an admission webhook server that can serve traffic and 42 // generates related k8s resources for deploying. 43 // 44 // TLS is required for a webhook to be accessed by kubernetes, so 45 // you must provide a CertName and KeyName or have valid cert/key 46 // at the default locations (tls.crt and tls.key). If you do not 47 // want to configure TLS (i.e for testing purposes) run an 48 // admission.StandaloneWebhook in your own server. 49 type Server interface { 50 // NeedLeaderElection implements the LeaderElectionRunnable interface, which indicates 51 // the webhook server doesn't need leader election. 52 NeedLeaderElection() bool 53 54 // Register marks the given webhook as being served at the given path. 55 // It panics if two hooks are registered on the same path. 56 Register(path string, hook http.Handler) 57 58 // Start runs the server. 59 // It will install the webhook related resources depend on the server configuration. 60 Start(ctx context.Context) error 61 62 // StartedChecker returns an healthz.Checker which is healthy after the 63 // server has been started. 64 StartedChecker() healthz.Checker 65 66 // WebhookMux returns the servers WebhookMux 67 WebhookMux() *http.ServeMux 68 } 69 70 // Options are all the available options for a webhook.Server 71 type Options struct { 72 // Host is the address that the server will listen on. 73 // Defaults to "" - all addresses. 74 Host string 75 76 // Port is the port number that the server will serve. 77 // It will be defaulted to 9443 if unspecified. 78 Port int 79 80 // CertDir is the directory that contains the server key and certificate. Defaults to 81 // <temp-dir>/k8s-webhook-server/serving-certs. 82 CertDir string 83 84 // CertName is the server certificate name. Defaults to tls.crt. 85 // 86 // Note: This option is only used when TLSOpts does not set GetCertificate. 87 CertName string 88 89 // KeyName is the server key name. Defaults to tls.key. 90 // 91 // Note: This option is only used when TLSOpts does not set GetCertificate. 92 KeyName string 93 94 // ClientCAName is the CA certificate name which server used to verify remote(client)'s certificate. 95 // Defaults to "", which means server does not verify client's certificate. 96 ClientCAName string 97 98 // TLSOpts is used to allow configuring the TLS config used for the server. 99 // This also allows providing a certificate via GetCertificate. 100 TLSOpts []func(*tls.Config) 101 102 // WebhookMux is the multiplexer that handles different webhooks. 103 WebhookMux *http.ServeMux 104 } 105 106 // NewServer constructs a new webhook.Server from the provided options. 107 func NewServer(o Options) Server { 108 return &DefaultServer{ 109 Options: o, 110 } 111 } 112 113 // DefaultServer is the default implementation used for Server. 114 type DefaultServer struct { 115 Options Options 116 117 // webhooks keep track of all registered webhooks 118 webhooks map[string]http.Handler 119 120 // defaultingOnce ensures that the default fields are only ever set once. 121 defaultingOnce sync.Once 122 123 // started is set to true immediately before the server is started 124 // and thus can be used to check if the server has been started 125 started bool 126 127 // mu protects access to the webhook map & setFields for Start, Register, etc 128 mu sync.Mutex 129 130 webhookMux *http.ServeMux 131 } 132 133 // setDefaults does defaulting for the Server. 134 func (o *Options) setDefaults() { 135 if o.WebhookMux == nil { 136 o.WebhookMux = http.NewServeMux() 137 } 138 139 if o.Port <= 0 { 140 o.Port = DefaultPort 141 } 142 143 if len(o.CertDir) == 0 { 144 o.CertDir = filepath.Join(os.TempDir(), "k8s-webhook-server", "serving-certs") 145 } 146 147 if len(o.CertName) == 0 { 148 o.CertName = "tls.crt" 149 } 150 151 if len(o.KeyName) == 0 { 152 o.KeyName = "tls.key" 153 } 154 } 155 156 func (s *DefaultServer) setDefaults() { 157 s.webhooks = map[string]http.Handler{} 158 s.Options.setDefaults() 159 160 s.webhookMux = s.Options.WebhookMux 161 } 162 163 // NeedLeaderElection implements the LeaderElectionRunnable interface, which indicates 164 // the webhook server doesn't need leader election. 165 func (*DefaultServer) NeedLeaderElection() bool { 166 return false 167 } 168 169 // Register marks the given webhook as being served at the given path. 170 // It panics if two hooks are registered on the same path. 171 func (s *DefaultServer) Register(path string, hook http.Handler) { 172 s.mu.Lock() 173 defer s.mu.Unlock() 174 175 s.defaultingOnce.Do(s.setDefaults) 176 if _, found := s.webhooks[path]; found { 177 panic(fmt.Errorf("can't register duplicate path: %v", path)) 178 } 179 s.webhooks[path] = hook 180 s.webhookMux.Handle(path, metrics.InstrumentedHook(path, hook)) 181 182 regLog := log.WithValues("path", path) 183 regLog.Info("Registering webhook") 184 } 185 186 // Start runs the server. 187 // It will install the webhook related resources depend on the server configuration. 188 func (s *DefaultServer) Start(ctx context.Context) error { 189 s.defaultingOnce.Do(s.setDefaults) 190 191 log.Info("Starting webhook server") 192 193 cfg := &tls.Config{ //nolint:gosec 194 NextProtos: []string{"h2"}, 195 } 196 // fallback TLS config ready, will now mutate if passer wants full control over it 197 for _, op := range s.Options.TLSOpts { 198 op(cfg) 199 } 200 201 if cfg.GetCertificate == nil { 202 certPath := filepath.Join(s.Options.CertDir, s.Options.CertName) 203 keyPath := filepath.Join(s.Options.CertDir, s.Options.KeyName) 204 205 // Create the certificate watcher and 206 // set the config's GetCertificate on the TLSConfig 207 certWatcher, err := certwatcher.New(certPath, keyPath) 208 if err != nil { 209 return err 210 } 211 cfg.GetCertificate = certWatcher.GetCertificate 212 213 go func() { 214 if err := certWatcher.Start(ctx); err != nil { 215 log.Error(err, "certificate watcher error") 216 } 217 }() 218 } 219 220 // Load CA to verify client certificate, if configured. 221 if s.Options.ClientCAName != "" { 222 certPool := x509.NewCertPool() 223 clientCABytes, err := os.ReadFile(filepath.Join(s.Options.CertDir, s.Options.ClientCAName)) 224 if err != nil { 225 return fmt.Errorf("failed to read client CA cert: %w", err) 226 } 227 228 ok := certPool.AppendCertsFromPEM(clientCABytes) 229 if !ok { 230 return fmt.Errorf("failed to append client CA cert to CA pool") 231 } 232 233 cfg.ClientCAs = certPool 234 cfg.ClientAuth = tls.RequireAndVerifyClientCert 235 } 236 237 listener, err := tls.Listen("tcp", net.JoinHostPort(s.Options.Host, strconv.Itoa(s.Options.Port)), cfg) 238 if err != nil { 239 return err 240 } 241 242 log.Info("Serving webhook server", "host", s.Options.Host, "port", s.Options.Port) 243 244 srv := httpserver.New(s.webhookMux) 245 246 idleConnsClosed := make(chan struct{}) 247 go func() { 248 <-ctx.Done() 249 log.Info("Shutting down webhook server with timeout of 1 minute") 250 251 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) 252 defer cancel() 253 if err := srv.Shutdown(ctx); err != nil { 254 // Error from closing listeners, or context timeout 255 log.Error(err, "error shutting down the HTTP server") 256 } 257 close(idleConnsClosed) 258 }() 259 260 s.mu.Lock() 261 s.started = true 262 s.mu.Unlock() 263 if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { 264 return err 265 } 266 267 <-idleConnsClosed 268 return nil 269 } 270 271 // StartedChecker returns an healthz.Checker which is healthy after the 272 // server has been started. 273 func (s *DefaultServer) StartedChecker() healthz.Checker { 274 config := &tls.Config{ 275 InsecureSkipVerify: true, //nolint:gosec // config is used to connect to our own webhook port. 276 } 277 return func(req *http.Request) error { 278 s.mu.Lock() 279 defer s.mu.Unlock() 280 281 if !s.started { 282 return fmt.Errorf("webhook server has not been started yet") 283 } 284 285 d := &net.Dialer{Timeout: 10 * time.Second} 286 conn, err := tls.DialWithDialer(d, "tcp", net.JoinHostPort(s.Options.Host, strconv.Itoa(s.Options.Port)), config) 287 if err != nil { 288 return fmt.Errorf("webhook server is not reachable: %w", err) 289 } 290 291 if err := conn.Close(); err != nil { 292 return fmt.Errorf("webhook server is not reachable: closing connection: %w", err) 293 } 294 295 return nil 296 } 297 } 298 299 // WebhookMux returns the servers WebhookMux 300 func (s *DefaultServer) WebhookMux() *http.ServeMux { 301 return s.webhookMux 302 }