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 := ¤t.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 }