Skip to content

Commit acc512e

Browse files
committed
tests: add webhook tests
Signed-off-by: Bharath Nallapeta <nr.bharath97@gmail.com>
1 parent 0fe38ef commit acc512e

16 files changed

Lines changed: 996 additions & 2752 deletions

config/webhook/manifests.yaml

Lines changed: 0 additions & 84 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/webhooks/openstackcluster_webhook.go

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
3738
func 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

4445
type openStackClusterWebhook struct{}
4546

46-
// Compile-time assertion that openStackClusterWebhook implements webhook.CustomValidator.
4747
var _ 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

Comments
 (0)