Skip to content

Commit 4ac330e

Browse files
Merge pull request #404 from theobarberbany/tb/machineset-vap-tests
OCPCLOUD-2640: Adds MachineSet VAP test suite
2 parents ef85dc9 + e235622 commit 4ac330e

File tree

4 files changed

+293
-46
lines changed

4 files changed

+293
-46
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ require (
3939
sigs.k8s.io/cluster-api-provider-vsphere v1.13.0
4040
sigs.k8s.io/controller-runtime v0.20.4
4141
sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20240927101401-4381fa0aeee4
42+
sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96
4243
sigs.k8s.io/randfill v1.0.0
4344
sigs.k8s.io/yaml v1.4.0
4445
)
@@ -300,6 +301,5 @@ require (
300301
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
301302
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.0 // indirect
302303
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
303-
sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 // indirect
304304
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
305305
)

pkg/admissionpolicy/testutils/util.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
. "github.com/onsi/gomega"
3333
"sigs.k8s.io/controller-runtime/pkg/envtest"
3434

35+
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
3536
"k8s.io/apimachinery/pkg/runtime"
3637
"k8s.io/apimachinery/pkg/util/yaml"
3738
"k8s.io/client-go/kubernetes/scheme"
@@ -174,6 +175,49 @@ func EnvTestWithAuditPolicy(policyYaml string, env *envtest.Environment) {
174175
args.Append("audit-log-format", "json")
175176
}
176177

