knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/configmaps/configmaps.go (about) 1 /* 2 Copyright 2019 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 configmaps 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "reflect" 25 26 "go.uber.org/zap" 27 admissionv1 "k8s.io/api/admission/v1" 28 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 29 corev1 "k8s.io/api/core/v1" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 "k8s.io/apimachinery/pkg/types" 33 "k8s.io/client-go/kubernetes" 34 admissionlisters "k8s.io/client-go/listers/admissionregistration/v1" 35 corelisters "k8s.io/client-go/listers/core/v1" 36 37 "knative.dev/pkg/configmap" 38 "knative.dev/pkg/controller" 39 "knative.dev/pkg/kmp" 40 "knative.dev/pkg/logging" 41 "knative.dev/pkg/ptr" 42 pkgreconciler "knative.dev/pkg/reconciler" 43 "knative.dev/pkg/system" 44 "knative.dev/pkg/webhook" 45 certresources "knative.dev/pkg/webhook/certificates/resources" 46 ) 47 48 // reconciler implements the AdmissionController for ConfigMaps 49 type reconciler struct { 50 webhook.StatelessAdmissionImpl 51 pkgreconciler.LeaderAwareFuncs 52 53 key types.NamespacedName 54 path string 55 constructors map[string]reflect.Value 56 57 client kubernetes.Interface 58 vwhlister admissionlisters.ValidatingWebhookConfigurationLister 59 secretlister corelisters.SecretLister 60 61 secretName string 62 disableNamespaceOwnership bool 63 } 64 65 var ( 66 _ controller.Reconciler = (*reconciler)(nil) 67 _ pkgreconciler.LeaderAware = (*reconciler)(nil) 68 _ webhook.AdmissionController = (*reconciler)(nil) 69 _ webhook.StatelessAdmissionController = (*reconciler)(nil) 70 ) 71 72 // Reconcile implements controller.Reconciler 73 func (ac *reconciler) Reconcile(ctx context.Context, key string) error { 74 logger := logging.FromContext(ctx) 75 76 if !ac.IsLeaderFor(ac.key) { 77 return controller.NewSkipKey(key) 78 } 79 80 secret, err := ac.secretlister.Secrets(system.Namespace()).Get(ac.secretName) 81 if err != nil { 82 logger.Errorw("Error fetching secret ", zap.Error(err)) 83 return err 84 } 85 86 caCert, ok := secret.Data[certresources.CACert] 87 if !ok { 88 return fmt.Errorf("secret %q is missing %q key", ac.secretName, certresources.CACert) 89 } 90 91 return ac.reconcileValidatingWebhook(ctx, caCert) 92 } 93 94 // Path implements AdmissionController 95 func (ac *reconciler) Path() string { 96 return ac.path 97 } 98 99 // Admit implements AdmissionController 100 func (ac *reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { 101 logger := logging.FromContext(ctx) 102 switch request.Operation { 103 case admissionv1.Create, admissionv1.Update: 104 default: 105 logger.Info("Unhandled webhook operation, letting it through ", request.Operation) 106 return &admissionv1.AdmissionResponse{Allowed: true} 107 } 108 109 if err := ac.validate(ctx, request); err != nil { 110 return webhook.MakeErrorStatus("validation failed: %v", err) 111 } 112 113 return &admissionv1.AdmissionResponse{ 114 Allowed: true, 115 } 116 } 117 118 func (ac *reconciler) reconcileValidatingWebhook(ctx context.Context, caCert []byte) error { 119 logger := logging.FromContext(ctx) 120 121 ruleScope := admissionregistrationv1.NamespacedScope 122 rules := []admissionregistrationv1.RuleWithOperations{{ 123 Operations: []admissionregistrationv1.OperationType{ 124 admissionregistrationv1.Create, 125 admissionregistrationv1.Update, 126 }, 127 Rule: admissionregistrationv1.Rule{ 128 APIGroups: []string{""}, 129 APIVersions: []string{"v1"}, 130 Resources: []string{"configmaps/*"}, 131 Scope: &ruleScope, 132 }, 133 }} 134 135 configuredWebhook, err := ac.vwhlister.Get(ac.key.Name) 136 if err != nil { 137 return fmt.Errorf("error retrieving webhook: %w", err) 138 } 139 140 webhook := configuredWebhook.DeepCopy() 141 142 if !ac.disableNamespaceOwnership { 143 // Set the owner to namespace. 144 ns, err := ac.client.CoreV1().Namespaces().Get(ctx, system.Namespace(), metav1.GetOptions{}) 145 if err != nil { 146 return fmt.Errorf("failed to fetch namespace: %w", err) 147 } 148 nsRef := *metav1.NewControllerRef(ns, corev1.SchemeGroupVersion.WithKind("Namespace")) 149 nsRef.Controller = ptr.Bool(false) 150 webhook.OwnerReferences = []metav1.OwnerReference{nsRef} 151 } 152 153 for i, wh := range webhook.Webhooks { 154 if wh.Name != webhook.Name { 155 continue 156 } 157 webhook.Webhooks[i].Rules = rules 158 webhook.Webhooks[i].ClientConfig.CABundle = caCert 159 if webhook.Webhooks[i].ClientConfig.Service == nil { 160 return errors.New("missing service reference for webhook: " + wh.Name) 161 } 162 webhook.Webhooks[i].ClientConfig.Service.Path = ptr.String(ac.Path()) 163 } 164 165 if ok, err := kmp.SafeEqual(configuredWebhook, webhook); err != nil { 166 return fmt.Errorf("error diffing webhooks: %w", err) 167 } else if !ok { 168 logger.Info("Updating webhook") 169 vwhclient := ac.client.AdmissionregistrationV1().ValidatingWebhookConfigurations() 170 if _, err := vwhclient.Update(ctx, webhook, metav1.UpdateOptions{}); err != nil { 171 return fmt.Errorf("failed to update webhook: %w", err) 172 } 173 } else { 174 logger.Info("Webhook is valid") 175 } 176 177 return nil 178 } 179 180 func (ac *reconciler) validate(ctx context.Context, req *admissionv1.AdmissionRequest) error { 181 logger := logging.FromContext(ctx) 182 kind := req.Kind 183 newBytes := req.Object.Raw 184 185 // Why, oh why are these different types... 186 gvk := schema.GroupVersionKind{ 187 Group: kind.Group, 188 Version: kind.Version, 189 Kind: kind.Kind, 190 } 191 192 resourceGVK := corev1.SchemeGroupVersion.WithKind("ConfigMap") 193 if gvk != resourceGVK { 194 logger.Error("Unhandled kind: ", gvk) 195 return fmt.Errorf("unhandled kind: %v", gvk) 196 } 197 198 var newObj corev1.ConfigMap 199 if len(newBytes) != 0 { 200 if err := json.Unmarshal(newBytes, &newObj); err != nil { 201 return fmt.Errorf("cannot decode incoming new object: %w", err) 202 } 203 } 204 205 if constructor, ok := ac.constructors[newObj.Name]; ok { 206 // Only validate example data if this is a configMap we know about. 207 exampleData, hasExampleData := newObj.Data[configmap.ExampleKey] 208 exampleChecksum, hasExampleChecksumAnnotation := newObj.Annotations[configmap.ExampleChecksumAnnotation] 209 if hasExampleData && hasExampleChecksumAnnotation && 210 exampleChecksum != configmap.Checksum(exampleData) { 211 return fmt.Errorf( 212 "the update modifies a key in %q which is probably not what you want. Instead, copy the respective setting to the top-level of the ConfigMap, directly below %q", 213 configmap.ExampleKey, "data") 214 } 215 216 inputs := []reflect.Value{ 217 reflect.ValueOf(&newObj), 218 } 219 220 outputs := constructor.Call(inputs) 221 errVal := outputs[1] 222 223 if !errVal.IsNil() { 224 return errVal.Interface().(error) 225 } 226 } 227 228 return nil 229 } 230 231 func (ac *reconciler) registerConfig(name string, constructor interface{}) { 232 if err := configmap.ValidateConstructor(constructor); err != nil { 233 panic(err) 234 } 235 236 ac.constructors[name] = reflect.ValueOf(constructor) 237 }