sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/runtime/extensionconfig_webhook.go (about) 1 /* 2 Copyright 2022 The Kubernetes 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 runtime 18 19 import ( 20 "context" 21 "fmt" 22 "net/url" 23 "strings" 24 25 apierrors "k8s.io/apimachinery/pkg/api/errors" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/util/validation" 29 "k8s.io/apimachinery/pkg/util/validation/field" 30 "k8s.io/utils/ptr" 31 ctrl "sigs.k8s.io/controller-runtime" 32 "sigs.k8s.io/controller-runtime/pkg/webhook" 33 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 34 35 runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1" 36 "sigs.k8s.io/cluster-api/feature" 37 ) 38 39 // ExtensionConfig is the webhook for runtimev1.ExtensionConfig. 40 type ExtensionConfig struct{} 41 42 func (webhook *ExtensionConfig) SetupWebhookWithManager(mgr ctrl.Manager) error { 43 return ctrl.NewWebhookManagedBy(mgr). 44 For(&runtimev1.ExtensionConfig{}). 45 WithDefaulter(webhook). 46 WithValidator(webhook). 47 Complete() 48 } 49 50 // +kubebuilder:webhook:verbs=create;update,path=/validate-runtime-cluster-x-k8s-io-v1alpha1-extensionconfig,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=runtime.cluster.x-k8s.io,resources=extensionconfigs,versions=v1alpha1,name=validation.extensionconfig.runtime.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 51 // +kubebuilder:webhook:verbs=create;update,path=/mutate-runtime-cluster-x-k8s-io-v1alpha1-extensionconfig,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=runtime.cluster.x-k8s.io,resources=extensionconfigs,versions=v1alpha1,name=default.extensionconfig.runtime.addons.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 52 53 var _ webhook.CustomValidator = &ExtensionConfig{} 54 var _ webhook.CustomDefaulter = &ExtensionConfig{} 55 56 // Default implements webhook.Defaulter so a webhook will be registered for the type. 57 func (webhook *ExtensionConfig) Default(_ context.Context, obj runtime.Object) error { 58 extensionConfig, ok := obj.(*runtimev1.ExtensionConfig) 59 if !ok { 60 return apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", obj)) 61 } 62 // Default NamespaceSelector to an empty LabelSelector, which matches everything, if not set. 63 if extensionConfig.Spec.NamespaceSelector == nil { 64 extensionConfig.Spec.NamespaceSelector = &metav1.LabelSelector{} 65 } 66 if extensionConfig.Spec.ClientConfig.Service != nil { 67 if extensionConfig.Spec.ClientConfig.Service.Port == nil { 68 extensionConfig.Spec.ClientConfig.Service.Port = ptr.To[int32](443) 69 } 70 } 71 return nil 72 } 73 74 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type. 75 func (webhook *ExtensionConfig) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 76 extensionConfig, ok := obj.(*runtimev1.ExtensionConfig) 77 if !ok { 78 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", obj)) 79 } 80 return webhook.validate(ctx, nil, extensionConfig) 81 } 82 83 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. 84 func (webhook *ExtensionConfig) ValidateUpdate(ctx context.Context, old, updated runtime.Object) (admission.Warnings, error) { 85 oldExtensionConfig, ok := old.(*runtimev1.ExtensionConfig) 86 if !ok { 87 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", old)) 88 } 89 newExtensionConfig, ok := updated.(*runtimev1.ExtensionConfig) 90 if !ok { 91 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", updated)) 92 } 93 return webhook.validate(ctx, oldExtensionConfig, newExtensionConfig) 94 } 95 96 // validate validates an ExtensionConfig create or update. 97 func (webhook *ExtensionConfig) validate(_ context.Context, _, newExtensionConfig *runtimev1.ExtensionConfig) (admission.Warnings, error) { 98 // NOTE: ExtensionConfig is behind the RuntimeSDK feature gate flag; the web hook 99 // must prevent creating and updating objects in case the feature flag is disabled. 100 if !feature.Gates.Enabled(feature.RuntimeSDK) { 101 return nil, field.Forbidden( 102 field.NewPath("spec"), 103 "can be set only if the RuntimeSDK feature flag is enabled", 104 ) 105 } 106 107 var allErrs field.ErrorList 108 109 // Name should match Kubernetes naming conventions - validated based on DNS1123 label rules. 110 if errStrings := validation.IsDNS1123Label(newExtensionConfig.Name); len(errStrings) > 0 { 111 allErrs = append(allErrs, field.Invalid( 112 field.NewPath("metadata", "name"), 113 newExtensionConfig.Name, 114 fmt.Sprintf("ExtensionConfig name should be a valid DNS1123 label name: %s", errStrings))) 115 } 116 allErrs = append(allErrs, validateExtensionConfigSpec(newExtensionConfig)...) 117 118 if len(allErrs) > 0 { 119 return nil, apierrors.NewInvalid(runtimev1.GroupVersion.WithKind("ExtensionConfig").GroupKind(), newExtensionConfig.Name, allErrs) 120 } 121 return nil, nil 122 } 123 124 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type. 125 func (webhook *ExtensionConfig) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { 126 return nil, nil 127 } 128 129 func validateExtensionConfigSpec(e *runtimev1.ExtensionConfig) field.ErrorList { 130 var allErrs field.ErrorList 131 132 specPath := field.NewPath("spec") 133 134 if e.Spec.ClientConfig.URL == nil && e.Spec.ClientConfig.Service == nil { 135 allErrs = append(allErrs, field.Required( 136 specPath.Child("clientConfig"), 137 "either url or service must be defined", 138 )) 139 } 140 if e.Spec.ClientConfig.URL != nil && e.Spec.ClientConfig.Service != nil { 141 allErrs = append(allErrs, field.Forbidden( 142 specPath.Child("clientConfig"), 143 "only one of url or service can be defined", 144 )) 145 } 146 147 // Validate URL 148 if e.Spec.ClientConfig.URL != nil { 149 if uri, err := url.ParseRequestURI(*e.Spec.ClientConfig.URL); err != nil { 150 allErrs = append(allErrs, field.Invalid( 151 specPath.Child("clientConfig", "url"), 152 *e.Spec.ClientConfig.URL, 153 fmt.Sprintf("must be a valid URL, e.g. https://example.com: %v", err), 154 )) 155 } else if uri.Scheme != "https" { 156 allErrs = append(allErrs, field.Invalid( 157 specPath.Child("clientConfig", "url"), 158 *e.Spec.ClientConfig.URL, 159 "'https' is the only allowed URL scheme, e.g. https://example.com", 160 )) 161 } 162 } 163 164 // Validate Service if defined 165 if e.Spec.ClientConfig.Service != nil { 166 // Validate that the name is not empty and is a Valid RFC1123 name. 167 if e.Spec.ClientConfig.Service.Name == "" { 168 allErrs = append(allErrs, field.Required( 169 specPath.Child("clientConfig", "service", "name"), 170 "must not be empty", 171 )) 172 } 173 174 for _, msg := range validation.IsDNS1035Label(e.Spec.ClientConfig.Service.Name) { 175 allErrs = append(allErrs, field.Invalid( 176 specPath.Child("clientConfig", "service", "name"), 177 e.Spec.ClientConfig.Service.Name, 178 msg, 179 )) 180 } 181 182 if e.Spec.ClientConfig.Service.Namespace == "" { 183 allErrs = append(allErrs, field.Required( 184 specPath.Child("clientConfig", "service", "namespace"), 185 "must not be empty", 186 )) 187 } 188 189 for _, msg := range validation.IsDNS1123Label(e.Spec.ClientConfig.Service.Namespace) { 190 allErrs = append(allErrs, field.Invalid( 191 specPath.Child("clientConfig", "service", "namespace"), 192 e.Spec.ClientConfig.Service.Namespace, 193 msg, 194 )) 195 } 196 197 if e.Spec.ClientConfig.Service.Path != nil { 198 path := *e.Spec.ClientConfig.Service.Path 199 if _, err := url.ParseRequestURI(path); err != nil { 200 allErrs = append(allErrs, field.Invalid( 201 specPath.Child("clientConfig", "service", "path"), 202 path, 203 fmt.Sprintf("must be a valid URL path e.g. /path/to/hook: %v", err), 204 )) 205 } 206 if !strings.HasPrefix(path, "/") { 207 allErrs = append(allErrs, field.Invalid( 208 specPath.Child("clientConfig", "service", "path"), 209 path, 210 "must start with \"/\" to be a valid URL path", 211 )) 212 } 213 } 214 if e.Spec.ClientConfig.Service.Port != nil { 215 for _, msg := range validation.IsValidPortNum(int(*e.Spec.ClientConfig.Service.Port)) { 216 allErrs = append(allErrs, field.Invalid( 217 specPath.Child("clientConfig", "service", "port"), 218 *e.Spec.ClientConfig.Service.Port, 219 msg, 220 )) 221 } 222 } 223 } 224 if e.Spec.NamespaceSelector == nil { 225 allErrs = append(allErrs, field.Required( 226 specPath.Child("namespaceSelector"), 227 "must be defined", 228 )) 229 } 230 231 if _, err := metav1.LabelSelectorAsSelector(e.Spec.NamespaceSelector); err != nil { 232 allErrs = append(allErrs, field.Invalid( 233 specPath.Child("namespaceSelector"), 234 e.Spec.NamespaceSelector, 235 err.Error(), 236 )) 237 } 238 return allErrs 239 }