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  }