sigs.k8s.io/cluster-api@v1.7.1/exp/ipam/internal/webhooks/ipaddress.go (about)

     1  /*
     2  Copyright 2022 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 webhooks
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/netip"
    23  	"reflect"
    24  
    25  	"github.com/pkg/errors"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/types"
    29  	"k8s.io/apimachinery/pkg/util/validation/field"
    30  	ctrl "sigs.k8s.io/controller-runtime"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    33  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    34  
    35  	ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1"
    36  )
    37  
    38  // SetupWebhookWithManager sets up IPAddress webhooks.
    39  func (webhook *IPAddress) SetupWebhookWithManager(mgr ctrl.Manager) error {
    40  	return ctrl.NewWebhookManagedBy(mgr).
    41  		For(&ipamv1.IPAddress{}).
    42  		WithValidator(webhook).
    43  		Complete()
    44  }
    45  
    46  // +kubebuilder:webhook:verbs=create;update;delete,path=/validate-ipam-cluster-x-k8s-io-v1beta1-ipaddress,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=ipam.cluster.x-k8s.io,resources=ipaddresses,versions=v1beta1,name=validation.ipaddress.ipam.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    47  // +kubebuilder:rbac:groups=ipam.cluster.x-k8s.io,resources=ipaddressclaims,verbs=get;list;watch
    48  
    49  // IPAddress implements a validating webhook for IPAddress.
    50  type IPAddress struct {
    51  	Client client.Reader
    52  }
    53  
    54  var _ webhook.CustomValidator = &IPAddress{}
    55  
    56  // ValidateCreate implements webhook.CustomValidator.
    57  func (webhook *IPAddress) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
    58  	ip, ok := obj.(*ipamv1.IPAddress)
    59  	if !ok {
    60  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an IPAddress but got a %T", obj))
    61  	}
    62  	return nil, webhook.validate(ctx, ip)
    63  }
    64  
    65  // ValidateUpdate implements webhook.CustomValidator.
    66  func (webhook *IPAddress) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
    67  	oldIP, ok := oldObj.(*ipamv1.IPAddress)
    68  	if !ok {
    69  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an IPAddress but got a %T", oldObj))
    70  	}
    71  	newIP, ok := newObj.(*ipamv1.IPAddress)
    72  	if !ok {
    73  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an IPAddress but got a %T", newObj))
    74  	}
    75  
    76  	if !reflect.DeepEqual(oldIP.Spec, newIP.Spec) {
    77  		return nil, field.Forbidden(field.NewPath("spec"), "the spec of IPAddress is immutable")
    78  	}
    79  	return nil, nil
    80  }
    81  
    82  // ValidateDelete implements webhook.CustomValidator.
    83  func (webhook *IPAddress) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
    84  	return nil, nil
    85  }
    86  
    87  func (webhook *IPAddress) validate(ctx context.Context, ip *ipamv1.IPAddress) error {
    88  	log := ctrl.LoggerFrom(ctx)
    89  	allErrs := field.ErrorList{}
    90  	specPath := field.NewPath("spec")
    91  
    92  	addr, err := netip.ParseAddr(ip.Spec.Address)
    93  	if err != nil {
    94  		allErrs = append(allErrs,
    95  			field.Invalid(
    96  				specPath.Child("address"),
    97  				ip.Spec.Address,
    98  				"not a valid IP address",
    99  			))
   100  	}
   101  
   102  	if ip.Spec.Prefix < 0 {
   103  		allErrs = append(allErrs,
   104  			field.Invalid(
   105  				specPath.Child("prefix"),
   106  				ip.Spec.Prefix,
   107  				"prefix cannot be negative",
   108  			))
   109  	}
   110  	if addr.Is4() && ip.Spec.Prefix > 32 {
   111  		allErrs = append(allErrs,
   112  			field.Invalid(
   113  				specPath.Child("prefix"),
   114  				ip.Spec.Prefix,
   115  				"prefix is too large for an IPv4 address",
   116  			))
   117  	}
   118  	if addr.Is6() && ip.Spec.Prefix > 128 {
   119  		allErrs = append(allErrs,
   120  			field.Invalid(
   121  				specPath.Child("prefix"),
   122  				ip.Spec.Prefix,
   123  				"prefix is too large for an IPv6 address",
   124  			))
   125  	}
   126  
   127  	if ip.Spec.Gateway != "" {
   128  		if _, err := netip.ParseAddr(ip.Spec.Gateway); err != nil {
   129  			allErrs = append(allErrs,
   130  				field.Invalid(
   131  					specPath.Child("gateway"),
   132  					ip.Spec.Gateway,
   133  					"not a valid IP address",
   134  				))
   135  		}
   136  	}
   137  
   138  	if ip.Spec.PoolRef.APIGroup == nil {
   139  		allErrs = append(allErrs,
   140  			field.Invalid(
   141  				specPath.Child("poolRef.apiGroup"),
   142  				ip.Spec.PoolRef.APIGroup,
   143  				"the pool reference needs to contain a group"))
   144  	}
   145  
   146  	claim := &ipamv1.IPAddressClaim{}
   147  	err = webhook.Client.Get(ctx, types.NamespacedName{Name: ip.Spec.ClaimRef.Name, Namespace: ip.ObjectMeta.Namespace}, claim)
   148  	if err != nil && !apierrors.IsNotFound(err) {
   149  		log.Error(err, "failed to fetch claim", "name", ip.Spec.ClaimRef.Name)
   150  		allErrs = append(allErrs,
   151  			field.InternalError(
   152  				specPath.Child("claimRef"),
   153  				errors.Wrap(err, "failed to fetch claim"),
   154  			),
   155  		)
   156  	}
   157  
   158  	if claim.Name != "" && // only report non-matching pool if the claim exists
   159  		!(ip.Spec.PoolRef.APIGroup != nil && claim.Spec.PoolRef.APIGroup != nil &&
   160  			*ip.Spec.PoolRef.APIGroup == *claim.Spec.PoolRef.APIGroup &&
   161  			ip.Spec.PoolRef.Kind == claim.Spec.PoolRef.Kind &&
   162  			ip.Spec.PoolRef.Name == claim.Spec.PoolRef.Name) {
   163  		allErrs = append(allErrs,
   164  			field.Invalid(
   165  				specPath.Child("poolRef"),
   166  				ip.Spec.PoolRef,
   167  				"the referenced pool is different from the pool referenced by the claim this address should fulfill",
   168  			))
   169  	}
   170  
   171  	return allErrs.ToAggregate()
   172  }