@@ -29,21 +29,21 @@ import (
2929 "sigs.k8s.io/controller-runtime/pkg/webhook"
3030 "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3131
32- infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1 "
32+ infrav1beta2 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta2 "
3333)
3434
35- // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1 -openstackcluster,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=openstackclusters,versions=v1beta1 ,name=validation.openstackcluster.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
35+ // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta2 -openstackcluster,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=openstackclusters,versions=v1beta2 ,name=validation.openstackcluster.v1beta2. infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1
3636
37+ // SetupOpenStackClusterWebhook registers the validating webhook for OpenStackCluster with the manager.
3738func SetupOpenStackClusterWebhook (mgr manager.Manager ) error {
3839 return builder .WebhookManagedBy (mgr ).
39- For (& infrav1 .OpenStackCluster {}).
40+ For (& infrav1beta2 .OpenStackCluster {}).
4041 WithValidator (& openStackClusterWebhook {}).
4142 Complete ()
4243}
4344
4445type openStackClusterWebhook struct {}
4546
46- // Compile-time assertion that openStackClusterWebhook implements webhook.CustomValidator.
4747var _ webhook.CustomValidator = & openStackClusterWebhook {}
4848
4949// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type.
@@ -56,10 +56,7 @@ func (*openStackClusterWebhook) ValidateCreate(_ context.Context, objRaw runtime
5656 }
5757
5858 if newObj .Spec .ManagedSecurityGroups != nil {
59- allErrs = append (allErrs , validateSecurityGroupRulesRemoteMutualExclusionV1Beta1 (
60- newObj .Spec .ManagedSecurityGroups .AllNodesSecurityGroupRules ,
61- field .NewPath ("spec" , "managedSecurityGroups" , "allNodesSecurityGroupRules" ),
62- )... )
59+ allErrs = append (allErrs , validateManagedSecurityGroupRules (newObj .Spec .ManagedSecurityGroups )... )
6360 }
6461
6562 return aggregateObjErrors (newObj .GroupVersionKind ().GroupKind (), newObj .Name , allErrs )
@@ -73,7 +70,7 @@ func (*openStackClusterWebhook) ValidateCreate(_ context.Context, objRaw runtime
7370// spec matches the subnet name in status, and the new ID matches the corresponding subnet ID.
7471//
7572// Returns true if all such transitions are valid; false otherwise.
76- func allowSubnetFilterToIDTransition (oldObj , newObj * infrav1 .OpenStackCluster ) bool {
73+ func allowSubnetFilterToIDTransition (oldObj , newObj * infrav1beta2 .OpenStackCluster ) bool {
7774 if newObj .Spec .Network == nil || oldObj .Spec .Network == nil || oldObj .Status .Network == nil {
7875 return false
7976 }
@@ -132,8 +129,8 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
132129 }
133130
134131 // Allow changes to Spec.IdentityRef
135- oldObj .Spec .IdentityRef = infrav1 .OpenStackIdentityReference {}
136- newObj .Spec .IdentityRef = infrav1 .OpenStackIdentityReference {}
132+ oldObj .Spec .IdentityRef = infrav1beta2 .OpenStackIdentityReference {}
133+ newObj .Spec .IdentityRef = infrav1beta2 .OpenStackIdentityReference {}
137134
138135 // Allow changes to ControlPlaneEndpoint fields only if it was not
139136 // previously set, or did not previously have a host.
@@ -144,7 +141,7 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
144141 newObj .Spec .ControlPlaneEndpoint = nil
145142 }
146143
147- // Allow change only for the first time.
144+ // Allow APIServerFixedIP to be set for the first time when floating IP is disabled .
148145 if ptr .Deref (oldObj .Spec .DisableAPIServerFloatingIP , false ) && ptr .Deref (oldObj .Spec .APIServerFixedIP , "" ) == "" {
149146 oldObj .Spec .APIServerFixedIP = nil
150147 newObj .Spec .APIServerFixedIP = nil
@@ -163,47 +160,55 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
163160 }
164161
165162 // Allow changes to the bastion spec.
166- oldObj .Spec .Bastion = & infrav1.Bastion {}
167- newObj .Spec .Bastion = & infrav1.Bastion {}
163+ oldObj .Spec .Bastion = & infrav1beta2.Bastion {}
164+ newObj .Spec .Bastion = & infrav1beta2.Bastion {}
165+
166+ // Validate new security group rules before zeroing them out for immutability comparison.
167+ if newObj .Spec .ManagedSecurityGroups != nil {
168+ allErrs = append (allErrs , validateManagedSecurityGroupRules (newObj .Spec .ManagedSecurityGroups )... )
169+ }
168170
169171 // Allow changes to the managed securityGroupRules.
170172 if newObj .Spec .ManagedSecurityGroups != nil {
171173 if oldObj .Spec .ManagedSecurityGroups == nil {
172- oldObj .Spec .ManagedSecurityGroups = & infrav1 .ManagedSecurityGroups {}
174+ oldObj .Spec .ManagedSecurityGroups = & infrav1beta2 .ManagedSecurityGroups {}
173175 }
174176
175- oldObj .Spec .ManagedSecurityGroups .AllNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
176- newObj .Spec .ManagedSecurityGroups .AllNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
177+ oldObj .Spec .ManagedSecurityGroups .AllNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
178+ newObj .Spec .ManagedSecurityGroups .AllNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
177179
178- oldObj .Spec .ManagedSecurityGroups .ControlPlaneNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
179- newObj .Spec .ManagedSecurityGroups .ControlPlaneNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
180+ oldObj .Spec .ManagedSecurityGroups .ControlPlaneNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
181+ newObj .Spec .ManagedSecurityGroups .ControlPlaneNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
180182
181- oldObj .Spec .ManagedSecurityGroups .WorkerNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
182- newObj .Spec .ManagedSecurityGroups .WorkerNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
183+ oldObj .Spec .ManagedSecurityGroups .WorkerNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
184+ newObj .Spec .ManagedSecurityGroups .WorkerNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
183185
184186 // Allow change to the allowAllInClusterTraffic.
185187 oldObj .Spec .ManagedSecurityGroups .AllowAllInClusterTraffic = false
186188 newObj .Spec .ManagedSecurityGroups .AllowAllInClusterTraffic = false
187189 }
188190
189- // Allow changes only to DNSNameservers in ManagedSubnets spec
191+ // Allow changes only to DNSNameservers in ManagedSubnets spec.
192+ // Subnets are matched by CIDR: the CIDR itself and AllocationPools are
193+ // immutable, but DNSNameservers can be updated. We zero out DNSNameservers
194+ // on both copies so the final DeepEqual ignores those changes.
190195 if newObj .Spec .ManagedSubnets != nil && oldObj .Spec .ManagedSubnets != nil {
191- // Check if any fields other than DNSNameservers have changed
196+ // Check if any fields other than DNSNameservers have changed.
192197 if len (oldObj .Spec .ManagedSubnets ) != len (newObj .Spec .ManagedSubnets ) {
193198 allErrs = append (allErrs , field .Forbidden (field .NewPath ("spec" , "managedSubnets" ), "cannot add or remove subnets" ))
194199 } else {
195- // Build maps of subnets by CIDR
196- oldSubnetMap := make (map [string ]* infrav1.SubnetSpec )
197-
200+ // Build maps of subnets by CIDR.
201+ oldSubnetMap := make (map [string ]* infrav1beta2.SubnetSpec )
198202 for i := range oldObj .Spec .ManagedSubnets {
199203 oldSubnet := & oldObj .Spec .ManagedSubnets [i ]
200204 oldSubnetMap [oldSubnet .CIDR ] = oldSubnet
201205 }
202206
203- // Check if all new subnets have matching old subnets with the same CIDR
207+ // Compare each new subnet against the old map by CIDR.
204208 for i := range newObj .Spec .ManagedSubnets {
205209 newSubnet := & newObj .Spec .ManagedSubnets [i ]
206210
211+ // If the CIDR is not found in the old map, it's a new subnet.
207212 oldSubnet , exists := oldSubnetMap [newSubnet .CIDR ]
208213 if ! exists {
209214 allErrs = append (allErrs , field .Forbidden (
@@ -213,7 +218,7 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
213218 continue
214219 }
215220
216- // DNSNameservers is mutable
221+ // Zero out DNSNameservers on both copies so they are ignored by DeepEqual.
217222 oldSubnet .DNSNameservers = nil
218223 newSubnet .DNSNameservers = nil
219224 }
@@ -228,8 +233,8 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
228233
229234 // Allow changes on APIServerLB monitors
230235 if newObj .Spec .APIServerLoadBalancer != nil && oldObj .Spec .APIServerLoadBalancer != nil {
231- oldObj .Spec .APIServerLoadBalancer .Monitor = & infrav1 .APIServerLoadBalancerMonitor {}
232- newObj .Spec .APIServerLoadBalancer .Monitor = & infrav1 .APIServerLoadBalancerMonitor {}
236+ oldObj .Spec .APIServerLoadBalancer .Monitor = & infrav1beta2 .APIServerLoadBalancerMonitor {}
237+ newObj .Spec .APIServerLoadBalancer .Monitor = & infrav1beta2 .APIServerLoadBalancerMonitor {}
233238 }
234239
235240 // Allow changes to the availability zones.
@@ -247,20 +252,21 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
247252 oldObj .Spec .APIServerFloatingIP = nil
248253 }
249254
250- // Allow changes from filter to id for spec.network and spec.subnets
255+ // Allow transitioning spec.network from filter to id and spec.subnets from
256+ // filter to id. This lets users pin to resolved IDs after initial creation.
251257 if newObj .Spec .Network != nil && oldObj .Spec .Network != nil && oldObj .Status .Network != nil {
252- // Allow change from spec.network.subnets from filter to id if it matches the current subnets.
253258 if allowSubnetFilterToIDTransition (oldObj , newObj ) {
254259 oldObj .Spec .Subnets = nil
255260 newObj .Spec .Subnets = nil
256261 }
257- // Allow change from spec.network.filter to spec.network.id only if it matches the current network.
258262 if ptr .Deref (newObj .Spec .Network .ID , "" ) == oldObj .Status .Network .ID {
259263 newObj .Spec .Network = nil
260264 oldObj .Spec .Network = nil
261265 }
262266 }
263267
268+ // After zeroing out all mutable fields above, any remaining difference
269+ // in spec is an immutability violation.
264270 if ! reflect .DeepEqual (oldObj .Spec , newObj .Spec ) {
265271 allErrs = append (allErrs , field .Forbidden (field .NewPath ("spec" ), "cannot be modified" ))
266272 }
@@ -273,8 +279,36 @@ func (*openStackClusterWebhook) ValidateDelete(_ context.Context, _ runtime.Obje
273279 return nil , nil
274280}
275281
276- func castToOpenStackCluster (obj runtime.Object ) (* infrav1.OpenStackCluster , error ) {
277- cast , ok := obj .(* infrav1.OpenStackCluster )
282+ // securityGroupRemoteFields returns whether each remote field is set on a SecurityGroupRuleSpec.
283+ func securityGroupRemoteFields (r * infrav1beta2.SecurityGroupRuleSpec ) (bool , bool , bool ) {
284+ return r .RemoteManagedGroups != nil , r .RemoteGroupID != nil , r .RemoteIPPrefix != nil
285+ }
286+
287+ // validateManagedSecurityGroupRules validates that remote* fields are mutually exclusive
288+ // across all security group rule lists in ManagedSecurityGroups.
289+ func validateManagedSecurityGroupRules (msg * infrav1beta2.ManagedSecurityGroups ) field.ErrorList {
290+ var allErrs field.ErrorList
291+ allErrs = append (allErrs , validateSecurityGroupRulesRemoteMutualExclusion (
292+ msg .AllNodesSecurityGroupRules ,
293+ field .NewPath ("spec" , "managedSecurityGroups" , "allNodesSecurityGroupRules" ),
294+ securityGroupRemoteFields ,
295+ )... )
296+ allErrs = append (allErrs , validateSecurityGroupRulesRemoteMutualExclusion (
297+ msg .ControlPlaneNodesSecurityGroupRules ,
298+ field .NewPath ("spec" , "managedSecurityGroups" , "controlPlaneNodesSecurityGroupRules" ),
299+ securityGroupRemoteFields ,
300+ )... )
301+ allErrs = append (allErrs , validateSecurityGroupRulesRemoteMutualExclusion (
302+ msg .WorkerNodesSecurityGroupRules ,
303+ field .NewPath ("spec" , "managedSecurityGroups" , "workerNodesSecurityGroupRules" ),
304+ securityGroupRemoteFields ,
305+ )... )
306+ return allErrs
307+ }
308+
309+ // castToOpenStackCluster casts a runtime.Object to an OpenStackCluster.
310+ func castToOpenStackCluster (obj runtime.Object ) (* infrav1beta2.OpenStackCluster , error ) {
311+ cast , ok := obj .(* infrav1beta2.OpenStackCluster )
278312 if ! ok {
279313 return nil , fmt .Errorf ("expected an OpenStackCluster but got a %T" , obj )
280314 }
0 commit comments