2020-10-21 05:49:41 +00:00
|
|
|
/*
|
|
|
|
Copyright 2018 The Kubernetes Authors.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package webhook
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strconv"
|
|
|
|
"sync"
|
2021-09-02 12:01:06 +00:00
|
|
|
"time"
|
2020-10-21 05:49:41 +00:00
|
|
|
|
2021-06-25 05:02:01 +00:00
|
|
|
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
|
2021-09-02 12:01:06 +00:00
|
|
|
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
2021-12-08 13:50:47 +00:00
|
|
|
"sigs.k8s.io/controller-runtime/pkg/internal/httpserver"
|
2020-10-21 05:49:41 +00:00
|
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics"
|
|
|
|
)
|
|
|
|
|
|
|
|
// DefaultPort is the default port that the webhook server serves.
|
2021-06-25 05:02:01 +00:00
|
|
|
var DefaultPort = 9443
|
2020-10-21 05:49:41 +00:00
|
|
|
|
|
|
|
// Server is an admission webhook server that can serve traffic and
|
|
|
|
// generates related k8s resources for deploying.
|
2021-06-25 05:02:01 +00:00
|
|
|
//
|
|
|
|
// TLS is required for a webhook to be accessed by kubernetes, so
|
|
|
|
// you must provide a CertName and KeyName or have valid cert/key
|
|
|
|
// at the default locations (tls.crt and tls.key). If you do not
|
|
|
|
// want to configure TLS (i.e for testing purposes) run an
|
|
|
|
// admission.StandaloneWebhook in your own server.
|
2023-06-01 17:01:19 +00:00
|
|
|
type Server interface {
|
|
|
|
// NeedLeaderElection implements the LeaderElectionRunnable interface, which indicates
|
|
|
|
// the webhook server doesn't need leader election.
|
|
|
|
NeedLeaderElection() bool
|
|
|
|
|
|
|
|
// Register marks the given webhook as being served at the given path.
|
|
|
|
// It panics if two hooks are registered on the same path.
|
|
|
|
Register(path string, hook http.Handler)
|
|
|
|
|
|
|
|
// Start runs the server.
|
|
|
|
// It will install the webhook related resources depend on the server configuration.
|
|
|
|
Start(ctx context.Context) error
|
|
|
|
|
|
|
|
// StartedChecker returns an healthz.Checker which is healthy after the
|
|
|
|
// server has been started.
|
|
|
|
StartedChecker() healthz.Checker
|
|
|
|
|
|
|
|
// WebhookMux returns the servers WebhookMux
|
|
|
|
WebhookMux() *http.ServeMux
|
|
|
|
}
|
|
|
|
|
|
|
|
// Options are all the available options for a webhook.Server
|
|
|
|
type Options struct {
|
2020-10-21 05:49:41 +00:00
|
|
|
// Host is the address that the server will listen on.
|
|
|
|
// Defaults to "" - all addresses.
|
|
|
|
Host string
|
|
|
|
|
|
|
|
// Port is the port number that the server will serve.
|
2021-06-25 05:02:01 +00:00
|
|
|
// It will be defaulted to 9443 if unspecified.
|
2020-10-21 05:49:41 +00:00
|
|
|
Port int
|
|
|
|
|
2023-08-28 20:44:55 +00:00
|
|
|
// CertDir is the directory that contains the server key and certificate. Defaults to
|
|
|
|
// <temp-dir>/k8s-webhook-server/serving-certs.
|
2020-10-21 05:49:41 +00:00
|
|
|
CertDir string
|
|
|
|
|
|
|
|
// CertName is the server certificate name. Defaults to tls.crt.
|
2023-06-01 17:01:19 +00:00
|
|
|
//
|
2023-08-28 20:44:55 +00:00
|
|
|
// Note: This option is only used when TLSOpts does not set GetCertificate.
|
2020-10-21 05:49:41 +00:00
|
|
|
CertName string
|
|
|
|
|
|
|
|
// KeyName is the server key name. Defaults to tls.key.
|
2023-06-01 17:01:19 +00:00
|
|
|
//
|
2023-08-28 20:44:55 +00:00
|
|
|
// Note: This option is only used when TLSOpts does not set GetCertificate.
|
2020-10-21 05:49:41 +00:00
|
|
|
KeyName string
|
|
|
|
|
|
|
|
// ClientCAName is the CA certificate name which server used to verify remote(client)'s certificate.
|
|
|
|
// Defaults to "", which means server does not verify client's certificate.
|
|
|
|
ClientCAName string
|
|
|
|
|
2023-08-28 20:44:55 +00:00
|
|
|
// TLSOpts is used to allow configuring the TLS config used for the server.
|
|
|
|
// This also allows providing a certificate via GetCertificate.
|
2023-02-01 17:06:36 +00:00
|
|
|
TLSOpts []func(*tls.Config)
|
|
|
|
|
2020-10-21 05:49:41 +00:00
|
|
|
// WebhookMux is the multiplexer that handles different webhooks.
|
|
|
|
WebhookMux *http.ServeMux
|
2023-06-01 17:01:19 +00:00
|
|
|
}
|
2020-10-21 05:49:41 +00:00
|
|
|
|
2023-08-28 20:44:55 +00:00
|
|
|
// NewServer constructs a new webhook.Server from the provided options.
|
2023-06-01 17:01:19 +00:00
|
|
|
func NewServer(o Options) Server {
|
|
|
|
return &DefaultServer{
|
|
|
|
Options: o,
|
|
|
|
}
|
|
|
|
}
|
2020-10-21 05:49:41 +00:00
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
// DefaultServer is the default implementation used for Server.
|
|
|
|
type DefaultServer struct {
|
|
|
|
Options Options
|
|
|
|
|
|
|
|
// webhooks keep track of all registered webhooks
|
|
|
|
webhooks map[string]http.Handler
|
2020-10-21 05:49:41 +00:00
|
|
|
|
|
|
|
// defaultingOnce ensures that the default fields are only ever set once.
|
|
|
|
defaultingOnce sync.Once
|
2021-06-25 05:02:01 +00:00
|
|
|
|
2021-09-02 12:01:06 +00:00
|
|
|
// started is set to true immediately before the server is started
|
|
|
|
// and thus can be used to check if the server has been started
|
|
|
|
started bool
|
|
|
|
|
2021-06-25 05:02:01 +00:00
|
|
|
// mu protects access to the webhook map & setFields for Start, Register, etc
|
|
|
|
mu sync.Mutex
|
2023-06-01 17:01:19 +00:00
|
|
|
|
|
|
|
webhookMux *http.ServeMux
|
2020-10-21 05:49:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// setDefaults does defaulting for the Server.
|
2023-06-01 17:01:19 +00:00
|
|
|
func (o *Options) setDefaults() {
|
|
|
|
if o.WebhookMux == nil {
|
|
|
|
o.WebhookMux = http.NewServeMux()
|
2020-10-21 05:49:41 +00:00
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
if o.Port <= 0 {
|
|
|
|
o.Port = DefaultPort
|
2020-10-21 05:49:41 +00:00
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
if len(o.CertDir) == 0 {
|
|
|
|
o.CertDir = filepath.Join(os.TempDir(), "k8s-webhook-server", "serving-certs")
|
2020-10-21 05:49:41 +00:00
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
if len(o.CertName) == 0 {
|
|
|
|
o.CertName = "tls.crt"
|
2020-10-21 05:49:41 +00:00
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
if len(o.KeyName) == 0 {
|
|
|
|
o.KeyName = "tls.key"
|
2020-10-21 05:49:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
func (s *DefaultServer) setDefaults() {
|
|
|
|
s.webhooks = map[string]http.Handler{}
|
|
|
|
s.Options.setDefaults()
|
|
|
|
|
|
|
|
s.webhookMux = s.Options.WebhookMux
|
|
|
|
}
|
|
|
|
|
2020-10-21 05:49:41 +00:00
|
|
|
// NeedLeaderElection implements the LeaderElectionRunnable interface, which indicates
|
|
|
|
// the webhook server doesn't need leader election.
|
2023-06-01 17:01:19 +00:00
|
|
|
func (*DefaultServer) NeedLeaderElection() bool {
|
2020-10-21 05:49:41 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register marks the given webhook as being served at the given path.
|
|
|
|
// It panics if two hooks are registered on the same path.
|
2023-06-01 17:01:19 +00:00
|
|
|
func (s *DefaultServer) Register(path string, hook http.Handler) {
|
2021-06-25 05:02:01 +00:00
|
|
|
s.mu.Lock()
|
|
|
|
defer s.mu.Unlock()
|
|
|
|
|
2020-10-21 05:49:41 +00:00
|
|
|
s.defaultingOnce.Do(s.setDefaults)
|
2021-06-25 05:02:01 +00:00
|
|
|
if _, found := s.webhooks[path]; found {
|
2020-10-21 05:49:41 +00:00
|
|
|
panic(fmt.Errorf("can't register duplicate path: %v", path))
|
|
|
|
}
|
|
|
|
s.webhooks[path] = hook
|
2023-06-01 17:01:19 +00:00
|
|
|
s.webhookMux.Handle(path, metrics.InstrumentedHook(path, hook))
|
2021-06-25 05:02:01 +00:00
|
|
|
|
|
|
|
regLog := log.WithValues("path", path)
|
2021-11-15 20:30:19 +00:00
|
|
|
regLog.Info("Registering webhook")
|
2021-06-25 05:02:01 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 05:49:41 +00:00
|
|
|
// Start runs the server.
|
|
|
|
// It will install the webhook related resources depend on the server configuration.
|
2023-06-01 17:01:19 +00:00
|
|
|
func (s *DefaultServer) Start(ctx context.Context) error {
|
2020-10-21 05:49:41 +00:00
|
|
|
s.defaultingOnce.Do(s.setDefaults)
|
|
|
|
|
2023-08-28 20:44:55 +00:00
|
|
|
log.Info("Starting webhook server")
|
2020-10-21 05:49:41 +00:00
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
cfg := &tls.Config{ //nolint:gosec
|
|
|
|
NextProtos: []string{"h2"},
|
|
|
|
}
|
|
|
|
// fallback TLS config ready, will now mutate if passer wants full control over it
|
|
|
|
for _, op := range s.Options.TLSOpts {
|
|
|
|
op(cfg)
|
2021-06-25 05:02:01 +00:00
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
if cfg.GetCertificate == nil {
|
|
|
|
certPath := filepath.Join(s.Options.CertDir, s.Options.CertName)
|
|
|
|
keyPath := filepath.Join(s.Options.CertDir, s.Options.KeyName)
|
|
|
|
|
|
|
|
// Create the certificate watcher and
|
|
|
|
// set the config's GetCertificate on the TLSConfig
|
|
|
|
certWatcher, err := certwatcher.New(certPath, keyPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
cfg.GetCertificate = certWatcher.GetCertificate
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
if err := certWatcher.Start(ctx); err != nil {
|
|
|
|
log.Error(err, "certificate watcher error")
|
|
|
|
}
|
|
|
|
}()
|
2020-10-21 05:49:41 +00:00
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
// Load CA to verify client certificate, if configured.
|
|
|
|
if s.Options.ClientCAName != "" {
|
2020-10-21 05:49:41 +00:00
|
|
|
certPool := x509.NewCertPool()
|
2023-06-01 17:01:19 +00:00
|
|
|
clientCABytes, err := os.ReadFile(filepath.Join(s.Options.CertDir, s.Options.ClientCAName))
|
2020-10-21 05:49:41 +00:00
|
|
|
if err != nil {
|
2023-02-01 17:06:36 +00:00
|
|
|
return fmt.Errorf("failed to read client CA cert: %w", err)
|
2020-10-21 05:49:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ok := certPool.AppendCertsFromPEM(clientCABytes)
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("failed to append client CA cert to CA pool")
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg.ClientCAs = certPool
|
|
|
|
cfg.ClientAuth = tls.RequireAndVerifyClientCert
|
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
listener, err := tls.Listen("tcp", net.JoinHostPort(s.Options.Host, strconv.Itoa(s.Options.Port)), cfg)
|
2020-10-21 05:49:41 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
log.Info("Serving webhook server", "host", s.Options.Host, "port", s.Options.Port)
|
2020-10-21 05:49:41 +00:00
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
srv := httpserver.New(s.webhookMux)
|
2020-10-21 05:49:41 +00:00
|
|
|
|
|
|
|
idleConnsClosed := make(chan struct{})
|
|
|
|
go func() {
|
2021-06-25 05:02:01 +00:00
|
|
|
<-ctx.Done()
|
2023-06-01 17:01:19 +00:00
|
|
|
log.Info("Shutting down webhook server with timeout of 1 minute")
|
2020-10-21 05:49:41 +00:00
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
|
|
|
defer cancel()
|
|
|
|
if err := srv.Shutdown(ctx); err != nil {
|
2020-10-21 05:49:41 +00:00
|
|
|
// Error from closing listeners, or context timeout
|
|
|
|
log.Error(err, "error shutting down the HTTP server")
|
|
|
|
}
|
|
|
|
close(idleConnsClosed)
|
|
|
|
}()
|
|
|
|
|
2021-09-02 12:01:06 +00:00
|
|
|
s.mu.Lock()
|
|
|
|
s.started = true
|
|
|
|
s.mu.Unlock()
|
2021-06-25 05:02:01 +00:00
|
|
|
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
2020-10-21 05:49:41 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
<-idleConnsClosed
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:01:06 +00:00
|
|
|
// StartedChecker returns an healthz.Checker which is healthy after the
|
|
|
|
// server has been started.
|
2023-06-01 17:01:19 +00:00
|
|
|
func (s *DefaultServer) StartedChecker() healthz.Checker {
|
2021-09-02 12:01:06 +00:00
|
|
|
config := &tls.Config{
|
2023-02-01 17:06:36 +00:00
|
|
|
InsecureSkipVerify: true, //nolint:gosec // config is used to connect to our own webhook port.
|
2021-09-02 12:01:06 +00:00
|
|
|
}
|
|
|
|
return func(req *http.Request) error {
|
|
|
|
s.mu.Lock()
|
|
|
|
defer s.mu.Unlock()
|
|
|
|
|
|
|
|
if !s.started {
|
|
|
|
return fmt.Errorf("webhook server has not been started yet")
|
|
|
|
}
|
|
|
|
|
|
|
|
d := &net.Dialer{Timeout: 10 * time.Second}
|
2023-06-01 17:01:19 +00:00
|
|
|
conn, err := tls.DialWithDialer(d, "tcp", net.JoinHostPort(s.Options.Host, strconv.Itoa(s.Options.Port)), config)
|
2021-09-02 12:01:06 +00:00
|
|
|
if err != nil {
|
2023-02-01 17:06:36 +00:00
|
|
|
return fmt.Errorf("webhook server is not reachable: %w", err)
|
2021-09-02 12:01:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := conn.Close(); err != nil {
|
2023-02-01 17:06:36 +00:00
|
|
|
return fmt.Errorf("webhook server is not reachable: closing connection: %w", err)
|
2021-09-02 12:01:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
// WebhookMux returns the servers WebhookMux
|
|
|
|
func (s *DefaultServer) WebhookMux() *http.ServeMux {
|
|
|
|
return s.webhookMux
|
2020-10-21 05:49:41 +00:00
|
|
|
}
|