178+
// SentinelValidationExpression is a CEL expression that blocks resources with the "test-sentinel" label.
179+
// Use this in tests to verify a VAP is actively enforcing.
180+
const SentinelValidationExpression = "!(has(object.metadata.labels) && \"test-sentinel\" in object.metadata.labels)"
181+
182+
// AddSentinelValidation appends a sentinel validation rule to a VAP.
183+
func AddSentinelValidation(vap *admissionregistrationv1.ValidatingAdmissionPolicy) {
184+
vap.Spec.Validations = append(vap.Spec.Validations, admissionregistrationv1.Validation{
185+
Expression: SentinelValidationExpression,
186+
Message: "policy in place",
187+
})
188+
}
189+
190+
// UpdateVAPBindingNamespaces updates a VAP binding's namespace configuration.
191+
//
192+
// Parameters:
193+
// - binding: The ValidatingAdmissionPolicyBinding to update
194+
// - paramNamespace: Namespace containing parameter resources, or "" if no paramRef
195+
// - targetNamespace: Namespace where policy is enforced
196+
func UpdateVAPBindingNamespaces(binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding, paramNamespace, targetNamespace string) {
197+
// Validate paramNamespace matches binding structure
198+
hasParamRef := binding.Spec.ParamRef != nil
199+
ExpectWithOffset(1, hasParamRef && paramNamespace == "").ToNot(BeTrue(),
200+
"paramNamespace cannot be empty for binding %q with paramRef", binding.Name)
201+
ExpectWithOffset(1, !hasParamRef && paramNamespace != "").ToNot(BeTrue(),
202+
"paramNamespace %q provided but binding %q has no paramRef", paramNamespace, binding.Name)
203+
204+
// Update paramRef namespace if parameterized
205+
if hasParamRef {
206+
binding.Spec.ParamRef.Namespace = paramNamespace
207+
}
208+
209+
// Validate MatchResources structure
210+
ExpectWithOffset(1, binding.Spec.MatchResources).ToNot(BeNil(),
211+
"binding %q has nil MatchResources", binding.Name)
212+
ExpectWithOffset(1, binding.Spec.MatchResources.NamespaceSelector).ToNot(BeNil(),
213+
"binding %q has nil NamespaceSelector", binding.Name)
214+
215+
// Always update target namespace
216+
binding.Spec.MatchResources.NamespaceSelector.MatchLabels = map[string]string{
217+
"kubernetes.io/metadata.name": targetNamespace,
218+
}
219+
}
220+
177221
// LoadTransportConfigMaps loads admission policies from the transport config maps in
178222
// `manifests`, providing a map of []client.Object, one per transport config map.
179223
//
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/*
2+
Copyright 2025 Red Hat, Inc.
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 machinesetsync
18+
19+
import (
20+
. "github.com/onsi/ginkgo/v2"
21+
. "github.com/onsi/gomega"
22+
23+
clusterv1resourcebuilder "github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/cluster-api/core/v1beta1"
24+
awsv1resourcebuilder "github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/cluster-api/infrastructure/v1beta2"
25+
corev1resourcebuilder "github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1"
26+
admissiontestutils "github.com/openshift/cluster-capi-operator/pkg/admissionpolicy/testutils"
27+
28+
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
29+
corev1 "k8s.io/api/core/v1"
30+
apierrors "k8s.io/apimachinery/pkg/api/errors"
31+
32+
mapiv1beta1 "github.com/openshift/api/machine/v1beta1"
33+
"github.com/openshift/cluster-api-actuator-pkg/testutils"
34+
awsv1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
35+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
36+
"sigs.k8s.io/controller-runtime/pkg/client"
37+
"sigs.k8s.io/controller-runtime/pkg/envtest/komega"
38+
"sigs.k8s.io/kube-storage-version-migrator/pkg/clients/clientset/scheme"
39+
)
40+
41+
var _ = Describe("MachineSet VAP Tests", func() {
42+
var k komega.Komega
43+
var vapCleanup func()
44+
45+
var capiNamespace *corev1.Namespace
46+
var mapiNamespace *corev1.Namespace
47+
48+
var capiMachineSet *clusterv1.MachineSet
49+
var policyBinding *admissionregistrationv1.ValidatingAdmissionPolicyBinding
50+
var machineSetVap *admissionregistrationv1.ValidatingAdmissionPolicy
51+
52+
BeforeEach(func() {
53+
k = komega.New(k8sClient)
54+
55+
By("Starting the ValidatingAdmissionPolicy status controller")
56+
var err error
57+
vapCleanup, err = admissiontestutils.StartVAPStatusController(ctx, cfg, scheme.Scheme)
58+
Expect(err).NotTo(HaveOccurred())
59+
60+
By("Setting up namespaces for the test")
61+
mapiNamespace = corev1resourcebuilder.Namespace().
62+
WithGenerateName("openshift-machine-api-").Build()
63+
Eventually(k8sClient.Create(ctx, mapiNamespace)).Should(Succeed(), "mapi namespace should be able to be created")
64+
65+
capiNamespace = corev1resourcebuilder.Namespace().
66+
WithGenerateName("openshift-cluster-api-").Build()
67+
Eventually(k8sClient.Create(ctx, capiNamespace)).Should(Succeed(), "capi namespace should be able to be created")
68+
69+
infrastructureName := "cluster-foo"
70+
71+
By("Creating infrastructure resources")
72+
capaClusterBuilder := awsv1resourcebuilder.AWSCluster().
73+
WithNamespace(capiNamespace.GetName()).
74+
WithName(infrastructureName)
75+
Eventually(k8sClient.Create(ctx, capaClusterBuilder.Build())).Should(Succeed(), "capa cluster should be able to be created")
76+
77+
capiClusterBuilder := clusterv1resourcebuilder.Cluster().
78+
WithNamespace(capiNamespace.GetName()).
79+
WithName(infrastructureName)
80+
Eventually(k8sClient.Create(ctx, capiClusterBuilder.Build())).Should(Succeed(), "capi cluster should be able to be created")
81+
82+
capaMachineTemplateBuilder := awsv1resourcebuilder.AWSMachineTemplate().
83+
WithNamespace(capiNamespace.GetName()).
84+
WithName("foo")
85+
86+
capaMachineTemplate := capaMachineTemplateBuilder.Build()
87+
88+
capiMachineTemplate := clusterv1.MachineTemplateSpec{
89+
Spec: clusterv1.MachineSpec{
90+
InfrastructureRef: corev1.ObjectReference{
91+
Kind: capaMachineTemplate.Kind,
92+
Name: capaMachineTemplate.GetName(),
93+
Namespace: capaMachineTemplate.GetNamespace(),
94+
},
95+
},
96+
}
97+
98+
Eventually(k8sClient.Create(ctx, capaMachineTemplate)).Should(Succeed(), "capa machine template should be able to be created")
99+
100+
capiMachineSetBuilder := clusterv1resourcebuilder.MachineSet().
101+
WithNamespace(capiNamespace.GetName()).
102+
WithName("test-machineset").
103+
WithTemplate(capiMachineTemplate).
104+
WithClusterName(infrastructureName)
105+
106+
capiMachineSet = capiMachineSetBuilder.Build()
107+
108+
By("Loading the transport config maps")
109+
transportConfigMaps := admissiontestutils.LoadTransportConfigMaps()
110+
111+
By("Applying the objects found in clusterAPICustomAdmissionPolicies")
112+
for _, obj := range transportConfigMaps[admissiontestutils.ClusterAPICustomAdmissionPolicies] {
113+
newObj, ok := obj.DeepCopyObject().(client.Object)
114+
Expect(ok).To(BeTrue())
115+
116+
Eventually(func() error {
117+
err := k8sClient.Create(ctx, newObj)
118+
if err != nil && !apierrors.IsAlreadyExists(err) {
119+
return err
120+
}
121+
122+
return nil
123+
}, timeout).Should(Succeed())
124+
}
125+
})
126+
127+
AfterEach(func() {
128+
By("Stopping VAP status controller")
129+
if vapCleanup != nil {
130+
vapCleanup()
131+
}
132+
133+
By("Cleaning up VAPs and bindings")
134+
testutils.CleanupResources(Default, ctx, cfg, k8sClient, "",
135+
&admissionregistrationv1.ValidatingAdmissionPolicy{},
136+
&admissionregistrationv1.ValidatingAdmissionPolicyBinding{},
137+
)
138+
139+
By("Cleaning up MAPI test resources")
140+
testutils.CleanupResources(Default, ctx, cfg, k8sClient, mapiNamespace.GetName(),
141+
&mapiv1beta1.Machine{},
142+
&mapiv1beta1.MachineSet{},
143+
)
144+
145+
By("Cleaning up CAPI test resources")
146+
testutils.CleanupResources(Default, ctx, cfg, k8sClient, capiNamespace.GetName(),
147+
&clusterv1.Machine{},
148+
&clusterv1.MachineSet{},
149+
&awsv1.AWSCluster{},
150+
&awsv1.AWSMachineTemplate{},
151+
)
152+
})
153+
154+
Context("Prevent setting of CAPI fields that are not supported by MAPI", func() {
155+
BeforeEach(func() {
156+
By("Waiting for VAP to be ready")
157+
machineSetVap = &admissionregistrationv1.ValidatingAdmissionPolicy{}
158+
Eventually(k8sClient.Get(ctx, client.ObjectKey{Name: "openshift-cluster-api-prevent-setting-of-capi-fields-unsupported-by-mapi"}, machineSetVap), timeout).Should(Succeed())
159+
Eventually(k.Update(machineSetVap, func() {
160+
admissiontestutils.AddSentinelValidation(machineSetVap)
161+
})).Should(Succeed())
162+
163+
Eventually(k.Object(machineSetVap), timeout).Should(
164+
HaveField("Status.ObservedGeneration", BeNumerically(">=", 2)),
165+
)
166+
167+
By("Updating the VAP binding")
168+
policyBinding = &admissionregistrationv1.ValidatingAdmissionPolicyBinding{}
169+
Eventually(k8sClient.Get(ctx, client.ObjectKey{
170+
Name: "openshift-cluster-api-prevent-setting-of-capi-fields-unsupported-by-mapi"}, policyBinding), timeout).Should(Succeed())
171+
172+
Eventually(k.Update(policyBinding, func() {
173+
admissiontestutils.UpdateVAPBindingNamespaces(policyBinding, "", capiNamespace.GetName())
174+
}), timeout).Should(Succeed())
175+
176+
Eventually(k.Object(policyBinding), timeout).Should(
177+
SatisfyAll(
178+
HaveField("Spec.MatchResources.NamespaceSelector.MatchLabels",
179+
HaveKeyWithValue("kubernetes.io/metadata.name",
180+
capiNamespace.GetName())),
181+
),
182+
)
183+
184+
By("Creating a sentinel MachineSet to verify VAP is enforcing")
185+
sentinelMachineSet := clusterv1resourcebuilder.MachineSet().
186+
WithName("sentinel-machineset").
187+
WithNamespace(capiNamespace.Name).
188+
Build()
189+
Eventually(k8sClient.Create(ctx, sentinelMachineSet)).Should(Succeed(), "sentinel machineset should be able to be created")
190+
191+
Eventually(k.Update(sentinelMachineSet, func() {
192+
sentinelMachineSet.ObjectMeta.Labels = map[string]string{"test-sentinel": "fubar"}
193+
}), timeout).Should(MatchError(ContainSubstring("policy in place")))
194+
})
195+
196+
It("should allow creating a MachineSet without forbidden fields", func() {
197+
Eventually(k8sClient.Create(ctx, capiMachineSet)).Should(Succeed())
198+
})
199+
200+
It("should allow updating a MachineSet without changing forbidden fields", func() {
201+
Eventually(k8sClient.Create(ctx, capiMachineSet)).Should(Succeed())
202+
203+
Eventually(k.Update(capiMachineSet, func() {
204+
replicas := int32(3)
205+
capiMachineSet.Spec.Replicas = &replicas
206+
}), timeout).Should(Succeed())
207+
})
208+
209+
It("should deny creating a MachineSet with spec.template.spec.version", func() {
210+
testVersion := "1"
211+
capiMachineSet.Spec.Template.Spec.Version = &testVersion
212+
213+
Eventually(k8sClient.Create(ctx, capiMachineSet), timeout).Should(MatchError(ContainSubstring(".version is a forbidden field")))
214+
})
215+
216+
It("should deny updating spec.template.spec.version on an existing MachineSet", func() {
217+
Eventually(k8sClient.Create(ctx, capiMachineSet)).Should(Succeed())
218+
219+
Eventually(k.Update(capiMachineSet, func() {
220+
testVersion := "1"
221+
capiMachineSet.Spec.Template.Spec.Version = &testVersion
222+
}), timeout).Should(MatchError(ContainSubstring(".version is a forbidden field")))
223+
})
224+
225+
It("should deny creating a MachineSet with spec.template.spec.readinessGates", func() {
226+
capiMachineSet.Spec.Template.Spec.ReadinessGates = []clusterv1.MachineReadinessGate{{ConditionType: "foo"}}
227+
228+
Eventually(k8sClient.Create(ctx, capiMachineSet), timeout).Should(MatchError(ContainSubstring(".readinessGates is a forbidden field")))
229+
})
230+
231+
It("should deny updating spec.template.spec.readinessGates on an existing MachineSet", func() {
232+
Eventually(k8sClient.Create(ctx, capiMachineSet)).Should(Succeed())
233+
234+
Eventually(k.Update(capiMachineSet, func() {
235+
capiMachineSet.Spec.Template.Spec.ReadinessGates = []clusterv1.MachineReadinessGate{{ConditionType: "foo"}}
236+
}), timeout).Should(MatchError(ContainSubstring(".readinessGates is a forbidden field")))
237+
})
238+
})
239+
})

0 commit comments

Comments
 (0)