sigs.k8s.io/cluster-api-provider-azure@v1.14.3/api/v1beta1/azurecluster_webhook.go (about)

     1  /*
     2  Copyright 2021 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  	"reflect"
    21  
    22  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    23  	"k8s.io/apimachinery/pkg/runtime"
    24  	"k8s.io/apimachinery/pkg/util/validation/field"
    25  	webhookutils "sigs.k8s.io/cluster-api-provider-azure/util/webhook"
    26  	ctrl "sigs.k8s.io/controller-runtime"
    27  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    28  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    29  )
    30  
    31  // SetupWebhookWithManager sets up and registers the webhook with the manager.
    32  func (c *AzureCluster) SetupWebhookWithManager(mgr ctrl.Manager) error {
    33  	return ctrl.NewWebhookManagedBy(mgr).
    34  		For(c).
    35  		Complete()
    36  }
    37  
    38  // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-azurecluster,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=azureclusters,versions=v1beta1,name=validation.azurecluster.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    39  // +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-azurecluster,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=azureclusters,versions=v1beta1,name=default.azurecluster.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    40  
    41  var _ webhook.Validator = &AzureCluster{}
    42  var _ webhook.Defaulter = &AzureCluster{}
    43  
    44  // Default implements webhook.Defaulter so a webhook will be registered for the type.
    45  func (c *AzureCluster) Default() {
    46  	c.setDefaults()
    47  }
    48  
    49  // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
    50  func (c *AzureCluster) ValidateCreate() (admission.Warnings, error) {
    51  	return c.validateCluster(nil)
    52  }
    53  
    54  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
    55  func (c *AzureCluster) ValidateUpdate(oldRaw runtime.Object) (admission.Warnings, error) {
    56  	var allErrs field.ErrorList
    57  	old := oldRaw.(*AzureCluster)
    58  
    59  	if err := webhookutils.ValidateImmutable(
    60  		field.NewPath("Spec", "ResourceGroup"),
    61  		old.Spec.ResourceGroup,
    62  		c.Spec.ResourceGroup); err != nil {
    63  		allErrs = append(allErrs, err)
    64  	}
    65  
    66  	if err := webhookutils.ValidateImmutable(
    67  		field.NewPath("Spec", "SubscriptionID"),
    68  		old.Spec.SubscriptionID,
    69  		c.Spec.SubscriptionID); err != nil {
    70  		allErrs = append(allErrs, err)
    71  	}
    72  
    73  	if err := webhookutils.ValidateImmutable(
    74  		field.NewPath("Spec", "Location"),
    75  		old.Spec.Location,
    76  		c.Spec.Location); err != nil {
    77  		allErrs = append(allErrs, err)
    78  	}
    79  
    80  	if old.Spec.ControlPlaneEndpoint.Host != "" && c.Spec.ControlPlaneEndpoint.Host != old.Spec.ControlPlaneEndpoint.Host {
    81  		allErrs = append(allErrs,
    82  			field.Invalid(field.NewPath("spec", "ControlPlaneEndpoint", "Host"),
    83  				c.Spec.ControlPlaneEndpoint.Host, "field is immutable"),
    84  		)
    85  	}
    86  
    87  	if old.Spec.ControlPlaneEndpoint.Port != 0 && c.Spec.ControlPlaneEndpoint.Port != old.Spec.ControlPlaneEndpoint.Port {
    88  		allErrs = append(allErrs,
    89  			field.Invalid(field.NewPath("spec", "ControlPlaneEndpoint", "Port"),
    90  				c.Spec.ControlPlaneEndpoint.Port, "field is immutable"),
    91  		)
    92  	}
    93  
    94  	if !reflect.DeepEqual(c.Spec.AzureEnvironment, old.Spec.AzureEnvironment) {
    95  		// The equality failure could be because of default mismatch between v1alpha3 and v1beta1. This happens because
    96  		// the new object `r` will have run through the default webhooks but the old object `old` would not have so.
    97  		// This means if the old object was in v1alpha3, it would not get the new defaults set in v1beta1 resulting
    98  		// in object inequality. To workaround this, we set the v1beta1 defaults here so that the old object also gets
    99  		// the new defaults.
   100  		old.setAzureEnvironmentDefault()
   101  
   102  		// if it's still not equal, return error.
   103  		if !reflect.DeepEqual(c.Spec.AzureEnvironment, old.Spec.AzureEnvironment) {
   104  			allErrs = append(allErrs,
   105  				field.Invalid(field.NewPath("spec", "AzureEnvironment"),
   106  					c.Spec.AzureEnvironment, "field is immutable"),
   107  			)
   108  		}
   109  	}
   110  
   111  	if err := webhookutils.ValidateImmutable(
   112  		field.NewPath("Spec", "NetworkSpec", "PrivateDNSZoneName"),
   113  		old.Spec.NetworkSpec.PrivateDNSZoneName,
   114  		c.Spec.NetworkSpec.PrivateDNSZoneName); err != nil {
   115  		allErrs = append(allErrs, err)
   116  	}
   117  
   118  	// Allow enabling azure bastion but avoid disabling it.
   119  	if old.Spec.BastionSpec.AzureBastion != nil && !reflect.DeepEqual(old.Spec.BastionSpec.AzureBastion, c.Spec.BastionSpec.AzureBastion) {
   120  		allErrs = append(allErrs,
   121  			field.Invalid(field.NewPath("spec", "BastionSpec", "AzureBastion"),
   122  				c.Spec.BastionSpec.AzureBastion, "azure bastion cannot be removed from a cluster"),
   123  		)
   124  	}
   125  
   126  	if err := webhookutils.ValidateImmutable(
   127  		field.NewPath("Spec", "NetworkSpec", "ControlPlaneOutboundLB"),
   128  		old.Spec.NetworkSpec.ControlPlaneOutboundLB,
   129  		c.Spec.NetworkSpec.ControlPlaneOutboundLB); err != nil {
   130  		allErrs = append(allErrs, err)
   131  	}
   132  
   133  	allErrs = append(allErrs, c.validateSubnetUpdate(old)...)
   134  
   135  	if len(allErrs) == 0 {
   136  		return c.validateCluster(old)
   137  	}
   138  
   139  	return nil, apierrors.NewInvalid(GroupVersion.WithKind(AzureClusterKind).GroupKind(), c.Name, allErrs)
   140  }
   141  
   142  // validateSubnetUpdate validates a ClusterSpec.NetworkSpec.Subnets for immutability.
   143  func (c *AzureCluster) validateSubnetUpdate(old *AzureCluster) field.ErrorList {
   144  	var allErrs field.ErrorList
   145  
   146  	oldSubnetMap := make(map[string]SubnetSpec, len(old.Spec.NetworkSpec.Subnets))
   147  	oldSubnetIndex := make(map[string]int, len(old.Spec.NetworkSpec.Subnets))
   148  	for i, subnet := range old.Spec.NetworkSpec.Subnets {
   149  		oldSubnetMap[subnet.Name] = subnet
   150  		oldSubnetIndex[subnet.Name] = i
   151  	}
   152  	for i, subnet := range c.Spec.NetworkSpec.Subnets {
   153  		if oldSubnet, ok := oldSubnetMap[subnet.Name]; ok {
   154  			// Verify the CIDR blocks haven't changed for an owned Vnet.
   155  			// A non-owned Vnet's CIDR block can change based on what's
   156  			// defined in the spec vs what's been loaded from Azure directly.
   157  			// This technically allows the cidr block to be modified in the brief
   158  			// moments before the Vnet is created (because the tags haven't been
   159  			// set yet) but once the Vnet has been created it becomes immutable.
   160  			if old.Spec.NetworkSpec.Vnet.Tags.HasOwned(old.Name) && !reflect.DeepEqual(subnet.CIDRBlocks, oldSubnet.CIDRBlocks) {
   161  				allErrs = append(allErrs,
   162  					field.Invalid(field.NewPath("spec", "networkSpec", "subnets").Index(oldSubnetIndex[subnet.Name]).Child("CIDRBlocks"),
   163  						c.Spec.NetworkSpec.Subnets[i].CIDRBlocks, "field is immutable"),
   164  				)
   165  			}
   166  			if subnet.RouteTable.Name != oldSubnet.RouteTable.Name {
   167  				allErrs = append(allErrs,
   168  					field.Invalid(field.NewPath("spec", "networkSpec", "subnets").Index(oldSubnetIndex[subnet.Name]).Child("RouteTable").Child("Name"),
   169  						c.Spec.NetworkSpec.Subnets[i].RouteTable.Name, "field is immutable"),
   170  				)
   171  			}
   172  			if (subnet.NatGateway.Name != oldSubnet.NatGateway.Name) && (oldSubnet.NatGateway.Name != "") {
   173  				allErrs = append(allErrs,
   174  					field.Invalid(field.NewPath("spec", "networkSpec", "subnets").Index(oldSubnetIndex[subnet.Name]).Child("NatGateway").Child("Name"),
   175  						c.Spec.NetworkSpec.Subnets[i].NatGateway.Name, "field is immutable"),
   176  				)
   177  			}
   178  			if subnet.SecurityGroup.Name != oldSubnet.SecurityGroup.Name {
   179  				allErrs = append(allErrs,
   180  					field.Invalid(field.NewPath("spec", "networkSpec", "subnets").Index(oldSubnetIndex[subnet.Name]).Child("SecurityGroup").Child("Name"),
   181  						c.Spec.NetworkSpec.Subnets[i].SecurityGroup.Name, "field is immutable"),
   182  				)
   183  			}
   184  		}
   185  	}
   186  
   187  	return allErrs
   188  }
   189  
   190  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
   191  func (c *AzureCluster) ValidateDelete() (admission.Warnings, error) {
   192  	return nil, nil
   193  }