From ac5eb0762f5eea1596c7ddac82431960a9a3f964 Mon Sep 17 00:00:00 2001 From: Justin Kulikauskas Date: Fri, 7 Oct 2022 14:36:52 -0400 Subject: [PATCH 1/7] Refactor: re-organize some loops Signed-off-by: Justin Kulikauskas --- internal/utils.go | 62 +++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/internal/utils.go b/internal/utils.go index 84da7b6c..e30fd687 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -45,6 +45,17 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e return nil, readErr } + // Handle when a Kustomization directory is specified + for _, f := range files { + _, filename := path.Split(f.Name()) + if filename == "kustomization.yml" || filename == "kustomization.yaml" { + hasKustomize[manifest.Path] = true + resolvedFiles = []string{manifest.Path} + + goto resolutioncomplete // TODO: remove goto after refactoring + } + } + for _, f := range files { if f.IsDir() { continue @@ -56,20 +67,34 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e if ext != ".yaml" && ext != ".yml" { continue } - // Handle when a Kustomization directory is specified - _, filename := path.Split(filepath) - if filename == "kustomization.yml" || filename == "kustomization.yaml" { - hasKustomize[manifest.Path] = true - resolvedFiles = []string{manifest.Path} - - break - } yamlPath := path.Join(manifest.Path, f.Name()) resolvedFiles = append(resolvedFiles, yamlPath) } + resolutioncomplete: manifestPaths = append(manifestPaths, resolvedFiles...) + + for _, manifestPath := range manifestPaths { + var manifestFile []map[string]interface{} + var err error + + if hasKustomize[manifestPath] { + manifestFile, err = processKustomizeDir(manifestPath) + } else { + manifestFile, err = unmarshalManifestFile(manifestPath) + } + + if err != nil { + return nil, err + } + + if len(manifestFile) == 0 { + continue + } + + manifestFiles = append(manifestFiles, manifestFile...) + } } else { // Unmarshal the manifest in order to check for metadata patch replacement manifestFile, err := unmarshalManifestFile(manifest.Path) @@ -101,27 +126,6 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e manifestFiles = append(manifestFiles, manifestFile...) } - for _, manifestPath := range manifestPaths { - var manifestFile []map[string]interface{} - var err error - - if hasKustomize[manifestPath] { - manifestFile, err = processKustomizeDir(manifestPath) - } else { - manifestFile, err = unmarshalManifestFile(manifestPath) - } - - if err != nil { - return nil, err - } - - if len(manifestFile) == 0 { - continue - } - - manifestFiles = append(manifestFiles, manifestFile...) - } - if len(manifest.Patches) > 0 { patcher := manifestPatcher{manifests: manifestFiles, patches: manifest.Patches} const errTemplate = `failed to process the manifest at "%s": %w` From a4a2f09bc9db0700cc801aad7a81f3643a6bc476 Mon Sep 17 00:00:00 2001 From: Justin Kulikauskas Date: Fri, 7 Oct 2022 14:45:16 -0400 Subject: [PATCH 2/7] Refactor: remove goto Signed-off-by: Justin Kulikauskas --- internal/utils.go | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/internal/utils.go b/internal/utils.go index e30fd687..16680699 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -25,7 +25,6 @@ import ( // be read. func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, error) { manifests := [][]map[string]interface{}{} - hasKustomize := map[string]bool{} for _, manifest := range policyConf.Manifests { manifestPaths := []string{} @@ -37,8 +36,6 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e return nil, readErr } - resolvedFiles := []string{} - if manifestPathInfo.IsDir() { files, err := ioutil.ReadDir(manifest.Path) if err != nil { @@ -46,40 +43,40 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e } // Handle when a Kustomization directory is specified + hasKustomize := false for _, f := range files { _, filename := path.Split(f.Name()) if filename == "kustomization.yml" || filename == "kustomization.yaml" { - hasKustomize[manifest.Path] = true - resolvedFiles = []string{manifest.Path} + hasKustomize = true + manifestPaths = []string{manifest.Path} - goto resolutioncomplete // TODO: remove goto after refactoring + break } } - for _, f := range files { - if f.IsDir() { - continue - } + if !hasKustomize { + for _, f := range files { + if f.IsDir() { + continue + } - filepath := f.Name() - ext := path.Ext(filepath) + filepath := f.Name() + ext := path.Ext(filepath) - if ext != ".yaml" && ext != ".yml" { - continue - } + if ext != ".yaml" && ext != ".yml" { + continue + } - yamlPath := path.Join(manifest.Path, f.Name()) - resolvedFiles = append(resolvedFiles, yamlPath) + yamlPath := path.Join(manifest.Path, f.Name()) + manifestPaths = append(manifestPaths, yamlPath) + } } - resolutioncomplete: - manifestPaths = append(manifestPaths, resolvedFiles...) - for _, manifestPath := range manifestPaths { var manifestFile []map[string]interface{} var err error - if hasKustomize[manifestPath] { + if hasKustomize { manifestFile, err = processKustomizeDir(manifestPath) } else { manifestFile, err = unmarshalManifestFile(manifestPath) From 534e11d08a89b34b0e1d237ba9f3ce6b412310cf Mon Sep 17 00:00:00 2001 From: Justin Kulikauskas Date: Fri, 7 Oct 2022 14:55:51 -0400 Subject: [PATCH 3/7] Refactor: remove an extra loop Signed-off-by: Justin Kulikauskas --- internal/utils.go | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/internal/utils.go b/internal/utils.go index 16680699..d5061a91 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -27,7 +27,6 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e manifests := [][]map[string]interface{}{} for _, manifest := range policyConf.Manifests { - manifestPaths := []string{} manifestFiles := []map[string]interface{}{} readErr := fmt.Errorf("failed to read the manifest path %s", manifest.Path) @@ -48,7 +47,12 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e _, filename := path.Split(f.Name()) if filename == "kustomization.yml" || filename == "kustomization.yaml" { hasKustomize = true - manifestPaths = []string{manifest.Path} + manifestDocs, err := processKustomizeDir(manifest.Path) + if err != nil { + return nil, err + } + + manifestFiles = manifestDocs break } @@ -67,30 +71,13 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e continue } - yamlPath := path.Join(manifest.Path, f.Name()) - manifestPaths = append(manifestPaths, yamlPath) - } - } - - for _, manifestPath := range manifestPaths { - var manifestFile []map[string]interface{} - var err error - - if hasKustomize { - manifestFile, err = processKustomizeDir(manifestPath) - } else { - manifestFile, err = unmarshalManifestFile(manifestPath) - } - - if err != nil { - return nil, err - } + manifestDocs, err := unmarshalManifestFile(path.Join(manifest.Path, f.Name())) + if err != nil { + return nil, err + } - if len(manifestFile) == 0 { - continue + manifestFiles = append(manifestFiles, manifestDocs...) } - - manifestFiles = append(manifestFiles, manifestFile...) } } else { // Unmarshal the manifest in order to check for metadata patch replacement From 9688c83478fa85fc38132ece177360dc87a9def0 Mon Sep 17 00:00:00 2001 From: Justin Kulikauskas Date: Fri, 7 Oct 2022 14:58:28 -0400 Subject: [PATCH 4/7] Refactor: remove unnecessary intermediate vars Signed-off-by: Justin Kulikauskas --- internal/utils.go | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/internal/utils.go b/internal/utils.go index d5061a91..47d8c45e 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -47,13 +47,11 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e _, filename := path.Split(f.Name()) if filename == "kustomization.yml" || filename == "kustomization.yaml" { hasKustomize = true - manifestDocs, err := processKustomizeDir(manifest.Path) + manifestFiles, err = processKustomizeDir(manifest.Path) if err != nil { return nil, err } - manifestFiles = manifestDocs - break } } @@ -81,19 +79,16 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e } } else { // Unmarshal the manifest in order to check for metadata patch replacement - manifestFile, err := unmarshalManifestFile(manifest.Path) + manifestFiles, err = unmarshalManifestFile(manifest.Path) if err != nil { return nil, err } - if len(manifestFile) == 0 { - continue - } // Allowing replace the original manifest metadata.name and/or metadata.namespace if it is a single // yaml structure in the manifest path - if len(manifestFile) == 1 && len(manifest.Patches) == 1 { + if len(manifestFiles) == 1 && len(manifest.Patches) == 1 { if patchMetadata, ok := manifest.Patches[0]["metadata"].(map[string]interface{}); ok { - if metadata, ok := manifestFile[0]["metadata"].(map[string]interface{}); ok { + if metadata, ok := manifestFiles[0]["metadata"].(map[string]interface{}); ok { name, ok := patchMetadata["name"].(string) if ok && name != "" { metadata["name"] = name @@ -102,12 +97,10 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e if ok && namespace != "" { metadata["namespace"] = namespace } - manifestFile[0]["metadata"] = metadata + manifestFiles[0]["metadata"] = metadata } } } - - manifestFiles = append(manifestFiles, manifestFile...) } if len(manifest.Patches) > 0 { From 51d7b6cc086ccb9172be4ca52e627545a220c04b Mon Sep 17 00:00:00 2001 From: Justin Kulikauskas Date: Fri, 7 Oct 2022 15:08:15 -0400 Subject: [PATCH 5/7] Add support for using stdin as a manifest This will help enable usage of this plugin as a transformer. Signed-off-by: Justin Kulikauskas --- internal/utils.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/internal/utils.go b/internal/utils.go index 47d8c45e..84416be0 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -30,7 +30,19 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e manifestFiles := []map[string]interface{}{} readErr := fmt.Errorf("failed to read the manifest path %s", manifest.Path) - manifestPathInfo, err := os.Stat(manifest.Path) + var manifestFD *os.File + var err error + + if manifest.Path == "stdin" { + manifestFD = os.Stdin + } else { + manifestFD, err = os.Open(manifest.Path) + if err != nil { + return nil, readErr + } + } + + manifestPathInfo, err := manifestFD.Stat() if err != nil { return nil, readErr } @@ -79,7 +91,12 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e } } else { // Unmarshal the manifest in order to check for metadata patch replacement - manifestFiles, err = unmarshalManifestFile(manifest.Path) + manifestBytes, err := io.ReadAll(manifestFD) + if err != nil { + return nil, err + } + + manifestFiles, err = unmarshalManifestBytes(manifestBytes) if err != nil { return nil, err } From 3a75e9f4a50ab591638b79c13dcbcca6344677b4 Mon Sep 17 00:00:00 2001 From: Justin Kulikauskas Date: Fri, 7 Oct 2022 16:08:59 -0400 Subject: [PATCH 6/7] Enable plugin to be run as a transformer Signed-off-by: Justin Kulikauskas --- README.md | 10 +- examples/transformer/kustomization.yaml | 4 + examples/transformer/output.yaml | 105 ++++++++++++++++++++ examples/transformer/policyTransformer.yaml | 16 +++ internal/plugin.go | 20 ++-- internal/utils.go | 23 +++++ 6 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 examples/transformer/kustomization.yaml create mode 100644 examples/transformer/output.yaml create mode 100644 examples/transformer/policyTransformer.yaml diff --git a/README.md b/README.md index 4e343af7..116887a5 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ For more about Open Cluster Management and its Policy Framework: **NOTE:** This will default to placing the binary in `${HOME}/.config/kustomize/plugin/`. You can change this by exporting `KUSTOMIZE_PLUGIN_HOME` to a different path. -#### Configuration +#### Configuration and use as a Generator 1. Create a `kustomization.yaml` file that points to `PolicyGenerator` manifest(s), with any additional desired patches or customizations (see @@ -80,6 +80,14 @@ For more about Open Cluster Management and its Policy Framework: kustomize build --enable-alpha-plugins ``` +#### Configuration and use as a Transformer + +The plugin can also be used as a transformer, to wrap all incoming `resources` from a +`kustomization.yaml` file into one Policy. This feature is somewhat experimental. + +An example configuration as a transformer (and its output) can be found in the +[`examples/generator/`](./examples/generator/) folder. + ### As a standalone binary In order to bypass Kustomize and run the generator binary directly: diff --git a/examples/transformer/kustomization.yaml b/examples/transformer/kustomization.yaml new file mode 100644 index 00000000..0466cbe4 --- /dev/null +++ b/examples/transformer/kustomization.yaml @@ -0,0 +1,4 @@ +resources: +- github.com/redhat-cop/gitops-catalog/advanced-cluster-management/operator/overlays/release-2.5?ref=main +transformers: +- ./policyTransformer.yaml diff --git a/examples/transformer/output.yaml b/examples/transformer/output.yaml new file mode 100644 index 00000000..f7c9b7f3 --- /dev/null +++ b/examples/transformer/output.yaml @@ -0,0 +1,105 @@ +apiVersion: apps.open-cluster-management.io/v1 +kind: PlacementRule +metadata: + name: placement-transformed-policy + namespace: default +spec: + clusterSelector: + matchExpressions: [] +--- +apiVersion: policy.open-cluster-management.io/v1 +kind: PlacementBinding +metadata: + name: binding-transformed-policy + namespace: default +placementRef: + apiGroup: apps.open-cluster-management.io + kind: PlacementRule + name: placement-transformed-policy +subjects: +- apiGroup: policy.open-cluster-management.io + kind: Policy + name: transformed-policy +--- +apiVersion: policy.open-cluster-management.io/v1 +kind: Policy +metadata: + annotations: + policy.open-cluster-management.io/categories: CM Configuration Management + policy.open-cluster-management.io/controls: CM-2 Baseline Configuration + policy.open-cluster-management.io/standards: NIST SP 800-53 + name: transformed-policy + namespace: default +spec: + disabled: false + policy-templates: + - objectDefinition: + apiVersion: policy.open-cluster-management.io/v1 + kind: ConfigurationPolicy + metadata: + name: transformed-policy + spec: + evaluationInterval: + compliant: 30m + noncompliant: 45s + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: v1 + kind: Namespace + metadata: + annotations: {} + labels: + openshift.io/cluster-monitoring: "true" + name: open-cluster-management + remediationAction: inform + severity: low + - objectDefinition: + apiVersion: policy.open-cluster-management.io/v1 + kind: ConfigurationPolicy + metadata: + name: transformed-policy2 + spec: + evaluationInterval: + compliant: 30m + noncompliant: 45s + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: operators.coreos.com/v1 + kind: OperatorGroup + metadata: + annotations: {} + name: open-cluster-management + namespace: open-cluster-management + spec: + targetNamespaces: + - open-cluster-management + remediationAction: inform + severity: low + - objectDefinition: + apiVersion: policy.open-cluster-management.io/v1 + kind: ConfigurationPolicy + metadata: + name: transformed-policy3 + spec: + evaluationInterval: + compliant: 30m + noncompliant: 45s + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: operators.coreos.com/v1alpha1 + kind: Subscription + metadata: + annotations: {} + name: advanced-cluster-management + namespace: open-cluster-management + spec: + channel: release-2.5 + installPlanApproval: Automatic + name: advanced-cluster-management + source: redhat-operators + sourceNamespace: openshift-marketplace + remediationAction: inform + severity: low diff --git a/examples/transformer/policyTransformer.yaml b/examples/transformer/policyTransformer.yaml new file mode 100644 index 00000000..0ab7ae03 --- /dev/null +++ b/examples/transformer/policyTransformer.yaml @@ -0,0 +1,16 @@ +apiVersion: policy.open-cluster-management.io/v1 +kind: PolicyGenerator +metadata: + name: policy-transformer + +policyDefaults: + namespace: default + consolidateManifests: false + evaluationInterval: + compliant: 30m + noncompliant: 45s + +policies: +- name: transformed-policy + manifests: + - path: stdin diff --git a/internal/plugin.go b/internal/plugin.go index 1e41b74b..eb5ee5ca 100644 --- a/internal/plugin.go +++ b/internal/plugin.go @@ -807,16 +807,18 @@ func (p *Plugin) assertValidConfig() error { ) } - _, err := os.Stat(manifest.Path) - if err != nil { - return fmt.Errorf( - "could not read the manifest path %s in policy %s", manifest.Path, policy.Name, - ) - } + if manifest.Path != "stdin" { + _, err := os.Stat(manifest.Path) + if err != nil { + return fmt.Errorf( + "could not read the manifest path %s in policy %s", manifest.Path, policy.Name, + ) + } - err = verifyManifestPath(p.baseDirectory, manifest.Path) - if err != nil { - return err + err = verifyManifestPath(p.baseDirectory, manifest.Path) + if err != nil { + return err + } } evalInterval := manifest.EvaluationInterval diff --git a/internal/utils.go b/internal/utils.go index 84416be0..0b9b5e19 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -55,11 +55,13 @@ func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, e // Handle when a Kustomization directory is specified hasKustomize := false + for _, f := range files { _, filename := path.Split(f.Name()) if filename == "kustomization.yml" || filename == "kustomization.yaml" { hasKustomize = true manifestFiles, err = processKustomizeDir(manifest.Path) + if err != nil { return nil, err } @@ -187,6 +189,27 @@ func getPolicyTemplates(policyConf *types.PolicyConfig) ([]map[string]map[string continue } + // Annotations with these prefixes might be added to resources by kustomize, + // and should be removed when the resource is wrapped in a policy. + prefixesToDelete := []string{ + "config.kubernetes.io/path", + "config.kubernetes.io/index", + "config.k8s.io/id", + "kustomize.config.k8s.io/id", + "internal.config.kubernetes.io", + } + annotations, _, _ := unstructured.NestedStringMap(manifest, "metadata", "annotations") + + for key := range annotations { + for _, prefix := range prefixesToDelete { + if strings.HasPrefix(key, prefix) { + delete(annotations, key) + } + } + } + + _ = unstructured.SetNestedStringMap(manifest, annotations, "metadata", "annotations") + objTemplate := map[string]interface{}{ "complianceType": complianceType, "objectDefinition": manifest, From 1d3fdfbf5425e941d31ce583159df3ca997465f4 Mon Sep 17 00:00:00 2001 From: Justin Kulikauskas Date: Fri, 21 Oct 2022 14:39:13 -0400 Subject: [PATCH 7/7] Add ability to run in a container as a KRM plugin When run as a container by kustomize (by setting an annotation in the generator yaml), kustomize will send the configuration and other inputs to the process via stdin. New code converts that input into the format that the generator currently expects. The examples here should make it more clear. Signed-off-by: Justin Kulikauskas --- Makefile | 7 ++ build/Dockerfile | 24 ++++ cmd/main.go | 95 ++++++++++++++++ .../container-generator/kustomization.yaml | 23 ++++ examples/container-generator/output.yaml | 59 ++++++++++ .../resources/configmap-fish.yaml | 12 ++ .../resources/kustomization.yaml | 3 + .../container-transformer/kustomization.yaml | 25 +++++ examples/container-transformer/output.yaml | 105 ++++++++++++++++++ examples/transformer/kustomization.yaml | 3 + internal/plugin.go | 3 +- 11 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 build/Dockerfile create mode 100644 examples/container-generator/kustomization.yaml create mode 100644 examples/container-generator/output.yaml create mode 100644 examples/container-generator/resources/configmap-fish.yaml create mode 100644 examples/container-generator/resources/kustomization.yaml create mode 100644 examples/container-transformer/kustomization.yaml create mode 100644 examples/container-transformer/output.yaml diff --git a/Makefile b/Makefile index 760eb1a4..e37f9cf8 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,9 @@ API_PLUGIN_PATH ?= $(KUSTOMIZE_PLUGIN_HOME)/policy.open-cluster-management.io/v1 # Kustomize arguments SOURCE_DIR ?= examples/ +# Image settings +IMAGE_TAG ?= policy-generator-plugin:latest + # go-get-tool will 'go install' any package $1 and install it to LOCAL_BIN. define go-get-tool @set -e ;\ @@ -54,6 +57,10 @@ build: layout build-binary: go build -o PolicyGenerator cmd/main.go +.PHONY: build-image +build-image: + docker build -f ./build/Dockerfile -t $(IMAGE_TAG) . + .PHONY: build-release build-release: @if [[ $(shell git status --porcelain | wc -l) -gt 0 ]]; \ diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 00000000..f595c956 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,24 @@ +# Build the manager binary +FROM golang:1.18 as builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd cmd +COPY internal internal + +# Build +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o generator cmd/main.go + +FROM registry.access.redhat.com/ubi8/ubi-minimal:latest +WORKDIR / +COPY --from=builder /workspace/generator /generator + +WORKDIR /tmp +ENTRYPOINT ["/generator"] diff --git a/cmd/main.go b/cmd/main.go index 5af698c0..e8c2bbe8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,12 +2,15 @@ package main import ( "bytes" + "errors" "fmt" + "io" "io/ioutil" "os" "github.com/spf13/pflag" "open-cluster-management.io/ocm-kustomize-generator-plugins/internal" + "sigs.k8s.io/kustomize/kyaml/kio" ) var debug = false @@ -23,6 +26,14 @@ func main() { generators := pflag.Args() var outputBuffer bytes.Buffer + if len(generators) == 0 { + if err := runKRMplugin(os.Stdin, os.Stdout); err != nil { + errorAndExit(err.Error()) + } + + return + } + for _, gen := range generators { outputBuffer.Write(processGeneratorConfig(gen)) } @@ -78,3 +89,87 @@ func processGeneratorConfig(filePath string) []byte { return generatedOutput } + +func runKRMplugin(input io.Reader, output io.Writer) error { + inputReader := kio.ByteReader{Reader: input} + + inputs, err := inputReader.Read() + if err != nil { + return fmt.Errorf("failed to read input: %w", err) + } + + config, err := inputReader.FunctionConfig.MarshalJSON() + if err != nil { + return fmt.Errorf("failed to marshal KRM configuration from input: %w", err) + } + + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to determine the current directory: %w", err) + } + + p := internal.Plugin{} + + err = p.Config(config, cwd) + if err != nil { + return fmt.Errorf("error processing the PolicyGenerator file '[stdin]': %w", err) + } + + // in KRM generator mode, this annotation will be set by kustomize + if inputs[0].GetAnnotations()["config.kubernetes.io/local-config"] != "true" { + // in KRM transformer mode, convert the KRM-style input yaml into the + // flat yaml format the generator uses, and write it to a temp file. + inpFile, err := os.CreateTemp(".", "transformer-intput-*.yaml") + if err != nil { + return fmt.Errorf("error creating an input file: %w", err) + } + + defer os.Remove(inpFile.Name()) // clean up + + inpwriter := kio.ByteWriter{ + Writer: inpFile, + ClearAnnotations: []string{ + "config.k8s.io/id", + "internal.config.kubernetes.io/annotations-migration-resource-id", + "internal.config.kubernetes.io/id", + "kustomize.config.k8s.io/id", + }, + } + + err = inpwriter.Write(inputs) + if err != nil { + return fmt.Errorf("error writing input KRM yaml to the temporary manifest: %w", err) + } + + if len(p.Policies) == 0 || len(p.Policies[0].Manifests) == 0 { + return errors.New("no manifests in config file") + } + + // overwrites the path in the generator yaml, from stdin to the temp file. + p.Policies[0].Manifests[0].Path = inpFile.Name() + } + + generatedOutput, err := p.Generate() + if err != nil { + return fmt.Errorf("error generating policies from the PolicyGenerator file: %w", err) + } + + nodes, err := (&kio.ByteReader{Reader: bytes.NewReader(generatedOutput)}).Read() + if err != nil { + return fmt.Errorf("error reading generator output: %w", err) + } + + // Write the result in a ResourceList + outputWriter := kio.ByteWriter{ + Writer: output, + WrappingAPIVersion: "config.kubernetes.io/v1", + WrappingKind: "ResourceList", + } + + err = outputWriter.Write(nodes) + if err != nil { + return fmt.Errorf("error writing generator output: %w", err) + } + + return nil +} diff --git a/examples/container-generator/kustomization.yaml b/examples/container-generator/kustomization.yaml new file mode 100644 index 00000000..ddd67a7b --- /dev/null +++ b/examples/container-generator/kustomization.yaml @@ -0,0 +1,23 @@ +# run this example with `kustomize build . --enable-alpha-plugins --mount type=bind,source=$(pwd),target=/tmp/,readonly > output.yaml` +# Note: the manifests can not use kustomization.yaml files referencing non-local resources (eg github) + +generators: +- |- + apiVersion: policy.open-cluster-management.io/v1 + kind: PolicyGenerator + metadata: + name: policy-generator + annotations: + config.kubernetes.io/function: | + container: + image: policy-generator-plugin:latest + policyDefaults: + namespace: default + consolidateManifests: false + evaluationInterval: + compliant: 30m + noncompliant: 45s + policies: + - name: made-in-container + manifests: + - path: resources/ diff --git a/examples/container-generator/output.yaml b/examples/container-generator/output.yaml new file mode 100644 index 00000000..8541b09d --- /dev/null +++ b/examples/container-generator/output.yaml @@ -0,0 +1,59 @@ +apiVersion: apps.open-cluster-management.io/v1 +kind: PlacementRule +metadata: + name: placement-made-in-container + namespace: default +spec: + clusterSelector: + matchExpressions: [] +--- +apiVersion: policy.open-cluster-management.io/v1 +kind: PlacementBinding +metadata: + name: binding-made-in-container + namespace: default +placementRef: + apiGroup: apps.open-cluster-management.io + kind: PlacementRule + name: placement-made-in-container +subjects: +- apiGroup: policy.open-cluster-management.io + kind: Policy + name: made-in-container +--- +apiVersion: policy.open-cluster-management.io/v1 +kind: Policy +metadata: + annotations: + policy.open-cluster-management.io/categories: CM Configuration Management + policy.open-cluster-management.io/controls: CM-2 Baseline Configuration + policy.open-cluster-management.io/standards: NIST SP 800-53 + name: made-in-container + namespace: default +spec: + disabled: false + policy-templates: + - objectDefinition: + apiVersion: policy.open-cluster-management.io/v1 + kind: ConfigurationPolicy + metadata: + name: made-in-container + spec: + evaluationInterval: + compliant: 30m + noncompliant: 45s + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: v1 + data: + game.properties: "enemies=goldfish \n" + ui.properties: | + color.good=neon-green + kind: ConfigMap + metadata: + annotations: {} + name: kustomized-game-config-fish + namespace: default + remediationAction: inform + severity: low diff --git a/examples/container-generator/resources/configmap-fish.yaml b/examples/container-generator/resources/configmap-fish.yaml new file mode 100644 index 00000000..e16eb429 --- /dev/null +++ b/examples/container-generator/resources/configmap-fish.yaml @@ -0,0 +1,12 @@ +# Taken from https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: game-config-fish + namespace: default +data: + game.properties: | + enemies=goldfish + ui.properties: | + color.good=neon-green diff --git a/examples/container-generator/resources/kustomization.yaml b/examples/container-generator/resources/kustomization.yaml new file mode 100644 index 00000000..35106774 --- /dev/null +++ b/examples/container-generator/resources/kustomization.yaml @@ -0,0 +1,3 @@ +namePrefix: kustomized- +resources: +- ./configmap-fish.yaml diff --git a/examples/container-transformer/kustomization.yaml b/examples/container-transformer/kustomization.yaml new file mode 100644 index 00000000..a091c417 --- /dev/null +++ b/examples/container-transformer/kustomization.yaml @@ -0,0 +1,25 @@ +# run this example with `kustomize build . --enable-alpha-plugins > output.yaml` +# Note: the container image must be available + +resources: +- github.com/redhat-cop/gitops-catalog/advanced-cluster-management/operator/overlays/release-2.5?ref=main +transformers: +- |- + apiVersion: policy.open-cluster-management.io/v1 + kind: PolicyGenerator + metadata: + name: policy-transformer + annotations: + config.kubernetes.io/function: | + container: + image: policy-generator-plugin:latest + policyDefaults: + namespace: default + consolidateManifests: false + evaluationInterval: + compliant: 30m + noncompliant: 45s + policies: + - name: transformed-policy + manifests: + - path: stdin diff --git a/examples/container-transformer/output.yaml b/examples/container-transformer/output.yaml new file mode 100644 index 00000000..f7c9b7f3 --- /dev/null +++ b/examples/container-transformer/output.yaml @@ -0,0 +1,105 @@ +apiVersion: apps.open-cluster-management.io/v1 +kind: PlacementRule +metadata: + name: placement-transformed-policy + namespace: default +spec: + clusterSelector: + matchExpressions: [] +--- +apiVersion: policy.open-cluster-management.io/v1 +kind: PlacementBinding +metadata: + name: binding-transformed-policy + namespace: default +placementRef: + apiGroup: apps.open-cluster-management.io + kind: PlacementRule + name: placement-transformed-policy +subjects: +- apiGroup: policy.open-cluster-management.io + kind: Policy + name: transformed-policy +--- +apiVersion: policy.open-cluster-management.io/v1 +kind: Policy +metadata: + annotations: + policy.open-cluster-management.io/categories: CM Configuration Management + policy.open-cluster-management.io/controls: CM-2 Baseline Configuration + policy.open-cluster-management.io/standards: NIST SP 800-53 + name: transformed-policy + namespace: default +spec: + disabled: false + policy-templates: + - objectDefinition: + apiVersion: policy.open-cluster-management.io/v1 + kind: ConfigurationPolicy + metadata: + name: transformed-policy + spec: + evaluationInterval: + compliant: 30m + noncompliant: 45s + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: v1 + kind: Namespace + metadata: + annotations: {} + labels: + openshift.io/cluster-monitoring: "true" + name: open-cluster-management + remediationAction: inform + severity: low + - objectDefinition: + apiVersion: policy.open-cluster-management.io/v1 + kind: ConfigurationPolicy + metadata: + name: transformed-policy2 + spec: + evaluationInterval: + compliant: 30m + noncompliant: 45s + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: operators.coreos.com/v1 + kind: OperatorGroup + metadata: + annotations: {} + name: open-cluster-management + namespace: open-cluster-management + spec: + targetNamespaces: + - open-cluster-management + remediationAction: inform + severity: low + - objectDefinition: + apiVersion: policy.open-cluster-management.io/v1 + kind: ConfigurationPolicy + metadata: + name: transformed-policy3 + spec: + evaluationInterval: + compliant: 30m + noncompliant: 45s + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: operators.coreos.com/v1alpha1 + kind: Subscription + metadata: + annotations: {} + name: advanced-cluster-management + namespace: open-cluster-management + spec: + channel: release-2.5 + installPlanApproval: Automatic + name: advanced-cluster-management + source: redhat-operators + sourceNamespace: openshift-marketplace + remediationAction: inform + severity: low diff --git a/examples/transformer/kustomization.yaml b/examples/transformer/kustomization.yaml index 0466cbe4..da04c7d3 100644 --- a/examples/transformer/kustomization.yaml +++ b/examples/transformer/kustomization.yaml @@ -1,3 +1,6 @@ +# run this example with `kustomize build . --enable-alpha-plugins > output.yaml` +# Note: you must have the plugin installed to your kustomize plugin directory. + resources: - github.com/redhat-cop/gitops-catalog/advanced-cluster-management/operator/overlays/release-2.5?ref=main transformers: diff --git a/internal/plugin.go b/internal/plugin.go index eb5ee5ca..f92eabf7 100644 --- a/internal/plugin.go +++ b/internal/plugin.go @@ -41,7 +41,8 @@ type Plugin struct { APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` Metadata struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` } `json:"metadata,omitempty" yaml:"metadata,omitempty"` PlacementBindingDefaults struct { Name string `json:"name,omitempty" yaml:"name,omitempty"`