github.com/argoproj/argo-events@v1.9.1/webhook/webhook.go (about)

     1  package webhook
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"crypto/x509"
     7  	"encoding/json"
     8  	"fmt"
     9  	"net/http"
    10  	"reflect"
    11  	"sort"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/go-openapi/inflect"
    16  	"go.uber.org/zap"
    17  	admissionv1 "k8s.io/api/admission/v1"
    18  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    19  	appsv1 "k8s.io/api/apps/v1"
    20  	corev1 "k8s.io/api/core/v1"
    21  	rbacv1 "k8s.io/api/rbac/v1"
    22  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/apimachinery/pkg/runtime"
    25  	"k8s.io/apimachinery/pkg/runtime/schema"
    26  	"k8s.io/client-go/kubernetes"
    27  	clientadmissionregistrationv1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1"
    28  
    29  	"github.com/argoproj/argo-events/common/logging"
    30  	commontls "github.com/argoproj/argo-events/common/tls"
    31  	eventbusclient "github.com/argoproj/argo-events/pkg/client/eventbus/clientset/versioned"
    32  	eventsourceclient "github.com/argoproj/argo-events/pkg/client/eventsource/clientset/versioned"
    33  	sensorclient "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned"
    34  	"github.com/argoproj/argo-events/webhook/validator"
    35  )
    36  
    37  const (
    38  	secretServerKey  = "server-key.pem"
    39  	secretServerCert = "server-cert.pem"
    40  	secretCACert     = "ca-cert.pem"
    41  
    42  	certOrg = "io.argoproj"
    43  )
    44  
    45  // Options contains the configuration for the webhook
    46  type Options struct {
    47  	// WebhookName is the name of the webhook
    48  	WebhookName string
    49  
    50  	// ServiceName is the service name of the webhook.
    51  	ServiceName string
    52  
    53  	// DeploymentName is the deployment name of the webhook.
    54  	DeploymentName string
    55  
    56  	// ClusterRoleName is the cluster role name of the webhook
    57  	ClusterRoleName string
    58  
    59  	// SecretName is the name of k8s secret that contains the webhook
    60  	// server key/cert and corresponding CA cert that signed them. The
    61  	// server key/cert are used to serve the webhook and the CA cert
    62  	// is provided to k8s apiserver during admission controller
    63  	// registration.
    64  	SecretName string
    65  
    66  	// Namespace is the namespace in which everything above lives
    67  	Namespace string
    68  
    69  	// Port where the webhook is served. Per k8s admission
    70  	// registration requirements this should be 443 unless there is
    71  	// only a single port for the service.
    72  	Port int
    73  
    74  	// ClientAuthType declares the policy the webhook server will follow for
    75  	// TLS Client Authentication.
    76  	// The default value is tls.NoClientCert.
    77  	ClientAuth tls.ClientAuthType
    78  }
    79  
    80  // AdmissionController implements a webhook for validation
    81  type AdmissionController struct {
    82  	Client            kubernetes.Interface
    83  	EventBusClient    eventbusclient.Interface
    84  	EventSourceClient eventsourceclient.Interface
    85  	SensorClient      sensorclient.Interface
    86  
    87  	Options  Options
    88  	Handlers map[schema.GroupVersionKind]runtime.Object
    89  
    90  	Logger *zap.SugaredLogger
    91  }
    92  
    93  // Run implements the admission controller run loop.
    94  func (ac *AdmissionController) Run(ctx context.Context) error {
    95  	logger := ac.Logger
    96  	tlsConfig, caCert, err := ac.configureCerts(ctx, ac.Options.ClientAuth)
    97  	if err != nil {
    98  		logger.Errorw("Could not configure admission webhook certs", zap.Error(err))
    99  		return err
   100  	}
   101  	server := &http.Server{
   102  		Handler:   ac,
   103  		Addr:      fmt.Sprintf(":%v", ac.Options.Port),
   104  		TLSConfig: tlsConfig,
   105  	}
   106  	cl := ac.Client.AdmissionregistrationV1().ValidatingWebhookConfigurations()
   107  	if err := ac.register(ctx, cl, caCert); err != nil {
   108  		logger.Errorw("Failed to register webhook", zap.Error(err))
   109  		return err
   110  	}
   111  	logger.Info("Successfully registered webhook")
   112  
   113  	serverStartErrCh := make(chan struct{})
   114  	go func() {
   115  		if err := server.ListenAndServeTLS("", ""); err != nil {
   116  			logger.Errorw("ListenAndServeTLS for admission webhook errored out", zap.Error(err))
   117  			close(serverStartErrCh)
   118  		}
   119  	}()
   120  	select {
   121  	case <-ctx.Done():
   122  		return server.Close()
   123  	case <-serverStartErrCh:
   124  		return fmt.Errorf("webhook server failed to start")
   125  	}
   126  }
   127  
   128  // Register registers the validating admission webhook
   129  func (ac *AdmissionController) register(
   130  	ctx context.Context, client clientadmissionregistrationv1.ValidatingWebhookConfigurationInterface, caCert []byte) error {
   131  	failurePolicy := admissionregistrationv1.Ignore
   132  
   133  	var rules []admissionregistrationv1.RuleWithOperations
   134  	for gvk := range ac.Handlers {
   135  		plural := strings.ToLower(inflect.Pluralize(gvk.Kind))
   136  
   137  		rules = append(rules, admissionregistrationv1.RuleWithOperations{
   138  			Operations: []admissionregistrationv1.OperationType{
   139  				admissionregistrationv1.Create,
   140  				admissionregistrationv1.Update,
   141  				admissionregistrationv1.Delete,
   142  			},
   143  			Rule: admissionregistrationv1.Rule{
   144  				APIGroups:   []string{gvk.Group},
   145  				APIVersions: []string{gvk.Version},
   146  				Resources:   []string{plural},
   147  			},
   148  		})
   149  	}
   150  
   151  	// sort
   152  	sort.Slice(rules, func(i, j int) bool {
   153  		lhs, rhs := rules[i], rules[j]
   154  		if lhs.APIGroups[0] != rhs.APIGroups[0] {
   155  			return lhs.APIGroups[0] < rhs.APIGroups[0]
   156  		}
   157  		if lhs.APIVersions[0] != rhs.APIVersions[0] {
   158  			return lhs.APIVersions[0] < rhs.APIVersions[0]
   159  		}
   160  		return lhs.Resources[0] < rhs.Resources[0]
   161  	})
   162  
   163  	sideEffects := admissionregistrationv1.SideEffectClassNone
   164  
   165  	port := int32(ac.Options.Port)
   166  	webhook := &admissionregistrationv1.ValidatingWebhookConfiguration{
   167  		ObjectMeta: metav1.ObjectMeta{
   168  			Name: ac.Options.WebhookName,
   169  		},
   170  		Webhooks: []admissionregistrationv1.ValidatingWebhook{{
   171  			Name:                    ac.Options.WebhookName,
   172  			Rules:                   rules,
   173  			SideEffects:             &sideEffects,
   174  			AdmissionReviewVersions: []string{"v1", "v1beta1"},
   175  			ClientConfig: admissionregistrationv1.WebhookClientConfig{
   176  				Service: &admissionregistrationv1.ServiceReference{
   177  					Namespace: ac.Options.Namespace,
   178  					Name:      ac.Options.ServiceName,
   179  					Port:      &port,
   180  				},
   181  				CABundle: caCert,
   182  			},
   183  			FailurePolicy: &failurePolicy,
   184  		}},
   185  	}
   186  	clusterRole, err := ac.Client.RbacV1().ClusterRoles().Get(ctx, ac.Options.ClusterRoleName, metav1.GetOptions{})
   187  	if err != nil {
   188  		return fmt.Errorf("failed to fetch webhook cluster role, %w", err)
   189  	}
   190  	clusterRoleRef := metav1.NewControllerRef(clusterRole, rbacv1.SchemeGroupVersion.WithKind("ClusterRole"))
   191  	webhook.OwnerReferences = append(webhook.OwnerReferences, *clusterRoleRef)
   192  
   193  	_, err = client.Create(ctx, webhook, metav1.CreateOptions{})
   194  	if err != nil {
   195  		if !apierrors.IsAlreadyExists(err) {
   196  			return fmt.Errorf("failed to create a webhook, %w", err)
   197  		}
   198  		ac.Logger.Info("Webhook already exists")
   199  		configuredWebhook, err := client.Get(ctx, ac.Options.WebhookName, metav1.GetOptions{})
   200  		if err != nil {
   201  			return fmt.Errorf("failed to retrieve webhook, %w", err)
   202  		}
   203  		if !reflect.DeepEqual(configuredWebhook.Webhooks, webhook.Webhooks) {
   204  			ac.Logger.Info("Updating webhook")
   205  			// Set the ResourceVersion as required by update.
   206  			webhook.ObjectMeta.ResourceVersion = configuredWebhook.ObjectMeta.ResourceVersion
   207  			if _, err := client.Update(ctx, webhook, metav1.UpdateOptions{}); err != nil {
   208  				return fmt.Errorf("failed to update webhook, %w", err)
   209  			}
   210  		} else {
   211  			ac.Logger.Info("Webhook is valid")
   212  		}
   213  	} else {
   214  		ac.Logger.Info("Created a webhook")
   215  	}
   216  	return nil
   217  }
   218  
   219  // ServeHTTP implements the validating admission webhook
   220  func (ac *AdmissionController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   221  	ac.Logger.Infof("Webhook ServeHTTP request=%#v", r)
   222  
   223  	// content type validation
   224  	contentType := r.Header.Get("Content-Type")
   225  	if contentType != "application/json" {
   226  		http.Error(w, "invalid Content-Type, want `application/json`", http.StatusUnsupportedMediaType)
   227  		return
   228  	}
   229  
   230  	var review admissionv1.AdmissionReview
   231  	defer r.Body.Close()
   232  	if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
   233  		http.Error(w, fmt.Sprintf("could not decode body: %v", err), http.StatusBadRequest)
   234  		return
   235  	}
   236  	logger := ac.Logger.With("kind", fmt.Sprint(review.Request.Kind)).
   237  		With("namespace", review.Request.Namespace).
   238  		With("name", review.Request.Name).
   239  		With("operation", fmt.Sprint(review.Request.Operation)).
   240  		With("resource", fmt.Sprint(review.Request.Resource)).
   241  		With("subResource", fmt.Sprint(review.Request.SubResource)).
   242  		With("userInfo", fmt.Sprint(review.Request.UserInfo))
   243  
   244  	reviewResponse := ac.admit(logging.WithLogger(r.Context(), logger), review.Request)
   245  	response := admissionv1.AdmissionReview{
   246  		TypeMeta: metav1.TypeMeta{
   247  			Kind:       "AdmissionReview",
   248  			APIVersion: "admission.k8s.io/v1",
   249  		},
   250  	}
   251  	if reviewResponse != nil {
   252  		response.Response = reviewResponse
   253  		response.Response.UID = review.Request.UID
   254  	}
   255  
   256  	logger.Infof("AdmissionReview for %s: %v/%v response=%v",
   257  		review.Request.Kind, review.Request.Namespace, review.Request.Name, reviewResponse)
   258  
   259  	if err := json.NewEncoder(w).Encode(response); err != nil {
   260  		http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError)
   261  		return
   262  	}
   263  }
   264  
   265  func (ac *AdmissionController) admit(ctx context.Context, request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
   266  	log := logging.FromContext(ctx)
   267  	switch request.Operation {
   268  	case admissionv1.Create, admissionv1.Update:
   269  	default:
   270  		log.Infof("Operation not interested: %v %v", request.Kind, request.Operation)
   271  		return &admissionv1.AdmissionResponse{Allowed: true}
   272  	}
   273  	v, err := validator.GetValidator(ctx, ac.Client, ac.EventBusClient, ac.EventSourceClient, ac.SensorClient,
   274  		request.Kind, request.OldObject.Raw, request.Object.Raw)
   275  	if err != nil {
   276  		return validator.DeniedResponse("failed to get a validator: %v", err)
   277  	}
   278  
   279  	switch request.Operation {
   280  	case admissionv1.Create:
   281  		return v.ValidateCreate(ctx)
   282  	case admissionv1.Update:
   283  		return v.ValidateUpdate(ctx)
   284  	default:
   285  		return validator.AllowedResponse()
   286  	}
   287  }
   288  
   289  // Generate cert secret
   290  func (ac *AdmissionController) generateSecret(ctx context.Context) (*corev1.Secret, error) {
   291  	hosts := []string{}
   292  	hosts = append(hosts, fmt.Sprintf("%s.%s.svc.cluster.local", ac.Options.ServiceName, ac.Options.Namespace))
   293  	hosts = append(hosts, fmt.Sprintf("%s.%s.svc", ac.Options.ServiceName, ac.Options.Namespace))
   294  	serverKey, serverCert, caCert, err := commontls.CreateCerts(certOrg, hosts, time.Now().Add(10*365*24*time.Hour), true, false)
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  	deployment, err := ac.Client.AppsV1().Deployments(ac.Options.Namespace).Get(ctx, ac.Options.DeploymentName, metav1.GetOptions{})
   299  	if err != nil {
   300  		return nil, fmt.Errorf("Failed to fetch webhook deployment, %w", err)
   301  	}
   302  	deploymentRef := metav1.NewControllerRef(deployment, appsv1.SchemeGroupVersion.WithKind("Deployment"))
   303  	secret := &corev1.Secret{
   304  		ObjectMeta: metav1.ObjectMeta{
   305  			Name:      ac.Options.SecretName,
   306  			Namespace: ac.Options.Namespace,
   307  		},
   308  		Data: map[string][]byte{
   309  			secretServerKey:  serverKey,
   310  			secretServerCert: serverCert,
   311  			secretCACert:     caCert,
   312  		},
   313  	}
   314  	secret.OwnerReferences = append(secret.OwnerReferences, *deploymentRef)
   315  	return secret, nil
   316  }
   317  
   318  // getOrGenerateKeyCertsFromSecret creates CERTs if not existing and store in a secret
   319  func (ac *AdmissionController) getOrGenerateKeyCertsFromSecret(ctx context.Context) (serverKey, serverCert, caCert []byte, err error) {
   320  	secret, err := ac.Client.CoreV1().Secrets(ac.Options.Namespace).Get(ctx, ac.Options.SecretName, metav1.GetOptions{})
   321  	if err != nil {
   322  		if !apierrors.IsNotFound(err) {
   323  			return nil, nil, nil, err
   324  		}
   325  		// No existing secret, creating one
   326  		newSecret, err := ac.generateSecret(ctx)
   327  		if err != nil {
   328  			return nil, nil, nil, err
   329  		}
   330  		_, err = ac.Client.CoreV1().Secrets(newSecret.Namespace).Create(ctx, newSecret, metav1.CreateOptions{})
   331  		if err != nil && !apierrors.IsAlreadyExists(err) {
   332  			return nil, nil, nil, err
   333  		}
   334  		// Something else might have created, try fetching it one more time
   335  		secret, err = ac.Client.CoreV1().Secrets(ac.Options.Namespace).Get(ctx, ac.Options.SecretName, metav1.GetOptions{})
   336  		if err != nil {
   337  			return nil, nil, nil, err
   338  		}
   339  	}
   340  
   341  	var ok bool
   342  	if serverKey, ok = secret.Data[secretServerKey]; !ok {
   343  		return nil, nil, nil, fmt.Errorf("server key missing")
   344  	}
   345  	if serverCert, ok = secret.Data[secretServerCert]; !ok {
   346  		return nil, nil, nil, fmt.Errorf("server cert missing")
   347  	}
   348  	if caCert, ok = secret.Data[secretCACert]; !ok {
   349  		return nil, nil, nil, fmt.Errorf("ca cert missing")
   350  	}
   351  	return serverKey, serverCert, caCert, nil
   352  }
   353  
   354  // GetAPIServerExtensionCACert gets the K8s aggregate apiserver
   355  // client CA cert used by validator. This certificate is provided by
   356  // kubernetes.
   357  func (ac *AdmissionController) getAPIServerExtensionCACert(ctx context.Context) ([]byte, error) {
   358  	const name = "extension-apiserver-authentication"
   359  	c, err := ac.Client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(ctx, name, metav1.GetOptions{})
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  	const caFileName = "requestheader-client-ca-file"
   364  	pem, ok := c.Data[caFileName]
   365  	if !ok {
   366  		return nil, fmt.Errorf("cannot find %s in ConfigMap %s", caFileName, name)
   367  	}
   368  	return []byte(pem), nil
   369  }
   370  
   371  func (ac *AdmissionController) configureCerts(ctx context.Context, clientAuth tls.ClientAuthType) (*tls.Config, []byte, error) {
   372  	var apiServerCACert []byte
   373  	if clientAuth >= tls.VerifyClientCertIfGiven {
   374  		var err error
   375  		apiServerCACert, err = ac.getAPIServerExtensionCACert(ctx)
   376  		if err != nil {
   377  			return nil, nil, err
   378  		}
   379  	}
   380  
   381  	serverKey, serverCert, caCert, err := ac.getOrGenerateKeyCertsFromSecret(ctx)
   382  	if err != nil {
   383  		return nil, nil, err
   384  	}
   385  	tlsConfig, err := makeTLSConfig(serverCert, serverKey, apiServerCACert, clientAuth)
   386  	if err != nil {
   387  		return nil, nil, err
   388  	}
   389  	return tlsConfig, caCert, nil
   390  }
   391  
   392  // makeTLSConfig makes a TLS configuration
   393  func makeTLSConfig(serverCert, serverKey, caCert []byte, clientAuthType tls.ClientAuthType) (*tls.Config, error) {
   394  	caCertPool := x509.NewCertPool()
   395  	caCertPool.AppendCertsFromPEM(caCert)
   396  	cert, err := tls.X509KeyPair(serverCert, serverKey)
   397  	if err != nil {
   398  		return nil, err
   399  	}
   400  	return &tls.Config{
   401  		Certificates: []tls.Certificate{cert},
   402  		ClientCAs:    caCertPool,
   403  		ClientAuth:   clientAuthType,
   404  	}, nil
   405  }