knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/resourcesemantics/validation/reconcile_config.go (about)

     1  /*
     2  Copyright 2020 The Knative 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 validation
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/gobuffalo/flect"
    26  	"go.uber.org/zap"
    27  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/client-go/kubernetes"
    33  	admissionlisters "k8s.io/client-go/listers/admissionregistration/v1"
    34  	corelisters "k8s.io/client-go/listers/core/v1"
    35  
    36  	"knative.dev/pkg/controller"
    37  	"knative.dev/pkg/kmp"
    38  	"knative.dev/pkg/logging"
    39  	"knative.dev/pkg/ptr"
    40  	pkgreconciler "knative.dev/pkg/reconciler"
    41  	"knative.dev/pkg/system"
    42  	"knative.dev/pkg/webhook"
    43  	certresources "knative.dev/pkg/webhook/certificates/resources"
    44  	"knative.dev/pkg/webhook/resourcesemantics"
    45  )
    46  
    47  // reconciler implements the AdmissionController for resources
    48  type reconciler struct {
    49  	webhook.StatelessAdmissionImpl
    50  	pkgreconciler.LeaderAwareFuncs
    51  
    52  	key       types.NamespacedName
    53  	path      string
    54  	handlers  map[schema.GroupVersionKind]resourcesemantics.GenericCRD
    55  	callbacks map[schema.GroupVersionKind]Callback
    56  
    57  	withContext func(context.Context) context.Context
    58  
    59  	client       kubernetes.Interface
    60  	vwhlister    admissionlisters.ValidatingWebhookConfigurationLister
    61  	secretlister corelisters.SecretLister
    62  
    63  	disallowUnknownFields     bool
    64  	secretName                string
    65  	disableNamespaceOwnership bool
    66  }
    67  
    68  var (
    69  	_ controller.Reconciler                = (*reconciler)(nil)
    70  	_ pkgreconciler.LeaderAware            = (*reconciler)(nil)
    71  	_ webhook.AdmissionController          = (*reconciler)(nil)
    72  	_ webhook.StatelessAdmissionController = (*reconciler)(nil)
    73  )
    74  
    75  // Path implements AdmissionController
    76  func (ac *reconciler) Path() string {
    77  	return ac.path
    78  }
    79  
    80  // Reconcile implements controller.Reconciler
    81  func (ac *reconciler) Reconcile(ctx context.Context, key string) error {
    82  	logger := logging.FromContext(ctx)
    83  
    84  	if !ac.IsLeaderFor(ac.key) {
    85  		return controller.NewSkipKey(key)
    86  	}
    87  
    88  	// Look up the webhook secret, and fetch the CA cert bundle.
    89  	secret, err := ac.secretlister.Secrets(system.Namespace()).Get(ac.secretName)
    90  	if err != nil {
    91  		logger.Errorw("Error fetching secret", zap.Error(err))
    92  		return err
    93  	}
    94  	caCert, ok := secret.Data[certresources.CACert]
    95  	if !ok {
    96  		return fmt.Errorf("secret %q is missing %q key", ac.secretName, certresources.CACert)
    97  	}
    98  
    99  	// Reconcile the webhook configuration.
   100  	return ac.reconcileValidatingWebhook(ctx, caCert)
   101  }
   102  
   103  func (ac *reconciler) reconcileValidatingWebhook(ctx context.Context, caCert []byte) error {
   104  	logger := logging.FromContext(ctx)
   105  
   106  	rules := make([]admissionregistrationv1.RuleWithOperations, 0, len(ac.handlers)+len(ac.callbacks))
   107  	for gvk, config := range ac.handlers {
   108  		plural := strings.ToLower(flect.Pluralize(gvk.Kind))
   109  
   110  		// If SupportedVerbs has not been given, provide the legacy defaults
   111  		// of Create, Update, and Delete
   112  		supportedVerbs := []admissionregistrationv1.OperationType{
   113  			admissionregistrationv1.Create,
   114  			admissionregistrationv1.Update,
   115  			admissionregistrationv1.Delete,
   116  		}
   117  
   118  		if vl, ok := config.(resourcesemantics.VerbLimited); ok {
   119  			logging.FromContext(ctx).Debugf("Using custom Verbs")
   120  			supportedVerbs = vl.SupportedVerbs()
   121  		}
   122  		logging.FromContext(ctx).Debugf("Registering verbs: %s", supportedVerbs)
   123  
   124  		resources := []string{}
   125  		// If SupportedSubResources has not been given, provide the legacy
   126  		// defaults of main resource, and status
   127  		if srl, ok := config.(resourcesemantics.SubResourceLimited); ok {
   128  			logging.FromContext(ctx).Debugf("Using custom SubResources")
   129  			for _, subResource := range srl.SupportedSubResources() {
   130  				if subResource == "" {
   131  					// Special case the actual plural if given
   132  					resources = append(resources, plural)
   133  				} else {
   134  					resources = append(resources, plural+subResource)
   135  				}
   136  			}
   137  		} else {
   138  			resources = append(resources, plural, plural+"/status")
   139  		}
   140  		logging.FromContext(ctx).Debugf("Registering SubResources: %s", resources)
   141  		rules = append(rules, admissionregistrationv1.RuleWithOperations{
   142  			Operations: supportedVerbs,
   143  			Rule: admissionregistrationv1.Rule{
   144  				APIGroups:   []string{gvk.Group},
   145  				APIVersions: []string{gvk.Version},
   146  				Resources:   resources,
   147  			},
   148  		})
   149  	}
   150  	for gvk, callback := range ac.callbacks {
   151  		if _, ok := ac.handlers[gvk]; ok {
   152  			continue
   153  		}
   154  		plural := strings.ToLower(flect.Pluralize(gvk.Kind))
   155  		resources := []string{plural, plural + "/status"}
   156  
   157  		verbs := make([]admissionregistrationv1.OperationType, 0, len(callback.supportedVerbs))
   158  		for verb := range callback.supportedVerbs {
   159  			verbs = append(verbs, admissionregistrationv1.OperationType(verb))
   160  		}
   161  		// supportedVerbs is a map which doesn't provide a stable order in for loops.
   162  		sort.Slice(verbs, func(i, j int) bool { return string(verbs[i]) < string(verbs[j]) })
   163  
   164  		rules = append(rules, admissionregistrationv1.RuleWithOperations{
   165  			Operations: verbs,
   166  			Rule: admissionregistrationv1.Rule{
   167  				APIGroups:   []string{gvk.Group},
   168  				APIVersions: []string{gvk.Version},
   169  				Resources:   resources,
   170  			},
   171  		})
   172  	}
   173  
   174  	for _, r := range rules {
   175  		logging.FromContext(ctx).Debugf("Rule: %+v", r)
   176  	}
   177  
   178  	// Sort the rules by Group, Version, Kind so that things are deterministically ordered.
   179  	sort.Slice(rules, func(i, j int) bool {
   180  		lhs, rhs := rules[i], rules[j]
   181  		if lhs.APIGroups[0] != rhs.APIGroups[0] {
   182  			return lhs.APIGroups[0] < rhs.APIGroups[0]
   183  		}
   184  		if lhs.APIVersions[0] != rhs.APIVersions[0] {
   185  			return lhs.APIVersions[0] < rhs.APIVersions[0]
   186  		}
   187  		return lhs.Resources[0] < rhs.Resources[0]
   188  	})
   189  
   190  	configuredWebhook, err := ac.vwhlister.Get(ac.key.Name)
   191  	if err != nil {
   192  		return fmt.Errorf("error retrieving webhook: %w", err)
   193  	}
   194  
   195  	current := configuredWebhook.DeepCopy()
   196  
   197  	if !ac.disableNamespaceOwnership {
   198  		// Set the owner to namespace.
   199  		ns, err := ac.client.CoreV1().Namespaces().Get(ctx, system.Namespace(), metav1.GetOptions{})
   200  		if err != nil {
   201  			return fmt.Errorf("failed to fetch namespace: %w", err)
   202  		}
   203  		nsRef := *metav1.NewControllerRef(ns, corev1.SchemeGroupVersion.WithKind("Namespace"))
   204  		nsRef.Controller = ptr.Bool(false)
   205  		current.OwnerReferences = []metav1.OwnerReference{nsRef}
   206  	}
   207  
   208  	for i, wh := range current.Webhooks {
   209  		if wh.Name != current.Name {
   210  			continue
   211  		}
   212  		cur := &current.Webhooks[i]
   213  		cur.Rules = rules
   214  
   215  		cur.NamespaceSelector = webhook.EnsureLabelSelectorExpressions(
   216  			cur.NamespaceSelector,
   217  			&metav1.LabelSelector{
   218  				MatchExpressions: []metav1.LabelSelectorRequirement{{
   219  					Key:      "webhooks.knative.dev/exclude",
   220  					Operator: metav1.LabelSelectorOpDoesNotExist,
   221  				}},
   222  			})
   223  
   224  		cur.ClientConfig.CABundle = caCert
   225  		if cur.ClientConfig.Service == nil {
   226  			return fmt.Errorf("missing service reference for webhook: %s", wh.Name)
   227  		}
   228  		cur.ClientConfig.Service.Path = ptr.String(ac.Path())
   229  	}
   230  
   231  	if ok, err := kmp.SafeEqual(configuredWebhook, current); err != nil {
   232  		return fmt.Errorf("error diffing webhooks: %w", err)
   233  	} else if !ok {
   234  		logger.Info("Updating webhook")
   235  		vwhclient := ac.client.AdmissionregistrationV1().ValidatingWebhookConfigurations()
   236  		if _, err := vwhclient.Update(ctx, current, metav1.UpdateOptions{}); err != nil {
   237  			return fmt.Errorf("failed to update webhook: %w", err)
   238  		}
   239  	} else {
   240  		logger.Info("Webhook is valid")
   241  	}
   242  	return nil
   243  }