Skip to content

Commit

Permalink
Add the skipObject template function
Browse files Browse the repository at this point in the history
When a user wants further filtering based on the object name, they can
call the `skipObject` template function to skip it from being considered
for evaluation.

When all objects are skipped or there's no match from the object
selector, the policy is considered compliant.

Relates:
https://issues.redhat.com/browse/ACM-15937

Signed-off-by: mprahl <[email protected]>
  • Loading branch information
mprahl authored and openshift-merge-bot[bot] committed Nov 27, 2024
1 parent cd6348a commit e931ef2
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 31 deletions.
61 changes: 49 additions & 12 deletions controllers/configurationpolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -1472,9 +1472,23 @@ func (r *ConfigurationPolicyReconciler) determineDesiredObjects(
// name and/or namespace.
var desiredObj *unstructured.Unstructured

var skipObjectCalled bool

// Iterate over the parsed object namespace to name map to resolve Go templates
for ns, names := range relevantNsNames {
for nameIdx, name := range names {
// If the templates use the .ObjectNamespace template variable, the desired object cannot be resused across
// namespaces.
if needsPerNamespaceTemplating {
desiredObj = nil
}

for _, name := range names {
// If the templates use the .ObjectName template variable, the desired object cannot be resused across
// names.
if needsPerNameTemplating {
desiredObj = nil
}

var rawDesiredObject []byte

// If object-templates-raw was used, the templates were already resolved
Expand All @@ -1486,18 +1500,36 @@ func (r *ConfigurationPolicyReconciler) determineDesiredObjects(
// used but ObjectNamespace is
// - Only on the first namespace (outer) loop if the ObjectNamespace
// template variable isn't used
if tmplResolver != nil && hasTemplate &&
(needsPerNameTemplating || (nameIdx == 0 && needsPerNamespaceTemplating) || desiredObj == nil) {
if tmplResolver != nil && hasTemplate && desiredObj == nil {
r.processedPolicyCache.Delete(plc.GetUID())

templateContext := struct {
ObjectNamespace string
ObjectName string
}{ObjectNamespace: ns, ObjectName: name}

skipObject := false

resolveOptions.CustomFunctions = map[string]interface{}{
"skipObject": func() string {
skipObject = true

return ""
},
}

resolvedTemplate, err := tmplResolver.ResolveTemplate(
objectT.ObjectDefinition.Raw, templateContext, resolveOptions,
)

if skipObject {
log.V(1).Info("skipObject called", "namespace", ns, "name", name, "objectTemplateIndex", index)

skipObjectCalled = true

continue
}

if err != nil {
var complianceMsg string

Expand Down Expand Up @@ -1599,18 +1631,23 @@ func (r *ConfigurationPolicyReconciler) determineDesiredObjects(
if len(desiredObjects) == 0 {
log.Info("Final desired object list is empty after processing selectors")

msg := fmt.Sprintf("object of kind %s has no name specified "+
"from the policy objectSelector nor the object metadata",
objGVK.Kind,
)
var msg string

errEvent := &objectTmplEvalEvent{
compliant: false,
reason: "objectSelector error",
message: msg,
if skipObjectCalled {
msg = "All objects of kind %s were skipped by the `skipObject` template function"
} else if objectSelector != nil {
msg = "No objects of kind %s were matched from the policy objectSelector"
} else {
msg = "No objects of kind %s were matched from the objectDefinition metadata"
}

event := &objectTmplEvalEvent{
compliant: true,
reason: "",
message: fmt.Sprintf(msg, objGVK.Kind),
}

return nil, &scopedGVR, errEvent, err
return nil, &scopedGVR, event, nil
}

return desiredObjects, &scopedGVR, nil, nil
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/stolostron/go-log-utils v0.1.2
github.com/stolostron/go-template-utils/v6 v6.3.0
github.com/stolostron/go-template-utils/v6 v6.4.0
github.com/stolostron/kubernetes-dependency-watches v0.10.0
github.com/stretchr/testify v1.9.0
go.uber.org/zap v1.27.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AV
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stolostron/go-log-utils v0.1.2 h1:7l1aJWvBqU2+DUyimcslT5SJpdygVY/clRDmX5sO29c=
github.com/stolostron/go-log-utils v0.1.2/go.mod h1:8zrB8UJmp1rXhv3Ck9bBl5SpNfKk3SApeElbo96YRtQ=
github.com/stolostron/go-template-utils/v6 v6.3.0 h1:ptbIZcJqcWtT8baOrQyoqVFGj1PKQqyJzA9m+mcAHx8=
github.com/stolostron/go-template-utils/v6 v6.3.0/go.mod h1:4+HWMKPMBDDekJlPve3zkuWdE0hcwgnKKOap2GoHjNY=
github.com/stolostron/go-template-utils/v6 v6.4.0 h1:FKEK8rNi2VGhuEXY6gROfO6VBJ0myGqjfwfnRsLkpuY=
github.com/stolostron/go-template-utils/v6 v6.4.0/go.mod h1:4+HWMKPMBDDekJlPve3zkuWdE0hcwgnKKOap2GoHjNY=
github.com/stolostron/kubernetes-dependency-watches v0.10.0 h1:brg9FCZUvd1gnm5wmsv/InfErcPUvYcZsK/LWNRr+wg=
github.com/stolostron/kubernetes-dependency-watches v0.10.0/go.mod h1:j1DBv/3JjwDX3bT/oKB4YvSwJ6DEVcrUpEzKbFLM0QM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
53 changes: 41 additions & 12 deletions test/e2e/case13_templatization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,14 +571,16 @@ var _ = Describe("Test templatization", Ordered, func() {

Describe("Policy with the ObjectName variable", Ordered, func() {
const (
preReqs = case13RsrcPath + "/case13_objectname_var_prereqs.yaml"
policyYAML = case13RsrcPath + "/case13_objectname_var.yaml"
policyName = "case13-objectname-var"
e2eBaseName = "case13-e2e-objectname-var"
invalidPolicyYAML = case13RsrcPath + "/case13_objectname_var_invalid_name.yaml"
invalidPolicyName = "case13-invalid-name"
emptyPolicyYAML = case13RsrcPath + "/case13_objectname_var_empty_name.yaml"
emptyPolicyName = "case13-empty-name"
preReqs = case13RsrcPath + "/case13_objectname_var_prereqs.yaml"
policyYAML = case13RsrcPath + "/case13_objectname_var.yaml"
policyName = "case13-objectname-var"
e2eBaseName = "case13-e2e-objectname-var"
invalidPolicyYAML = case13RsrcPath + "/case13_objectname_var_invalid_name.yaml"
invalidPolicyName = "case13-invalid-name"
emptyPolicyYAML = case13RsrcPath + "/case13_objectname_var_empty_name.yaml"
emptyPolicyName = "case13-empty-name"
allSkippedPolicyYAML = case13RsrcPath + "/case13_objectname_var_all_skipped.yaml"
allSkippedPolicyName = "case13-objectname-var-all-skipped"
)

BeforeEach(func() {
Expand All @@ -598,19 +600,19 @@ var _ = Describe("Test templatization", Ordered, func() {

utils.CheckComplianceStatus(g, managedPlc, "Compliant")
g.Expect(utils.GetStatusMessage(managedPlc)).Should(Equal(fmt.Sprintf(
"configmaps [case13-e2e-objectname-var1] found as specified in namespace %[1]s; "+
"configmaps [case13-e2e-objectname-var2] found as specified in namespace %[1]s; "+
"configmaps [case13-e2e-objectname-var2] found as specified in namespace %[1]s; "+
"configmaps [case13-e2e-objectname-var3] found as specified in namespace %[1]s", e2eBaseName)))

relatedObjects, _, _ := unstructured.NestedSlice(managedPlc.Object, "status", "relatedObjects")
g.Expect(relatedObjects).To(HaveLen(3))
g.Expect(relatedObjects).To(HaveLen(2))

for idx := range relatedObjects {
relatedObject, ok := relatedObjects[idx].(map[string]interface{})
g.Expect(ok).To(BeTrue(), "Related object is not a map")
relatedObject1NS, _, _ := unstructured.NestedString(relatedObject, "object", "metadata", "name")
// The first object is skipped.
g.Expect(relatedObject1NS).To(
Equal(fmt.Sprintf("%s%d", e2eBaseName, idx+1)),
Equal(fmt.Sprintf("%s%d", e2eBaseName, idx+2)),
"Related object name should match")
}
}, defaultTimeoutSeconds, 1).Should(Succeed())
Expand All @@ -624,6 +626,10 @@ var _ = Describe("Test templatization", Ordered, func() {
Expect(err).ToNot(HaveOccurred())

for _, cm := range configMaps.Items {
if cm.Name == "case13-e2e-objectname-var1" {
continue
}

Expect(cm.ObjectMeta.Labels).To(HaveKeyWithValue("case13", "passed"))
Expect(cm.ObjectMeta.Labels).To(HaveKeyWithValue("name", cm.GetName()))
Expect(cm.ObjectMeta.Labels).To(HaveKeyWithValue("namespace", cm.GetNamespace()))
Expand Down Expand Up @@ -674,10 +680,33 @@ var _ = Describe("Test templatization", Ordered, func() {
}, defaultTimeoutSeconds, 1).Should(Succeed())
})

It("Should be compliant when all objects are skipped with skipObject", func(ctx SpecContext) {
By("Applying the " + allSkippedPolicyName + " ConfigurationPolicy")
utils.Kubectl("apply", "-n", testNamespace, "-f", allSkippedPolicyYAML)

By("By verifying that the ConfigurationPolicy is compliant")
Eventually(func(g Gomega) {
managedPlc := utils.GetWithTimeout(
clientManagedDynamic,
gvrConfigPolicy,
allSkippedPolicyName,
testNamespace,
true,
defaultTimeoutSeconds,
)

utils.CheckComplianceStatus(g, managedPlc, "Compliant")
g.Expect(utils.GetStatusMessage(managedPlc)).To(Equal(
"All objects of kind ConfigMap were skipped by the `skipObject` template function",
))
}, defaultTimeoutSeconds, 1).Should(Succeed())
})

AfterEach(func() {
utils.KubectlDelete("configurationpolicy", policyName, "-n", testNamespace)
utils.KubectlDelete("configurationpolicy", invalidPolicyName, "-n", testNamespace)
utils.KubectlDelete("configurationpolicy", emptyPolicyName, "-n", testNamespace)
utils.KubectlDelete("configurationpolicy", allSkippedPolicyName, "-n", testNamespace)
utils.KubectlDelete("configmaps", "-n", e2eBaseName, "--all")
})

Expand Down
5 changes: 2 additions & 3 deletions test/e2e/case42_resource_selection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ var _ = Describe("Test results of resource selection", Ordered, func() {
policyName = "case42-selector-results-e2e"

filterErrMsgFmt = "Error parsing provided objectSelector in the object-template at index [0]: %s"
noMatchesMsg = "object of kind FakeAPI has no name specified from " +
"the policy objectSelector nor the object metadata"
noMatchesMsg = "No objects of kind FakeAPI were matched from the policy objectSelector"
)

// Test setup for resource selection policy tests:
Expand Down Expand Up @@ -61,7 +60,7 @@ var _ = Describe("Test results of resource selection", Ordered, func() {
})

DescribeTable("ObjectSelector matching all is specified", func(patch string) {
By("Verifying policy is noncompliant and returns no objects")
By("Verifying policy is compliant and returns no objects")
utils.Kubectl("patch", "--namespace=managed", "configurationpolicy", policyName, "--type=json",
fmt.Sprintf(objectSelectorPatchFmt, `{"matchLabels":{"selects":"nothing"}}`),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ spec:
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{ .ObjectName }}"
name: '{{ if eq .ObjectName "case13-e2e-objectname-var1" }}{{ skipObject }}{{ else }}{{ .ObjectName }}{{ end }}'
namespace: "{{ .ObjectNamespace }}"
labels:
case13: passed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: policy.open-cluster-management.io/v1
kind: ConfigurationPolicy
metadata:
name: case13-objectname-var-all-skipped
spec:
remediationAction: enforce
namespaceSelector:
include:
- case13-e2e-objectname-var
object-templates:
- complianceType: musthave
objectSelector:
matchExpressions:
- key: case13
operator: Exists
objectDefinition:
apiVersion: v1
kind: ConfigMap
metadata:
name: '{{ if true }}{{ skipObject }}{{ end }}'
namespace: "{{ .ObjectNamespace }}"
labels:
case13: passed
name: "{{ .ObjectName }}"
namespace: "{{ .ObjectNamespace }}"

0 comments on commit e931ef2

Please sign in to comment.