sigs.k8s.io/cluster-api-provider-azure@v1.14.3/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 if old.Spec.Template.Spec.VirtualNetwork.Name != mcp.Spec.Template.Spec.VirtualNetwork.Name { 256 allErrs = append(allErrs, 257 field.Invalid( 258 field.NewPath("Spec", "Template", "Spec", "VirtualNetwork.Name"), 259 mcp.Spec.Template.Spec.VirtualNetwork.Name, 260 "Virtual Network Name is immutable")) 261 } 262 263 if old.Spec.Template.Spec.VirtualNetwork.CIDRBlock != mcp.Spec.Template.Spec.VirtualNetwork.CIDRBlock { 264 allErrs = append(allErrs, 265 field.Invalid( 266 field.NewPath("Spec", "Template", "Spec", "VirtualNetwork.CIDRBlock"), 267 mcp.Spec.Template.Spec.VirtualNetwork.CIDRBlock, 268 "Virtual Network CIDRBlock is immutable")) 269 } 270 271 if old.Spec.Template.Spec.VirtualNetwork.Subnet.Name != mcp.Spec.Template.Spec.VirtualNetwork.Subnet.Name { 272 allErrs = append(allErrs, 273 field.Invalid( 274 field.NewPath("Spec", "Template", "Spec", "VirtualNetwork.Subnet.Name"), 275 mcp.Spec.Template.Spec.VirtualNetwork.Subnet.Name, 276 "Subnet Name is immutable")) 277 } 278 279 // NOTE: This only works because we force the user to set the CIDRBlock for both the 280 // managed and unmanaged Vnets. If we ever update the subnet cidr based on what's 281 // actually set in the subnet, and it is different from what's in the Spec, for 282 // unmanaged Vnets like we do with the AzureCluster this logic will break. 283 if old.Spec.Template.Spec.VirtualNetwork.Subnet.CIDRBlock != mcp.Spec.Template.Spec.VirtualNetwork.Subnet.CIDRBlock { 284 allErrs = append(allErrs, 285 field.Invalid( 286 field.NewPath("Spec", "Template", "Spec", "VirtualNetwork.Subnet.CIDRBlock"), 287 mcp.Spec.Template.Spec.VirtualNetwork.Subnet.CIDRBlock, 288 "Subnet CIDRBlock is immutable")) 289 } 290 291 if errs := mcp.Spec.Template.Spec.AzureManagedControlPlaneClassSpec.validateSecurityProfileUpdate(&old.Spec.Template.Spec.AzureManagedControlPlaneClassSpec); len(errs) > 0 { 292 allErrs = append(allErrs, errs...) 293 } 294 295 return allErrs 296 } 297 298 // validateAPIServerAccessProfileTemplateUpdate validates update to APIServerAccessProfileTemplate. 299 func (mcp *AzureManagedControlPlaneTemplate) validateAPIServerAccessProfileTemplateUpdate(old *AzureManagedControlPlaneTemplate) field.ErrorList { 300 var allErrs field.ErrorList 301 302 newAPIServerAccessProfileNormalized := &APIServerAccessProfile{} 303 oldAPIServerAccessProfileNormalized := &APIServerAccessProfile{} 304 if mcp.Spec.Template.Spec.APIServerAccessProfile != nil { 305 newAPIServerAccessProfileNormalized = &APIServerAccessProfile{ 306 APIServerAccessProfileClassSpec: APIServerAccessProfileClassSpec{ 307 EnablePrivateCluster: mcp.Spec.Template.Spec.APIServerAccessProfile.EnablePrivateCluster, 308 PrivateDNSZone: mcp.Spec.Template.Spec.APIServerAccessProfile.PrivateDNSZone, 309 EnablePrivateClusterPublicFQDN: mcp.Spec.Template.Spec.APIServerAccessProfile.EnablePrivateClusterPublicFQDN, 310 }, 311 } 312 } 313 if old.Spec.Template.Spec.APIServerAccessProfile != nil { 314 oldAPIServerAccessProfileNormalized = &APIServerAccessProfile{ 315 APIServerAccessProfileClassSpec: APIServerAccessProfileClassSpec{ 316 EnablePrivateCluster: old.Spec.Template.Spec.APIServerAccessProfile.EnablePrivateCluster, 317 PrivateDNSZone: old.Spec.Template.Spec.APIServerAccessProfile.PrivateDNSZone, 318 EnablePrivateClusterPublicFQDN: old.Spec.Template.Spec.APIServerAccessProfile.EnablePrivateClusterPublicFQDN, 319 }, 320 } 321 } 322 323 if !reflect.DeepEqual(newAPIServerAccessProfileNormalized, oldAPIServerAccessProfileNormalized) { 324 allErrs = append(allErrs, 325 field.Invalid(field.NewPath("Spec", "Template", "Spec", "APIServerAccessProfile"), 326 mcp.Spec.Template.Spec.APIServerAccessProfile, "fields are immutable"), 327 ) 328 } 329 330 return allErrs 331 }