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 }