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 }