@@ -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,17 +56,7 @@ func (*openStackClusterWebhook) ValidateCreate(_ context.Context, objRaw runtime
5656 }
5757
5858 if newObj .Spec .ManagedSecurityGroups != nil {
59- for _ , rule := range newObj .Spec .ManagedSecurityGroups .AllNodesSecurityGroupRules {
60- if rule .RemoteManagedGroups != nil && (rule .RemoteGroupID != nil || rule .RemoteIPPrefix != nil ) {
61- allErrs = append (allErrs , field .Forbidden (field .NewPath ("spec" , "managedSecurityGroups" , "allNodesSecurityGroupRules" ), "remoteManagedGroups cannot be used with remoteGroupID or remoteIPPrefix" ))
62- }
63- if rule .RemoteGroupID != nil && (rule .RemoteManagedGroups != nil || rule .RemoteIPPrefix != nil ) {
64- allErrs = append (allErrs , field .Forbidden (field .NewPath ("spec" , "managedSecurityGroups" , "allNodesSecurityGroupRules" ), "remoteGroupID cannot be used with remoteManagedGroups or remoteIPPrefix" ))
65- }
66- if rule .RemoteIPPrefix != nil && (rule .RemoteManagedGroups != nil || rule .RemoteGroupID != nil ) {
67- allErrs = append (allErrs , field .Forbidden (field .NewPath ("spec" , "managedSecurityGroups" , "allNodesSecurityGroupRules" ), "remoteIPPrefix cannot be used with remoteManagedGroups or remoteGroupID" ))
68- }
69- }
59+ allErrs = append (allErrs , validateManagedSecurityGroupRules (newObj .Spec .ManagedSecurityGroups )... )
7060 }
7161
7262 return aggregateObjErrors (newObj .GroupVersionKind ().GroupKind (), newObj .Name , allErrs )
@@ -80,7 +70,7 @@ func (*openStackClusterWebhook) ValidateCreate(_ context.Context, objRaw runtime
8070// spec matches the subnet name in status, and the new ID matches the corresponding subnet ID.
8171//
8272// Returns true if all such transitions are valid; false otherwise.
83- func allowSubnetFilterToIDTransition (oldObj , newObj * infrav1 .OpenStackCluster ) bool {
73+ func allowSubnetFilterToIDTransition (oldObj , newObj * infrav1beta2 .OpenStackCluster ) bool {
8474 if newObj .Spec .Network == nil || oldObj .Spec .Network == nil || oldObj .Status .Network == nil {
8575 return false
8676 }
@@ -139,8 +129,8 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
139129 }
140130
141131 // Allow changes to Spec.IdentityRef
142- oldObj .Spec .IdentityRef = infrav1 .OpenStackIdentityReference {}
143- newObj .Spec .IdentityRef = infrav1 .OpenStackIdentityReference {}
132+ oldObj .Spec .IdentityRef = infrav1beta2 .OpenStackIdentityReference {}
133+ newObj .Spec .IdentityRef = infrav1beta2 .OpenStackIdentityReference {}
144134
145135 // Allow changes to ControlPlaneEndpoint fields only if it was not
146136 // previously set, or did not previously have a host.
@@ -151,7 +141,7 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
151141 newObj .Spec .ControlPlaneEndpoint = nil
152142 }
153143
154- // Allow change only for the first time.
144+ // Allow APIServerFixedIP to be set for the first time when floating IP is disabled .
155145 if ptr .Deref (oldObj .Spec .DisableAPIServerFloatingIP , false ) && ptr .Deref (oldObj .Spec .APIServerFixedIP , "" ) == "" {
156146 oldObj .Spec .APIServerFixedIP = nil
157147 newObj .Spec .APIServerFixedIP = nil
@@ -170,47 +160,55 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
170160 }
171161
172162 // Allow changes to the bastion spec.
173- oldObj .Spec .Bastion = & infrav1.Bastion {}
174- 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+ }
175170
176171 // Allow changes to the managed securityGroupRules.
177172 if newObj .Spec .ManagedSecurityGroups != nil {
178173 if oldObj .Spec .ManagedSecurityGroups == nil {
179- oldObj .Spec .ManagedSecurityGroups = & infrav1 .ManagedSecurityGroups {}
174+ oldObj .Spec .ManagedSecurityGroups = & infrav1beta2 .ManagedSecurityGroups {}
180175 }
181176
182- oldObj .Spec .ManagedSecurityGroups .AllNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
183- newObj .Spec .ManagedSecurityGroups .AllNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
177+ oldObj .Spec .ManagedSecurityGroups .AllNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
178+ newObj .Spec .ManagedSecurityGroups .AllNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
184179
185- oldObj .Spec .ManagedSecurityGroups .ControlPlaneNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
186- newObj .Spec .ManagedSecurityGroups .ControlPlaneNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
180+ oldObj .Spec .ManagedSecurityGroups .ControlPlaneNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
181+ newObj .Spec .ManagedSecurityGroups .ControlPlaneNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
187182
188- oldObj .Spec .ManagedSecurityGroups .WorkerNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
189- newObj .Spec .ManagedSecurityGroups .WorkerNodesSecurityGroupRules = []infrav1 .SecurityGroupRuleSpec {}
183+ oldObj .Spec .ManagedSecurityGroups .WorkerNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
184+ newObj .Spec .ManagedSecurityGroups .WorkerNodesSecurityGroupRules = []infrav1beta2 .SecurityGroupRuleSpec {}
190185
191186 // Allow change to the allowAllInClusterTraffic.
192187 oldObj .Spec .ManagedSecurityGroups .AllowAllInClusterTraffic = false
193188 newObj .Spec .ManagedSecurityGroups .AllowAllInClusterTraffic = false
194189 }
195190
196- // 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.
197195 if newObj .Spec .ManagedSubnets != nil && oldObj .Spec .ManagedSubnets != nil {
198- // Check if any fields other than DNSNameservers have changed
196+ // Check if any fields other than DNSNameservers have changed.
199197 if len (oldObj .Spec .ManagedSubnets ) != len (newObj .Spec .ManagedSubnets ) {
200198 allErrs = append (allErrs , field .Forbidden (field .NewPath ("spec" , "managedSubnets" ), "cannot add or remove subnets" ))
201199 } else {
202- // Build maps of subnets by CIDR
203- oldSubnetMap := make (map [string ]* infrav1.SubnetSpec )
204-
200+ // Build maps of subnets by CIDR.
201+ oldSubnetMap := make (map [string ]* infrav1beta2.SubnetSpec )
205202 for i := range oldObj .Spec .ManagedSubnets {
206203 oldSubnet := & oldObj .Spec .ManagedSubnets [i ]
207204 oldSubnetMap [oldSubnet .CIDR ] = oldSubnet
208205 }
209206
210- // Check if all new subnets have matching old subnets with the same CIDR
207+ // Compare each new subnet against the old map by CIDR.
211208 for i := range newObj .Spec .ManagedSubnets {
212209 newSubnet := & newObj .Spec .ManagedSubnets [i ]
213210
211+ // If the CIDR is not found in the old map, it's a new subnet.
214212 oldSubnet , exists := oldSubnetMap [newSubnet .CIDR ]
215213 if ! exists {
216214 allErrs = append (allErrs , field .Forbidden (
@@ -220,7 +218,7 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
220218 continue
221219 }
222220
223- // DNSNameservers is mutable
221+ // Zero out DNSNameservers on both copies so they are ignored by DeepEqual.
224222 oldSubnet .DNSNameservers = nil
225223 newSubnet .DNSNameservers = nil
226224 }
@@ -235,8 +233,8 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
235233
236234 // Allow changes on APIServerLB monitors
237235 if newObj .Spec .APIServerLoadBalancer != nil && oldObj .Spec .APIServerLoadBalancer != nil {
238- oldObj .Spec .APIServerLoadBalancer .Monitor = & infrav1 .APIServerLoadBalancerMonitor {}
239- newObj .Spec .APIServerLoadBalancer .Monitor = & infrav1 .APIServerLoadBalancerMonitor {}
236+ oldObj .Spec .APIServerLoadBalancer .Monitor = & infrav1beta2 .APIServerLoadBalancerMonitor {}
237+ newObj .Spec .APIServerLoadBalancer .Monitor = & infrav1beta2 .APIServerLoadBalancerMonitor {}
240238 }
241239
242240 // Allow changes to the availability zones.
@@ -254,20 +252,21 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new
254252 oldObj .Spec .APIServerFloatingIP = nil
255253 }
256254
257- // 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.
258257 if newObj .Spec .Network != nil && oldObj .Spec .Network != nil && oldObj .Status .Network != nil {
259- // Allow change from spec.network.subnets from filter to id if it matches the current subnets.
260258 if allowSubnetFilterToIDTransition (oldObj , newObj ) {
261259 oldObj .Spec .Subnets = nil
262260 newObj .Spec .Subnets = nil
263261 }
264- // Allow change from spec.network.filter to spec.network.id only if it matches the current network.
265262 if ptr .Deref (newObj .Spec .Network .ID , "" ) == oldObj .Status .Network .ID {
266263 newObj .Spec .Network = nil
267264 oldObj .Spec .Network = nil
268265 }
269266 }
270267
268+ // After zeroing out all mutable fields above, any remaining difference
269+ // in spec is an immutability violation.
271270 if ! reflect .DeepEqual (oldObj .Spec , newObj .Spec ) {
272271 allErrs = append (allErrs , field .Forbidden (field .NewPath ("spec" ), "cannot be modified" ))
273272 }
@@ -280,8 +279,36 @@ func (*openStackClusterWebhook) ValidateDelete(_ context.Context, _ runtime.Obje
280279 return nil , nil
281280}
282281
283- func castToOpenStackCluster (obj runtime.Object ) (* infrav1.OpenStackCluster , error ) {
284- 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 )
285312 if ! ok {
286313 return nil , fmt .Errorf ("expected an OpenStackCluster but got a %T" , obj )
287314 }
0 commit comments