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  }