sigs.k8s.io/cluster-api-provider-azure@v1.17.0/api/v1beta1/azuremanagedcontrolplanetemplate_webhook.go (about) 1 /* 2 Copyright 2023 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 v1beta1 18 19 import ( 20 "context" 21 "reflect" 22 23 apierrors "k8s.io/apimachinery/pkg/api/errors" 24 "k8s.io/apimachinery/pkg/runtime" 25 "k8s.io/apimachinery/pkg/util/validation/field" 26 "sigs.k8s.io/cluster-api-provider-azure/feature" 27 "sigs.k8s.io/cluster-api-provider-azure/util/versions" 28 webhookutils "sigs.k8s.io/cluster-api-provider-azure/util/webhook" 29 capifeature "sigs.k8s.io/cluster-api/feature" 30 ctrl "sigs.k8s.io/controller-runtime" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 33 ) 34 35 // SetupAzureManagedControlPlaneTemplateWebhookWithManager will set up the webhook to be managed by the specified manager. 36 func SetupAzureManagedControlPlaneTemplateWebhookWithManager(mgr ctrl.Manager) error { 37 mcpw := &azureManagedControlPlaneTemplateWebhook{Client: mgr.GetClient()} 38 return ctrl.NewWebhookManagedBy(mgr). 39 For(&AzureManagedControlPlaneTemplate{}). 40 WithDefaulter(mcpw). 41 WithValidator(mcpw). 42 Complete() 43 } 44 45 // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-azuremanagedcontrolplanetemplate,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=azuremanagedcontrolplanetemplates,versions=v1beta1,name=validation.azuremanagedcontrolplanetemplates.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 46 // +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-azuremanagedcontrolplanetemplate,mutating=true,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=azuremanagedcontrolplanetemplates,versions=v1beta1,name=default.azuremanagedcontrolplanetemplates.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 47 48 type azureManagedControlPlaneTemplateWebhook struct { 49 Client client.Client 50 } 51 52 // Default implements webhook.Defaulter so a webhook will be registered for the type. 53 func (mcpw *azureManagedControlPlaneTemplateWebhook) Default(ctx context.Context, obj runtime.Object) error { 54 mcp, ok := obj.(*AzureManagedControlPlaneTemplate) 55 if !ok { 56 return apierrors.NewBadRequest("expected an AzureManagedControlPlaneTemplate") 57 } 58 mcp.setDefaults() 59 return nil 60 } 61 62 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type. 63 func (mcpw *azureManagedControlPlaneTemplateWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 64 mcp, ok := obj.(*AzureManagedControlPlaneTemplate) 65 if !ok { 66 return nil, apierrors.NewBadRequest("expected an AzureManagedControlPlaneTemplate") 67 } 68 // NOTE: AzureManagedControlPlaneTemplate relies upon MachinePools, which is behind a feature gate flag. 69 // The webhook must prevent creating new objects in case the feature flag is disabled. 70 if !feature.Gates.Enabled(capifeature.MachinePool) { 71 return nil, field.Forbidden( 72 field.NewPath("spec"), 73 "can be set only if the Cluster API 'MachinePool' feature flag is enabled", 74 ) 75 } 76 77 return nil, mcp.validateManagedControlPlaneTemplate(mcpw.Client) 78 } 79 80 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. 81 func (mcpw *azureManagedControlPlaneTemplateWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { 82 var allErrs field.ErrorList 83 old, ok := oldObj.(*AzureManagedControlPlaneTemplate) 84 if !ok { 85 return nil, apierrors.NewBadRequest("expected an AzureManagedControlPlaneTemplate") 86 } 87 mcp, ok := newObj.(*AzureManagedControlPlaneTemplate) 88 if !ok { 89 return nil, apierrors.NewBadRequest("expected an AzureManagedControlPlaneTemplate") 90 } 91 if err := webhookutils.ValidateImmutable( 92 field.NewPath("spec", "template", "spec", "subscriptionID"), 93 old.Spec.Template.Spec.SubscriptionID, 94 mcp.Spec.Template.Spec.SubscriptionID); err != nil { 95 allErrs = append(allErrs, err) 96 } 97 98 if err := webhookutils.ValidateImmutable( 99 field.NewPath("spec", "template", "spec", "location"), 100 old.Spec.Template.Spec.Location, 101 mcp.Spec.Template.Spec.Location); err != nil { 102 allErrs = append(allErrs, err) 103 } 104 105 if err := webhookutils.ValidateImmutable( 106 field.NewPath("spec", "template", "spec", "dnsServiceIP"), 107 old.Spec.Template.Spec.DNSServiceIP, 108 mcp.Spec.Template.Spec.DNSServiceIP); err != nil { 109 allErrs = append(allErrs, err) 110 } 111 112 if err := webhookutils.ValidateImmutable( 113 field.NewPath("spec", "template", "spec", "networkPlugin"), 114 old.Spec.Template.Spec.NetworkPlugin, 115 mcp.Spec.Template.Spec.NetworkPlugin); err != nil { 116 allErrs = append(allErrs, err) 117 } 118 119 if err := webhookutils.ValidateImmutable( 120 field.NewPath("spec", "template", "spec", "networkPolicy"), 121 old.Spec.Template.Spec.NetworkPolicy, 122 mcp.Spec.Template.Spec.NetworkPolicy); err != nil { 123 allErrs = append(allErrs, err) 124 } 125 126 if err := webhookutils.ValidateImmutable( 127 field.NewPath("spec", "template", "spec", "networkDataplane"), 128 old.Spec.Template.Spec.NetworkDataplane, 129 mcp.Spec.Template.Spec.NetworkDataplane); err != nil { 130 allErrs = append(allErrs, err) 131 } 132 133 if err := webhookutils.ValidateImmutable( 134 field.NewPath("spec", "template", "spec", "loadBalancerSKU"), 135 old.Spec.Template.Spec.LoadBalancerSKU, 136 mcp.Spec.Template.Spec.LoadBalancerSKU); err != nil { 137 allErrs = append(allErrs, err) 138 } 139 140 if old.Spec.Template.Spec.AADProfile != nil { 141 if mcp.Spec.Template.Spec.AADProfile == nil { 142 allErrs = append(allErrs, 143 field.Invalid( 144 field.NewPath("spec", "template", "spec", "aadProfile"), 145 mcp.Spec.Template.Spec.AADProfile, 146 "field cannot be nil, cannot disable AADProfile")) 147 } else { 148 if !mcp.Spec.Template.Spec.AADProfile.Managed && old.Spec.Template.Spec.AADProfile.Managed { 149 allErrs = append(allErrs, 150 field.Invalid( 151 field.NewPath("spec", "template", "spec", "aadProfile", "managed"), 152 mcp.Spec.Template.Spec.AADProfile.Managed, 153 "cannot set AADProfile.Managed to false")) 154 } 155 if len(mcp.Spec.Template.Spec.AADProfile.AdminGroupObjectIDs) == 0 { 156 allErrs = append(allErrs, 157 field.Invalid( 158 field.NewPath("spec", "template", "spec", "aadProfile", "adminGroupObjectIDs"), 159 mcp.Spec.Template.Spec.AADProfile.AdminGroupObjectIDs, 160 "length of AADProfile.AdminGroupObjectIDs cannot be zero")) 161 } 162 } 163 } 164 165 // Consider removing this once moves out of preview 166 // Updating outboundType after cluster creation (PREVIEW) 167 // https://learn.microsoft.com/en-us/azure/aks/egress-outboundtype#updating-outboundtype-after-cluster-creation-preview 168 if err := webhookutils.ValidateImmutable( 169 field.NewPath("spec", "template", "spec", "outboundType"), 170 old.Spec.Template.Spec.OutboundType, 171 mcp.Spec.Template.Spec.OutboundType); err != nil { 172 allErrs = append(allErrs, err) 173 } 174 175 if errs := mcp.validateVirtualNetworkTemplateUpdate(old); len(errs) > 0 { 176 allErrs = append(allErrs, errs...) 177 } 178 179 if errs := mcp.validateAPIServerAccessProfileTemplateUpdate(old); len(errs) > 0 { 180 allErrs = append(allErrs, errs...) 181 } 182 183 if errs := validateAKSExtensionsUpdate(old.Spec.Template.Spec.Extensions, mcp.Spec.Template.Spec.Extensions); len(errs) > 0 { 184 allErrs = append(allErrs, errs...) 185 } 186 if errs := mcp.validateK8sVersionUpdate(old); len(errs) > 0 { 187 allErrs = append(allErrs, errs...) 188 } 189 190 if len(allErrs) == 0 { 191 return nil, mcp.validateManagedControlPlaneTemplate(mcpw.Client) 192 } 193 194 return nil, apierrors.NewInvalid(GroupVersion.WithKind(AzureManagedControlPlaneTemplateKind).GroupKind(), mcp.Name, allErrs) 195 } 196 197 // Validate the Azure Managed Control Plane Template and return an aggregate error. 198 func (mcp *AzureManagedControlPlaneTemplate) validateManagedControlPlaneTemplate(cli client.Client) error { 199 var allErrs field.ErrorList 200 201 allErrs = append(allErrs, validateVersion( 202 mcp.Spec.Template.Spec.Version, 203 field.NewPath("spec").Child("template").Child("spec").Child("version"))...) 204 205 allErrs = append(allErrs, validateLoadBalancerProfile( 206 mcp.Spec.Template.Spec.LoadBalancerProfile, 207 field.NewPath("spec").Child("template").Child("spec").Child("loadBalancerProfile"))...) 208 209 allErrs = append(allErrs, validateManagedClusterNetwork( 210 cli, 211 mcp.Labels, 212 mcp.Namespace, 213 mcp.Spec.Template.Spec.DNSServiceIP, 214 mcp.Spec.Template.Spec.VirtualNetwork.Subnet, 215 field.NewPath("spec").Child("template").Child("spec"))...) 216 217 allErrs = append(allErrs, validateName(mcp.Name, field.NewPath("name"))...) 218 219 allErrs = append(allErrs, validateAutoScalerProfile(mcp.Spec.Template.Spec.AutoScalerProfile, field.NewPath("spec").Child("template").Child("spec").Child("autoScalerProfile"))...) 220 221 allErrs = append(allErrs, validateAKSExtensions(mcp.Spec.Template.Spec.Extensions, field.NewPath("spec").Child("extensions"))...) 222 223 allErrs = append(allErrs, mcp.Spec.Template.Spec.AzureManagedControlPlaneClassSpec.validateSecurityProfile()...) 224 225 allErrs = append(allErrs, validateNetworkPolicy(mcp.Spec.Template.Spec.NetworkPolicy, mcp.Spec.Template.Spec.NetworkDataplane, field.NewPath("spec").Child("template").Child("spec").Child("networkPolicy"))...) 226 227 allErrs = append(allErrs, validateNetworkDataplane(mcp.Spec.Template.Spec.NetworkDataplane, mcp.Spec.Template.Spec.NetworkPolicy, mcp.Spec.Template.Spec.NetworkPluginMode, field.NewPath("spec").Child("template").Child("spec").Child("networkDataplane"))...) 228 229 allErrs = append(allErrs, validateAPIServerAccessProfile(mcp.Spec.Template.Spec.APIServerAccessProfile, field.NewPath("spec").Child("template").Child("spec").Child("apiServerAccessProfile"))...) 230 231 allErrs = append(allErrs, validateAMCPVirtualNetwork(mcp.Spec.Template.Spec.VirtualNetwork, field.NewPath("spec").Child("template").Child("spec").Child("virtualNetwork"))...) 232 233 return allErrs.ToAggregate() 234 } 235 236 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type. 237 func (mcpw *azureManagedControlPlaneTemplateWebhook) ValidateDelete(ctx context.Context, _ runtime.Object) (admission.Warnings, error) { 238 return nil, nil 239 } 240 241 // validateK8sVersionUpdate validates K8s version. 242 func (mcp *AzureManagedControlPlaneTemplate) validateK8sVersionUpdate(old *AzureManagedControlPlaneTemplate) field.ErrorList { 243 var allErrs field.ErrorList 244 if hv := versions.GetHigherK8sVersion(mcp.Spec.Template.Spec.Version, old.Spec.Template.Spec.Version); hv != mcp.Spec.Template.Spec.Version { 245 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "template", "spec", "version"), 246 mcp.Spec.Template.Spec.Version, "field version cannot be downgraded"), 247 ) 248 } 249 return allErrs 250 } 251 252 // validateVirtualNetworkTemplateUpdate validates update to VirtualNetworkTemplate. 253 func (mcp *AzureManagedControlPlaneTemplate) validateVirtualNetworkTemplateUpdate(old *AzureManagedControlPlaneTemplate) field.ErrorList { 254 var allErrs field.ErrorList 255 256 if old.Spec.Template.Spec.VirtualNetwork.CIDRBlock != mcp.Spec.Template.Spec.VirtualNetwork.CIDRBlock { 257 allErrs = append(allErrs, 258 field.Invalid( 259 field.NewPath("spec", "template", "spec", "virtualNetwork", "cidrBlock"), 260 mcp.Spec.Template.Spec.VirtualNetwork.CIDRBlock, 261 "Virtual Network CIDRBlock is immutable")) 262 } 263 264 if old.Spec.Template.Spec.VirtualNetwork.Subnet.Name != mcp.Spec.Template.Spec.VirtualNetwork.Subnet.Name { 265 allErrs = append(allErrs, 266 field.Invalid( 267 field.NewPath("spec", "template", "spec", "virtualNetwork", "subnet", "name"), 268 mcp.Spec.Template.Spec.VirtualNetwork.Subnet.Name, 269 "Subnet Name is immutable")) 270 } 271 272 // NOTE: This only works because we force the user to set the CIDRBlock for both the 273 // managed and unmanaged Vnets. If we ever update the subnet cidr based on what's 274 // actually set in the subnet, and it is different from what's in the Spec, for 275 // unmanaged Vnets like we do with the AzureCluster this logic will break. 276 if old.Spec.Template.Spec.VirtualNetwork.Subnet.CIDRBlock != mcp.Spec.Template.Spec.VirtualNetwork.Subnet.CIDRBlock { 277 allErrs = append(allErrs, 278 field.Invalid( 279 field.NewPath("spec", "template", "spec", "virtualNetwork", "subnet", "cidrBlock"), 280 mcp.Spec.Template.Spec.VirtualNetwork.Subnet.CIDRBlock, 281 "Subnet CIDRBlock is immutable")) 282 } 283 284 if errs := mcp.Spec.Template.Spec.AzureManagedControlPlaneClassSpec.validateSecurityProfileUpdate(&old.Spec.Template.Spec.AzureManagedControlPlaneClassSpec); len(errs) > 0 { 285 allErrs = append(allErrs, errs...) 286 } 287 288 return allErrs 289 } 290 291 // validateAPIServerAccessProfileTemplateUpdate validates update to APIServerAccessProfileTemplate. 292 func (mcp *AzureManagedControlPlaneTemplate) validateAPIServerAccessProfileTemplateUpdate(old *AzureManagedControlPlaneTemplate) field.ErrorList { 293 var allErrs field.ErrorList 294 295 newAPIServerAccessProfileNormalized := &APIServerAccessProfile{} 296 oldAPIServerAccessProfileNormalized := &APIServerAccessProfile{} 297 if mcp.Spec.Template.Spec.APIServerAccessProfile != nil { 298 newAPIServerAccessProfileNormalized = &APIServerAccessProfile{ 299 APIServerAccessProfileClassSpec: APIServerAccessProfileClassSpec{ 300 EnablePrivateCluster: mcp.Spec.Template.Spec.APIServerAccessProfile.EnablePrivateCluster, 301 PrivateDNSZone: mcp.Spec.Template.Spec.APIServerAccessProfile.PrivateDNSZone, 302 EnablePrivateClusterPublicFQDN: mcp.Spec.Template.Spec.APIServerAccessProfile.EnablePrivateClusterPublicFQDN, 303 }, 304 } 305 } 306 if old.Spec.Template.Spec.APIServerAccessProfile != nil { 307 oldAPIServerAccessProfileNormalized = &APIServerAccessProfile{ 308 APIServerAccessProfileClassSpec: APIServerAccessProfileClassSpec{ 309 EnablePrivateCluster: old.Spec.Template.Spec.APIServerAccessProfile.EnablePrivateCluster, 310 PrivateDNSZone: old.Spec.Template.Spec.APIServerAccessProfile.PrivateDNSZone, 311 EnablePrivateClusterPublicFQDN: old.Spec.Template.Spec.APIServerAccessProfile.EnablePrivateClusterPublicFQDN, 312 }, 313 } 314 } 315 316 if !reflect.DeepEqual(newAPIServerAccessProfileNormalized, oldAPIServerAccessProfileNormalized) { 317 allErrs = append(allErrs, 318 field.Invalid(field.NewPath("spec", "template", "spec", "apiServerAccessProfile"), 319 mcp.Spec.Template.Spec.APIServerAccessProfile, "fields are immutable"), 320 ) 321 } 322 323 return allErrs 324 }