From d3e1d0ac6468fb6c7af1bafb4cb73ef28da7d0e7 Mon Sep 17 00:00:00 2001 From: Will Kutler Date: Tue, 17 Jan 2023 14:24:27 -0500 Subject: [PATCH] Allow multiline templatization Adds a new field, object-templates-raw, that allows users to paste in an object template. This makes multiline templates, like those using the range keyword, possible. It can be set instead of object-templates, but both cannot be set. ref: https://issues.redhat.com/browse/ACM-2739?filter=-1 Signed-off-by: Will Kutler --- Makefile | 4 + api/v1/configurationpolicy_types.go | 15 +- controllers/configurationpolicy_controller.go | 59 +++- deploy/crds/kustomize/kustomization.yaml | 11 + .../kustomize/obj-template-validation.json | 11 + ...r-management.io_configurationpolicies.yaml | 320 ++++++++++++++++++ ...r-management.io_configurationpolicies.yaml | 19 ++ go.mod | 4 +- go.sum | 4 +- .../case28_multiline_templatization_test.go | 134 ++++++++ .../case28_configmaps.yaml | 13 + .../case28_policy.yaml | 19 ++ .../case28_unterminated.yaml | 22 ++ .../case28_wrong_args.yaml | 22 ++ 14 files changed, 637 insertions(+), 20 deletions(-) create mode 100644 deploy/crds/kustomize/kustomization.yaml create mode 100644 deploy/crds/kustomize/obj-template-validation.json create mode 100644 deploy/crds/kustomize/policy.open-cluster-management.io_configurationpolicies.yaml create mode 100644 test/e2e/case28_multiline_templatization_test.go create mode 100644 test/resources/case28_multiline_templatization/case28_configmaps.yaml create mode 100644 test/resources/case28_multiline_templatization/case28_policy.yaml create mode 100644 test/resources/case28_multiline_templatization/case28_unterminated.yaml create mode 100644 test/resources/case28_multiline_templatization/case28_wrong_args.yaml diff --git a/Makefile b/Makefile index ccdfe077..985ef664 100644 --- a/Makefile +++ b/Makefile @@ -208,6 +208,10 @@ CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false" .PHONY: manifests manifests: controller-gen $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=config-policy-controller paths="./..." output:crd:artifacts:config=deploy/crds output:rbac:artifacts:config=deploy/rbac + mv deploy/crds/policy.open-cluster-management.io_configurationpolicies.yaml deploy/crds/kustomize/policy.open-cluster-management.io_configurationpolicies.yaml + # Add a newline so that the format matches what kubebuilder generates + @printf "\n---\n" > deploy/crds/policy.open-cluster-management.io_configurationpolicies.yaml + $(KUSTOMIZE) build deploy/crds/kustomize >> deploy/crds/policy.open-cluster-management.io_configurationpolicies.yaml .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. diff --git a/api/v1/configurationpolicy_types.go b/api/v1/configurationpolicy_types.go index 9add6ad0..d285bc03 100644 --- a/api/v1/configurationpolicy_types.go +++ b/api/v1/configurationpolicy_types.go @@ -157,8 +157,19 @@ type ConfigurationPolicySpec struct { // 'matchLabels' and/or 'matchExpressions' are, 'include' will behave as if ['*'] were given. If // 'matchExpressions' and 'matchLabels' are both not provided, 'include' must be provided to // retrieve namespaces. - NamespaceSelector Target `json:"namespaceSelector,omitempty"` - ObjectTemplates []*ObjectTemplate `json:"object-templates,omitempty"` + NamespaceSelector Target `json:"namespaceSelector,omitempty"` + // 'object-templates' and 'object-templates-raw' are arrays of objects for the configuration + // policy to check, create, modify, or delete on the cluster. 'object-templates' is an array + // of objects, while 'object-templates-raw' is a string containing an array of objects in + // YAML format. Only one of the two object-templates variables can be set in a given + // configurationPolicy. + ObjectTemplates []*ObjectTemplate `json:"object-templates,omitempty"` + // 'object-templates' and 'object-templates-raw' are arrays of objects for the configuration + // policy to check, create, modify, or delete on the cluster. 'object-templates' is an array + // of objects, while 'object-templates-raw' is a string containing an array of objects in + // YAML format. Only one of the two object-templates variables can be set in a given + // configurationPolicy. + ObjectTemplatesRaw string `json:"object-templates-raw,omitempty"` EvaluationInterval EvaluationInterval `json:"evaluationInterval,omitempty"` // +kubebuilder:default:=None PruneObjectBehavior PruneObjectBehavior `json:"pruneObjectBehavior,omitempty"` diff --git a/controllers/configurationpolicy_controller.go b/controllers/configurationpolicy_controller.go index 83d060ef..23c7565e 100644 --- a/controllers/configurationpolicy_controller.go +++ b/controllers/configurationpolicy_controller.go @@ -773,6 +773,22 @@ func (r *ConfigurationPolicyReconciler) handleObjectTemplates(plc policyv1.Confi } } + // set up raw data for template processing + var rawDataList [][]byte + var isRawObjTemplate bool + + if plc.Spec.ObjectTemplatesRaw != "" { + rawDataList = [][]byte{[]byte(plc.Spec.ObjectTemplatesRaw)} + isRawObjTemplate = true + } else { + for _, objectT := range plc.Spec.ObjectTemplates { + rawDataList = append(rawDataList, objectT.ObjectDefinition.Raw) + } + isRawObjTemplate = false + } + + tmplResolverCfg.InputIsYAML = isRawObjTemplate + tmplResolver, err := templates.NewResolver(&r.TargetK8sClient, r.TargetK8sConfig, tmplResolverCfg) if err != nil { // If the encryption key is invalid, clear the cache. @@ -791,10 +807,13 @@ func (r *ConfigurationPolicyReconciler) handleObjectTemplates(plc policyv1.Confi if !disableTemplates { startTime := time.Now().UTC() - for _, objectT := range plc.Spec.ObjectTemplates { + var objTemps []*policyv1.ObjectTemplate + + // process object templates for go template usage + for i, rawData := range rawDataList { // first check to make sure there are no hub-templates with delimiter - {{hub // if one exists, it means the template resolution on the hub did not succeed. - if templates.HasTemplate(objectT.ObjectDefinition.Raw, "{{hub", false) { + if templates.HasTemplate(rawData, "{{hub", false) { // check to see there is an annotation set to the hub error msg, // if not ,set a generic msg hubTemplatesErrMsg, ok := annotations["policy.open-cluster-management.io/hub-templates-error"] @@ -814,10 +833,10 @@ func (r *ConfigurationPolicyReconciler) handleObjectTemplates(plc policyv1.Confi return } - if templates.HasTemplate(objectT.ObjectDefinition.Raw, "", true) { + if templates.HasTemplate(rawData, "", true) { log.V(1).Info("Processing policy templates") - resolvedTemplate, tplErr := tmplResolver.ResolveTemplate(objectT.ObjectDefinition.Raw, nil) + resolvedTemplate, tplErr := tmplResolver.ResolveTemplate(rawData, nil) if errors.Is(tplErr, templates.ErrMissingAPIResource) || errors.Is(tplErr, templates.ErrMissingAPIResourceInvalidTemplate) { @@ -829,7 +848,7 @@ func (r *ConfigurationPolicyReconciler) handleObjectTemplates(plc policyv1.Confi discoveryErr := r.refreshDiscoveryInfo() if discoveryErr == nil { tmplResolver.SetKubeAPIResourceList(r.apiResourceList) - resolvedTemplate, tplErr = tmplResolver.ResolveTemplate(objectT.ObjectDefinition.Raw, nil) + resolvedTemplate, tplErr = tmplResolver.ResolveTemplate(rawData, nil) } else { log.V(2).Info( "Failed to refresh the API discovery information after a template encountered an unknown " + @@ -871,7 +890,7 @@ func (r *ConfigurationPolicyReconciler) handleObjectTemplates(plc policyv1.Confi return } - resolvedTemplate, tplErr = tmplResolver.ResolveTemplate(objectT.ObjectDefinition.Raw, nil) + resolvedTemplate, tplErr = tmplResolver.ResolveTemplate(rawData, nil) } if tplErr != nil { @@ -880,8 +899,22 @@ func (r *ConfigurationPolicyReconciler) handleObjectTemplates(plc policyv1.Confi return } - // Set the resolved data for use in further processing - objectT.ObjectDefinition.Raw = resolvedTemplate.ResolvedJSON + // If raw data, only one passthrough is needed, since all the object templates are in it + if isRawObjTemplate { + err := json.Unmarshal(resolvedTemplate.ResolvedJSON, &objTemps) + if err != nil { + addTemplateErrorViolation("Error unmarshalling raw template", err.Error()) + + return + } + + plc.Spec.ObjectTemplates = objTemps + + break + } + + // Otherwise, set the resolved data for use in further processing + plc.Spec.ObjectTemplates[i].ObjectDefinition.Raw = resolvedTemplate.ResolvedJSON } } @@ -2503,13 +2536,11 @@ func (r *ConfigurationPolicyReconciler) addForUpdate(policy *policyv1.Configurat if policy.Spec == nil { compliant = false } else { - for index := range policy.Spec.ObjectTemplates { - if index < len(policy.Status.CompliancyDetails) { - if policy.Status.CompliancyDetails[index].ComplianceState == policyv1.NonCompliant { - compliant = false + for index := range policy.Status.CompliancyDetails { + if policy.Status.CompliancyDetails[index].ComplianceState == policyv1.NonCompliant { + compliant = false - break - } + break } } } diff --git a/deploy/crds/kustomize/kustomization.yaml b/deploy/crds/kustomize/kustomization.yaml new file mode 100644 index 00000000..e7b590bb --- /dev/null +++ b/deploy/crds/kustomize/kustomization.yaml @@ -0,0 +1,11 @@ +resources: +- policy.open-cluster-management.io_configurationpolicies.yaml + +# Add validation more complicated than Kubebuilder markers can provide +patches: +- path: obj-template-validation.json + target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: configurationpolicies.policy.open-cluster-management.io diff --git a/deploy/crds/kustomize/obj-template-validation.json b/deploy/crds/kustomize/obj-template-validation.json new file mode 100644 index 00000000..57145f1b --- /dev/null +++ b/deploy/crds/kustomize/obj-template-validation.json @@ -0,0 +1,11 @@ +[ + { + "op":"add", + "path":"/spec/versions/0/schema/openAPIV3Schema/properties/spec/oneOf", + "value": [{ + "required": ["object-templates"] + },{ + "required": ["object-templates-raw"] + }] + } +] diff --git a/deploy/crds/kustomize/policy.open-cluster-management.io_configurationpolicies.yaml b/deploy/crds/kustomize/policy.open-cluster-management.io_configurationpolicies.yaml new file mode 100644 index 00000000..9d70f1d2 --- /dev/null +++ b/deploy/crds/kustomize/policy.open-cluster-management.io_configurationpolicies.yaml @@ -0,0 +1,320 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: configurationpolicies.policy.open-cluster-management.io +spec: + group: policy.open-cluster-management.io + names: + kind: ConfigurationPolicy + listKind: ConfigurationPolicyList + plural: configurationpolicies + singular: configurationpolicy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.compliant + name: Compliance state + type: string + name: v1 + schema: + openAPIV3Schema: + description: ConfigurationPolicy is the Schema for the configurationpolicies + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ConfigurationPolicySpec defines the desired state of ConfigurationPolicy + properties: + evaluationInterval: + description: Configures the minimum elapsed time before a ConfigurationPolicy + is reevaluated + properties: + compliant: + description: The minimum elapsed time before a ConfigurationPolicy + is reevaluated when in the compliant state. Set this to "never" + to disable reevaluation when in the compliant state. + pattern: ^(?:(?:(?:[0-9]+(?:.[0-9])?)(?:h|m|s|(?:ms)|(?:us)|(?:ns)))|never)+$ + type: string + noncompliant: + description: The minimum elapsed time before a ConfigurationPolicy + is reevaluated when in the noncompliant state. Set this to "never" + to disable reevaluation when in the noncompliant state. + pattern: ^(?:(?:(?:[0-9]+(?:.[0-9])?)(?:h|m|s|(?:ms)|(?:us)|(?:ns)))|never)+$ + type: string + type: object + namespaceSelector: + description: '''namespaceSelector'' defines the list of namespaces + to include/exclude for objects defined in spec.objectTemplates. + All selector rules are ANDed. If ''include'' is not provided but + ''matchLabels'' and/or ''matchExpressions'' are, ''include'' will + behave as if [''*''] were given. If ''matchExpressions'' and ''matchLabels'' + are both not provided, ''include'' must be provided to retrieve + namespaces.' + properties: + exclude: + description: '''exclude'' is an array of filepath expressions + to exclude objects by name.' + items: + minLength: 1 + type: string + type: array + include: + description: '''include'' is an array of filepath expressions + to include objects by name.' + items: + minLength: 1 + type: string + type: array + matchExpressions: + description: '''matchExpressions'' is an array of label selector + requirements matching objects by label.' + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: '''matchLabels'' is a map of {key,value} pairs matching + objects by label.' + type: object + type: object + object-templates: + description: '''object-templates'' and ''object-templates-raw'' are + arrays of objects for the configuration policy to check, create, + modify, or delete on the cluster. ''object-templates'' is an array + of objects, while ''object-templates-raw'' is a string containing + an array of objects in YAML format. Only one of the two object-templates + variables can be set in a given configurationPolicy.' + items: + description: ObjectTemplate describes how an object should look + properties: + complianceType: + description: 'ComplianceType specifies whether it is: musthave, + mustnothave, mustonlyhave' + enum: + - MustHave + - Musthave + - musthave + - MustOnlyHave + - Mustonlyhave + - mustonlyhave + - MustNotHave + - Mustnothave + - mustnothave + type: string + metadataComplianceType: + description: MetadataComplianceType describes how to check compliance + for the labels/annotations of a given object + enum: + - MustHave + - Musthave + - musthave + - MustOnlyHave + - Mustonlyhave + - mustonlyhave + type: string + objectDefinition: + description: ObjectDefinition defines required fields for the + object + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - complianceType + type: object + type: array + object-templates-raw: + description: '''object-templates'' and ''object-templates-raw'' are + arrays of objects for the configuration policy to check, create, + modify, or delete on the cluster. ''object-templates'' is an array + of objects, while ''object-templates-raw'' is a string containing + an array of objects in YAML format. Only one of the two object-templates + variables can be set in a given configurationPolicy.' + type: string + pruneObjectBehavior: + default: None + description: PruneObjectBehavior is used to remove objects that are + managed by the policy upon policy deletion. + enum: + - DeleteAll + - DeleteIfCreated + - None + type: string + remediationAction: + description: 'RemediationAction : enforce or inform' + enum: + - Inform + - inform + - Enforce + - enforce + type: string + severity: + description: 'Severity : low, medium, high, or critical' + enum: + - low + - Low + - medium + - Medium + - high + - High + - critical + - Critical + type: string + type: object + status: + description: ConfigurationPolicyStatus defines the observed state of ConfigurationPolicy + properties: + compliancyDetails: + items: + description: TemplateStatus hold the status result + properties: + Compliant: + description: ComplianceState shows the state of enforcement + type: string + Validity: + description: Validity describes if it is valid or not + properties: + reason: + type: string + valid: + type: boolean + type: object + conditions: + items: + description: Condition is the base struct for representing + resource conditions + properties: + lastTransitionTime: + description: The last time the condition transitioned + from one status to another. + format: date-time + type: string + message: + description: A human readable message indicating details + about the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, + Unknown. + type: string + type: + description: Type of condition, e.g Complete or Failed. + type: string + required: + - type + type: object + type: array + type: object + type: array + compliant: + description: ComplianceState shows the state of enforcement + type: string + lastEvaluated: + description: An ISO-8601 timestamp of the last time the policy was + evaluated + type: string + lastEvaluatedGeneration: + description: The generation of the ConfigurationPolicy object when + it was last evaluated + format: int64 + type: integer + relatedObjects: + description: List of resources processed by the policy + items: + description: RelatedObject is the list of objects matched by this + Policy resource. + properties: + compliant: + type: string + object: + description: ObjectResource is an object identified by the policy + as a resource that needs to be validated. + properties: + apiVersion: + description: API version of the referent. + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: Metadata values from the referent. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + type: object + type: object + properties: + properties: + createdByPolicy: + description: Whether the object was created by the parent + policy + type: boolean + uid: + description: Store object UID to help track object ownership + for deletion + type: string + type: object + reason: + type: string + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/deploy/crds/policy.open-cluster-management.io_configurationpolicies.yaml b/deploy/crds/policy.open-cluster-management.io_configurationpolicies.yaml index b4f38e2e..3fe794b7 100644 --- a/deploy/crds/policy.open-cluster-management.io_configurationpolicies.yaml +++ b/deploy/crds/policy.open-cluster-management.io_configurationpolicies.yaml @@ -40,6 +40,11 @@ spec: type: object spec: description: ConfigurationPolicySpec defines the desired state of ConfigurationPolicy + oneOf: + - required: + - object-templates + - required: + - object-templates-raw properties: evaluationInterval: description: Configures the minimum elapsed time before a ConfigurationPolicy @@ -120,6 +125,12 @@ spec: type: object type: object object-templates: + description: '''object-templates'' and ''object-templates-raw'' are + arrays of objects for the configuration policy to check, create, + modify, or delete on the cluster. ''object-templates'' is an array + of objects, while ''object-templates-raw'' is a string containing + an array of objects in YAML format. Only one of the two object-templates + variables can be set in a given configurationPolicy.' items: description: ObjectTemplate describes how an object should look properties: @@ -157,6 +168,14 @@ spec: - complianceType type: object type: array + object-templates-raw: + description: '''object-templates'' and ''object-templates-raw'' are + arrays of objects for the configuration policy to check, create, + modify, or delete on the cluster. ''object-templates'' is an array + of objects, while ''object-templates-raw'' is a string containing + an array of objects in YAML format. Only one of the two object-templates + variables can be set in a given configurationPolicy.' + type: string pruneObjectBehavior: default: None description: PruneObjectBehavior is used to remove objects that are diff --git a/go.mod b/go.mod index f4a8ebe5..c8327eea 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,10 @@ require ( github.com/prometheus/client_golang v1.13.0 github.com/spf13/pflag v1.0.5 github.com/stolostron/go-log-utils v0.1.1 - github.com/stolostron/go-template-utils/v3 v3.0.1 + github.com/stolostron/go-template-utils/v3 v3.0.2 github.com/stretchr/testify v1.7.0 k8s.io/api v0.23.9 + k8s.io/apiextensions-apiserver v0.23.5 k8s.io/apimachinery v0.23.9 k8s.io/client-go v12.0.0+incompatible k8s.io/klog v1.0.0 @@ -89,7 +90,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.23.5 // indirect k8s.io/component-base v0.23.9 // indirect k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect open-cluster-management.io/api v0.6.1-0.20220208144021-3297cac74dc5 // indirect diff --git a/go.sum b/go.sum index 6557b05b..b2013842 100644 --- a/go.sum +++ b/go.sum @@ -515,8 +515,8 @@ github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stolostron/go-log-utils v0.1.1 h1:T48GyfuGpn2+6817FQVec1CyxGf0ibuVhoBiz9SeCec= github.com/stolostron/go-log-utils v0.1.1/go.mod h1:2Uc5mbuLvSFpoXFFEKRTEFOlR7nqGVMu9mbU+FIttTI= -github.com/stolostron/go-template-utils/v3 v3.0.1 h1:E+BcQZubHmHWepXjqcSXUNPmzXoWzCLF3pO26w4NNVQ= -github.com/stolostron/go-template-utils/v3 v3.0.1/go.mod h1:k9qoQ/OH/jOQI5ovfC11QJ6ApWsa3E5rmbkfUAEUF3g= +github.com/stolostron/go-template-utils/v3 v3.0.2 h1:0wUbfcvG5pYoEa0p4qj3oBpz9k6WPyE5S5IaYIBWbbg= +github.com/stolostron/go-template-utils/v3 v3.0.2/go.mod h1:k9qoQ/OH/jOQI5ovfC11QJ6ApWsa3E5rmbkfUAEUF3g= github.com/stolostron/kubernetes-dependency-watches v0.0.0-20221007134235-7551d84cf688 h1:Q/0MspxKFkqBN59keMsJI2hGqGGpTSxdNEvrxTOBlRg= github.com/stolostron/kubernetes-dependency-watches v0.0.0-20221007134235-7551d84cf688/go.mod h1:jGnjY4g8N1PaBqt35EjqNRjymjG0DWqki/2JOLMN1Pg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/test/e2e/case28_multiline_templatization_test.go b/test/e2e/case28_multiline_templatization_test.go new file mode 100644 index 00000000..d9a5d717 --- /dev/null +++ b/test/e2e/case28_multiline_templatization_test.go @@ -0,0 +1,134 @@ +// Copyright (c) 2020 Red Hat, Inc. +// Copyright Contributors to the Open Cluster Management project + +package e2e + +import ( + "context" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "open-cluster-management.io/config-policy-controller/test/utils" +) + +const ( + case28RangePolicyName string = "case28-configpolicy" + case28RangePolicyYaml string = "../resources/case28_multiline_templatization/case28_policy.yaml" + case28ConfigMapsYaml string = "../resources/case28_multiline_templatization/case28_configmaps.yaml" +) + +const ( + case28Unterminated string = "policy-pod-create-unterminated" + case28UnterminatedYaml string = "../resources/case28_multiline_templatization/case28_unterminated.yaml" + case28WrongArgs string = "policy-pod-create-wrong-args" + case28WrongArgsYaml string = "../resources/case28_multiline_templatization/case28_wrong_args.yaml" +) + +var _ = Describe("Test multiline templatization", Ordered, func() { + Describe("Verify multiline template with range keyword", Ordered, func() { + It("configmap should be created properly on the managed cluster", func() { + By("Creating config maps on managed") + utils.Kubectl("apply", "-f", case28ConfigMapsYaml, "-n", "default") + for _, cfgMapName := range []string{"28config1", "28config2"} { + cfgmap := utils.GetWithTimeout(clientManagedDynamic, gvrConfigMap, + cfgMapName, "default", true, defaultTimeoutSeconds) + Expect(cfgmap).NotTo(BeNil()) + } + }) + It("both configmaps should be updated properly on the managed cluster", func() { + By("Creating policy with range template on managed") + utils.Kubectl("apply", "-f", case28RangePolicyYaml, "-n", testNamespace) + plc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case28RangePolicyName, testNamespace, true, defaultTimeoutSeconds) + Expect(plc).NotTo(BeNil()) + + By("Verifying that the " + case28RangePolicyName + " policy is compliant") + Eventually(func() interface{} { + managedPlc := utils.GetWithTimeout( + clientManagedDynamic, + gvrConfigPolicy, + case28RangePolicyName, + testNamespace, + true, + defaultTimeoutSeconds, + ) + + return utils.GetComplianceState(managedPlc) + }, defaultTimeoutSeconds, 1).Should(Equal("Compliant")) + + By("Verifying that both configmaps have the updated data") + for _, cfgMapName := range []string{"28config1", "28config2"} { + Eventually( + func() interface{} { + configMap, err := clientManaged.CoreV1().ConfigMaps("default").Get( + context.TODO(), cfgMapName, v1.GetOptions{}, + ) + if err != nil { + return "" + } + + return configMap.Data["extraData"] + }, + defaultTimeoutSeconds, + 1, + ).Should(Equal("exists!")) + } + }) + + AfterAll(func() { + deleteConfigPolicies([]string{case28RangePolicyName}) + utils.Kubectl("delete", "-f", case28ConfigMapsYaml) + }) + }) + Describe("Test invalid multiline templates", func() { + It("should generate noncompliant for invalid template strings", func() { + By("Creating policies on managed") + // create policy with unterminated template + utils.Kubectl("apply", "-f", case28UnterminatedYaml, "-n", testNamespace) + plc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case28Unterminated, testNamespace, true, defaultTimeoutSeconds) + Expect(plc).NotTo(BeNil()) + Eventually(func() interface{} { + managedPlc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case28Unterminated, testNamespace, true, defaultTimeoutSeconds) + + return utils.GetComplianceState(managedPlc) + }, defaultTimeoutSeconds, 1).Should(Equal("NonCompliant")) + Eventually(func() interface{} { + managedPlc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case28Unterminated, testNamespace, true, defaultTimeoutSeconds) + + return strings.Contains( + utils.GetStatusMessage(managedPlc).(string), + "unterminated character constant", + ) + }, 10, 1).Should(BeTrue()) + // create policy with incomplete args in template + utils.Kubectl("apply", "-f", case28WrongArgsYaml, "-n", testNamespace) + plc = utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case28WrongArgs, testNamespace, true, defaultTimeoutSeconds) + Expect(plc).NotTo(BeNil()) + Eventually(func() interface{} { + managedPlc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case28WrongArgs, testNamespace, true, defaultTimeoutSeconds) + + return utils.GetComplianceState(managedPlc) + }, defaultTimeoutSeconds, 1).Should(Equal("NonCompliant")) + Eventually(func() interface{} { + managedPlc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case28WrongArgs, testNamespace, true, defaultTimeoutSeconds) + + return strings.Contains( + utils.GetStatusMessage(managedPlc).(string), + "wrong number of args for lookup: want 4 got 1", + ) + }, 10, 1).Should(BeTrue()) + }) + AfterAll(func() { + deleteConfigPolicies([]string{case28Unterminated, case28WrongArgs}) + }) + }) +}) diff --git a/test/resources/case28_multiline_templatization/case28_configmaps.yaml b/test/resources/case28_multiline_templatization/case28_configmaps.yaml new file mode 100644 index 00000000..87ebef25 --- /dev/null +++ b/test/resources/case28_multiline_templatization/case28_configmaps.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: 28config1 +data: + name: testvalue1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: 28config2 +data: + name: testvalue2 \ No newline at end of file diff --git a/test/resources/case28_multiline_templatization/case28_policy.yaml b/test/resources/case28_multiline_templatization/case28_policy.yaml new file mode 100644 index 00000000..412e2197 --- /dev/null +++ b/test/resources/case28_multiline_templatization/case28_policy.yaml @@ -0,0 +1,19 @@ +apiVersion: policy.open-cluster-management.io/v1 +kind: ConfigurationPolicy +metadata: + name: case28-configpolicy +spec: + remediationAction: enforce + severity: low + object-templates-raw: | + {{ range (lookup "v1" "ConfigMap" "default" "").items }} + - complianceType: musthave + objectDefinition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ .metadata.name }} + namespace: default + data: + extraData: exists! + {{ end }} diff --git a/test/resources/case28_multiline_templatization/case28_unterminated.yaml b/test/resources/case28_multiline_templatization/case28_unterminated.yaml new file mode 100644 index 00000000..113cffe4 --- /dev/null +++ b/test/resources/case28_multiline_templatization/case28_unterminated.yaml @@ -0,0 +1,22 @@ +apiVersion: policy.open-cluster-management.io/v1 +kind: ConfigurationPolicy +metadata: + name: policy-pod-create-unterminated +spec: + remediationAction: enforce + namespaceSelector: + exclude: ["kube-*"] + include: ["default"] + object-templates-raw: | + - complianceType: musthave + objectDefinition: + apiVersion: v1 + kind: Pod + metadata: + name: '{{' + spec: + containers: + - image: nginx:1.7.9 + name: nginx + ports: + - containerPort: 80 diff --git a/test/resources/case28_multiline_templatization/case28_wrong_args.yaml b/test/resources/case28_multiline_templatization/case28_wrong_args.yaml new file mode 100644 index 00000000..2b4d312f --- /dev/null +++ b/test/resources/case28_multiline_templatization/case28_wrong_args.yaml @@ -0,0 +1,22 @@ +apiVersion: policy.open-cluster-management.io/v1 +kind: ConfigurationPolicy +metadata: + name: policy-pod-create-wrong-args +spec: + remediationAction: enforce + namespaceSelector: + exclude: ["kube-*"] + include: ["default"] + object-templates-raw: | + - complianceType: musthave + objectDefinition: + apiVersion: v1 + kind: Pod + metadata: + name: '{{ (lookup "cluster.open-cluster-management.io/v1alpha1") }}' + spec: + containers: + - image: nginx:1.7.9 + name: nginx + ports: + - containerPort: 80