From a0c1979798ab3cf2d53ab42cac3675a176233b20 Mon Sep 17 00:00:00 2001 From: Donny Xia Date: Tue, 16 Jun 2020 13:59:44 -0700 Subject: [PATCH 1/5] Add validator to kustomization --- api/internal/plugins/execplugin/execplugin.go | 5 ++++ api/internal/plugins/loader/loader.go | 26 ++++++++++++++++++ api/internal/target/kusttarget.go | 27 +++++++++++++++++++ api/resmap/resmap.go | 11 ++++++++ api/types/kustomization.go | 3 +++ 5 files changed, 72 insertions(+) diff --git a/api/internal/plugins/execplugin/execplugin.go b/api/internal/plugins/execplugin/execplugin.go index d780121e6..a7df41981 100644 --- a/api/internal/plugins/execplugin/execplugin.go +++ b/api/internal/plugins/execplugin/execplugin.go @@ -140,6 +140,11 @@ func (p *ExecPlugin) Transform(rm resmap.ResMap) error { return p.updateResMapValues(output, rm) } +func (p *ExecPlugin) Validate(rm resmap.ResMap) error { + // Validate works exactly same with Transformer + return p.Transform(rm) +} + // invokePlugin writes plugin config to a temp file, then // passes the full temp file path as the first arg to a process // running the plugin binary. Process output is returned. diff --git a/api/internal/plugins/loader/loader.go b/api/internal/plugins/loader/loader.go index 09dc3a167..1d1fe8e80 100644 --- a/api/internal/plugins/loader/loader.go +++ b/api/internal/plugins/loader/loader.go @@ -87,6 +87,32 @@ func (l *Loader) LoadTransformer( return t, nil } +func (l *Loader) LoadValidators( + ldr ifc.Loader, v ifc.Validator, rm resmap.ResMap) ([]resmap.Validator, error) { + var result []resmap.Validator + for _, res := range rm.Resources() { + t, err := l.LoadValidator(ldr, v, res) + if err != nil { + return nil, err + } + result = append(result, t) + } + return result, nil +} + +func (l *Loader) LoadValidator( + ldr ifc.Loader, v ifc.Validator, res *resource.Resource) (resmap.Validator, error) { + c, err := l.loadAndConfigurePlugin(ldr, v, res) + if err != nil { + return nil, err + } + t, ok := c.(resmap.Validator) + if !ok { + return nil, fmt.Errorf("plugin %s not a validator", res.OrgId()) + } + return t, nil +} + func relativePluginPath(id resid.ResId) string { return filepath.Join( id.Group, diff --git a/api/internal/target/kusttarget.go b/api/internal/target/kusttarget.go index 4cade158d..06af972bc 100644 --- a/api/internal/target/kusttarget.go +++ b/api/internal/target/kusttarget.go @@ -209,6 +209,10 @@ func (kt *KustTarget) accumulateTarget(ra *accumulator.ResAccumulator) ( if err != nil { return nil, err } + err = kt.runValidators(ra) + if err != nil { + return nil, err + } err = ra.MergeVars(kt.kustomization.Vars) if err != nil { return nil, errors.Wrapf( @@ -278,6 +282,29 @@ func (kt *KustTarget) configureExternalTransformers() ([]resmap.Transformer, err return kt.pLdr.LoadTransformers(kt.ldr, kt.validator, ra.ResMap()) } +func (kt *KustTarget) runValidators(ra *accumulator.ResAccumulator) error { + validators, err := kt.configureExternalValidators() + if err != nil { + return err + } + for _, v := range validators { + err := v.Validate(ra.ResMap()) + if err != nil { + return err + } + } + return nil +} + +func (kt *KustTarget) configureExternalValidators() ([]resmap.Validator, error) { + ra := accumulator.MakeEmptyAccumulator() + ra, err := kt.accumulateResources(ra, kt.kustomization.Validators) + if err != nil { + return nil, err + } + return kt.pLdr.LoadValidators(kt.ldr, kt.validator, ra.ResMap()) +} + // accumulateResources fills the given resourceAccumulator // with resources read from the given list of paths. func (kt *KustTarget) accumulateResources( diff --git a/api/resmap/resmap.go b/api/resmap/resmap.go index 91e87df66..db971b462 100644 --- a/api/resmap/resmap.go +++ b/api/resmap/resmap.go @@ -30,6 +30,12 @@ type Generator interface { Generate() (ResMap, error) } +// A Validator checkes the ResMap and return an error +// if it's not valid. +type Validator interface { + Validate(m ResMap) error +} + // Something that's configurable accepts an // instance of PluginHelpers and a raw config // object (YAML in []byte form). @@ -73,6 +79,11 @@ type TransformerPlugin interface { Configurable } +type ValidatorPlugin interface { + Validator + Configurable +} + // ResMap is an interface describing operations on the // core kustomize data structure, a list of Resources. // diff --git a/api/types/kustomization.go b/api/types/kustomization.go index 4a2c42d36..e7a1f9dac 100644 --- a/api/types/kustomization.go +++ b/api/types/kustomization.go @@ -124,6 +124,9 @@ type Kustomization struct { // Transformers is a list of files containing transformers Transformers []string `json:"transformers,omitempty" yaml:"transformers,omitempty"` + // Validators is a list of files containing validators + Validators []string `json:"validators,omitempty" yaml:"validators,omitempty"` + // Inventory appends an object that contains the record // of all other objects, which can be used in apply, prune and delete Inventory *Inventory `json:"inventory,omitempty" yaml:"inventory,omitempty"` From 4843718a2b57bcd6d734079b4dee8aaf8d62cc04 Mon Sep 17 00:00:00 2001 From: Donny Xia Date: Tue, 16 Jun 2020 14:21:26 -0700 Subject: [PATCH 2/5] Check modification --- api/internal/target/kusttarget.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/api/internal/target/kusttarget.go b/api/internal/target/kusttarget.go index 06af972bc..d7cb1f4fd 100644 --- a/api/internal/target/kusttarget.go +++ b/api/internal/target/kusttarget.go @@ -5,6 +5,7 @@ package target import ( "bytes" + "crypto/sha1" "encoding/json" "fmt" "strings" @@ -288,14 +289,36 @@ func (kt *KustTarget) runValidators(ra *accumulator.ResAccumulator) error { return err } for _, v := range validators { - err := v.Validate(ra.ResMap()) + // Validators shouldn't modify the resource map + orignalHash, err := getSha1Hash(ra.ResMap()) if err != nil { return err } + err = v.Validate(ra.ResMap()) + if err != nil { + return err + } + newHash, err := getSha1Hash(ra.ResMap()) + if err != nil { + return err + } + if !bytes.Equal(orignalHash, newHash) { + return fmt.Errorf("validator %#v shouldn't modify the resource map", v) + } } return nil } +func getSha1Hash(rm resmap.ResMap) ([]byte, error) { + sha1Hash := sha1.New() + yamlBytes, err := rm.AsYaml() + if err != nil { + return nil, err + } + sha1Hash.Write(yamlBytes) + return sha1Hash.Sum(nil), nil +} + func (kt *KustTarget) configureExternalValidators() ([]resmap.Validator, error) { ra := accumulator.MakeEmptyAccumulator() ra, err := kt.accumulateResources(ra, kt.kustomization.Validators) From cea1154cd966ce32cfd299dcac9e06b225c389cb Mon Sep 17 00:00:00 2001 From: Donny Xia Date: Tue, 16 Jun 2020 15:21:38 -0700 Subject: [PATCH 3/5] Add exception for "validated-by" label --- api/internal/target/kusttarget.go | 38 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/api/internal/target/kusttarget.go b/api/internal/target/kusttarget.go index d7cb1f4fd..9337e5057 100644 --- a/api/internal/target/kusttarget.go +++ b/api/internal/target/kusttarget.go @@ -5,7 +5,6 @@ package target import ( "bytes" - "crypto/sha1" "encoding/json" "fmt" "strings" @@ -290,33 +289,36 @@ func (kt *KustTarget) runValidators(ra *accumulator.ResAccumulator) error { } for _, v := range validators { // Validators shouldn't modify the resource map - orignalHash, err := getSha1Hash(ra.ResMap()) - if err != nil { - return err - } + orignal := ra.ResMap().DeepCopy() err = v.Validate(ra.ResMap()) if err != nil { return err } - newHash, err := getSha1Hash(ra.ResMap()) - if err != nil { - return err - } - if !bytes.Equal(orignalHash, newHash) { - return fmt.Errorf("validator %#v shouldn't modify the resource map", v) + new := ra.ResMap().DeepCopy() + kt.removeValidatedByLabel(new) + if err = orignal.ErrorIfNotEqualSets(new); err != nil { + return fmt.Errorf("validator shouldn't modify the resource map: %v", err) } } return nil } -func getSha1Hash(rm resmap.ResMap) ([]byte, error) { - sha1Hash := sha1.New() - yamlBytes, err := rm.AsYaml() - if err != nil { - return nil, err +func (kt *KustTarget) removeValidatedByLabel(rm resmap.ResMap) { + var validatedByLabelName string = "validated-by" + + resources := rm.Resources() + for _, r := range resources { + labels := r.GetLabels() + if _, found := labels[validatedByLabelName]; !found { + continue + } + delete(labels, validatedByLabelName) + if len(labels) == 0 { + r.SetLabels(nil) + } else { + r.SetLabels(labels) + } } - sha1Hash.Write(yamlBytes) - return sha1Hash.Sum(nil), nil } func (kt *KustTarget) configureExternalValidators() ([]resmap.Validator, error) { From 160485ef193d9455134ac17fb0cafcd7fb8347dc Mon Sep 17 00:00:00 2001 From: Donny Xia Date: Wed, 17 Jun 2020 14:42:20 -0700 Subject: [PATCH 4/5] Add examples to validators --- examples/multibases/validatorPlugin.md | 224 +++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 examples/multibases/validatorPlugin.md diff --git a/examples/multibases/validatorPlugin.md b/examples/multibases/validatorPlugin.md new file mode 100644 index 000000000..6c350410e --- /dev/null +++ b/examples/multibases/validatorPlugin.md @@ -0,0 +1,224 @@ +# Examples for Validator Plugin + +Previously, Kustomize suggested to used a transformer plugin to [perform validation](https://github.com/kubernetes-sigs/kustomize/tree/master/examples/validationTransformer). Now we introduce a new type of plugin: validator. As the name says, validator is used to validate the result YAML output. It works in the same way with *transformers* but cannot *modify* the input YAML content. Let's take a look at how it works. + +## Make a Place to Work + + +``` +DEMO_HOME=$(mktemp -d) +mkdir -p $DEMO_HOME/valid +PLUGINDIR=$DEMO_HOME/kustomize/plugin/someteam.example.com/v1/validator +mkdir -p $PLUGINDIR +``` + +## Write a Validator Plugin + +Kustomize has the following assumption of a validator plugin: +- The resources are passed to the validator plugin from stdin. +- The configuration file for the validator plugin is passed in + as the first argument. +- The working directory of the plugin is the kustomization + directory where it is used as a validator. +- The validated resources are written to stdout by the plugin. Or the validator can print nothing to the stdout if there is no need to change the input. +- Validator can **only** add a label named `validated-by` (case-sensitive) to the **top-level** resources. If there is any other modification in the validator, Kustomize will throw an error. +- If the return code of the transformer plugin is non zero, + Kustomize regards there is an error during the validation. + +You can use either exec plugin or Go plugin as a validator. Here we use a bash script as an exec plugin. + + +```bash +cat <<'EOF' > $PLUGINDIR/Validator +#!/bin/bash + +# Do whatever you want here. In this example we +# just print out the input + +cat + +EOF +chmod +x $PLUGINDIR/Validator +``` + +## Use the Validator Plugin + +Define a kustomization containing a valid ConfigMap +and the transformer plugin. + + +```bash +cat <<'EOF' >$DEMO_HOME/valid/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + foo: bar +EOF + +cat <<'EOF' >$DEMO_HOME/valid/validator.yaml +apiVersion: someteam.example.com/v1 +kind: Validator +metadata: + name: notImportantHere +EOF + +cat <<'EOF' >$DEMO_HOME/valid/kustomization.yaml +resources: +- configmap.yaml + +validators: +- validator.yaml +EOF +``` + +The directory structure is as the following: + +``` +/tmp/tmp.69tTCuXuYc +├── kustomize +│   └── plugin +│   └── someteam.example.com +│   └── v1 +│   └── validator +│   └── Validator +└── valid + ├── configmap.yaml + ├── kustomization.yaml + └── validator.yaml +``` + +Define a helper function to run kustomize with the +correct environment and flags for plugins: + + +```bash +function kustomizeBd { + XDG_CONFIG_HOME=$DEMO_HOME \ + kustomize build \ + --enable_alpha_plugins \ + $DEMO_HOME/$1 +} +``` + +Build the valid variant + + +```bash +kustomizeBd valid +``` +The output contains a ConfigMap as + +```yaml +apiVersion: v1 +data: + foo: bar +kind: ConfigMap +metadata: + name: cm +``` + +### Validator Failure + +Now lets try a failed validator + +```bash +cat <<'EOF' > $PLUGINDIR/Validator +#!/bin/bash + +# Non-zero indicates a failed validation +>&2 echo "Validation failed" +exit 1 + +EOF +chmod +x $PLUGINDIR/Validator +``` + +Build the valid variant + +```bash +kustomizeBd valid +``` +The output contains the error information that is printed to stderr +by validator. + +``` +Validation failed +Error: failure in plugin configured via /tmp/kust-plugin-config-369137659; exit status 1: exit status 1 +``` + +### Input Modification + +Typically a validator shouldn't modify the content to be validated. If it does, Kustomize will complain about it. + +```bash +cat <<'EOF' > $PLUGINDIR/Validator +#!/bin/bash + +# Modify the input content + +sed 's/bar/baz/g' + +EOF +chmod +x $PLUGINDIR/Validator +``` + +Then build + +``` +kustomizeBd valid +``` + +The error output will indicate you where is modified by the validator + +``` +Error: validator shouldn't modify the resource map: kunstruct not equal: + -- {"apiVersion":"v1","data":{"foo":"bar"},"kind":"ConfigMap","metadata":{"name":"cm"}}{nsfx:false,beh:unspecified}, + -- {"apiVersion":"v1","data":{"foo":"baz"},"kind":"ConfigMap","metadata":{"name":"cm"}}{nsfx:false,beh:unspecified} + +-- +&resource.Resource{Kunstructured:(*kunstruct.UnstructAdapter)(0xc000118408), originalName:"cm", originalNs:"", options:(*types.GenArgs)(0xc00059e5e8), refBy:[]resid.ResId(nil), refVarNames:[]string(nil), namePrefixes:[]string{""}, nameSuffixes:[]string{""}} +------ +&resource.Resource{Kunstructured:(*kunstruct.UnstructAdapter)(0xc000118510), originalName:"cm", originalNs:"", options:(*types.GenArgs)(0xc00059e5e8), refBy:[]resid.ResId(nil), refVarNames:[]string(nil), namePrefixes:[]string{""}, nameSuffixes:[]string{""}} +``` + +There is an exception that the validator can add a `validated-by` label to the **top** level resources. + + +```bash +cat <<'EOF' > $PLUGINDIR/Validator +#!/usr/bin/bash + +sed 's/^ name: cm$/ name: cm\n labels:\n validated-by: whatever/' + +EOF +chmod +x $PLUGINDIR/Validator +``` + +Then build + + +``` +kustomizeBd valid +``` + +The output will be + +```yaml +apiVersion: v1 +data: + foo: bar +kind: ConfigMap +metadata: + labels: + validated-by: whatever + name: cm +``` + +## cleanup + + +``` +rm -rf $DEMO_HOME +``` \ No newline at end of file From 7e2d3ff5ab073cfe8dbc2da9f2173ce998b85b78 Mon Sep 17 00:00:00 2001 From: Donny Xia Date: Thu, 18 Jun 2020 12:18:37 -0700 Subject: [PATCH 5/5] Reuse tansformer codes --- api/internal/plugins/execplugin/execplugin.go | 5 ---- api/internal/plugins/loader/loader.go | 26 ------------------- api/internal/target/kusttarget.go | 25 ++++++------------ api/konfig/general.go | 3 +++ api/resmap/resmap.go | 11 -------- examples/{multibases => }/validatorPlugin.md | 0 6 files changed, 11 insertions(+), 59 deletions(-) rename examples/{multibases => }/validatorPlugin.md (100%) diff --git a/api/internal/plugins/execplugin/execplugin.go b/api/internal/plugins/execplugin/execplugin.go index a7df41981..d780121e6 100644 --- a/api/internal/plugins/execplugin/execplugin.go +++ b/api/internal/plugins/execplugin/execplugin.go @@ -140,11 +140,6 @@ func (p *ExecPlugin) Transform(rm resmap.ResMap) error { return p.updateResMapValues(output, rm) } -func (p *ExecPlugin) Validate(rm resmap.ResMap) error { - // Validate works exactly same with Transformer - return p.Transform(rm) -} - // invokePlugin writes plugin config to a temp file, then // passes the full temp file path as the first arg to a process // running the plugin binary. Process output is returned. diff --git a/api/internal/plugins/loader/loader.go b/api/internal/plugins/loader/loader.go index 1d1fe8e80..09dc3a167 100644 --- a/api/internal/plugins/loader/loader.go +++ b/api/internal/plugins/loader/loader.go @@ -87,32 +87,6 @@ func (l *Loader) LoadTransformer( return t, nil } -func (l *Loader) LoadValidators( - ldr ifc.Loader, v ifc.Validator, rm resmap.ResMap) ([]resmap.Validator, error) { - var result []resmap.Validator - for _, res := range rm.Resources() { - t, err := l.LoadValidator(ldr, v, res) - if err != nil { - return nil, err - } - result = append(result, t) - } - return result, nil -} - -func (l *Loader) LoadValidator( - ldr ifc.Loader, v ifc.Validator, res *resource.Resource) (resmap.Validator, error) { - c, err := l.loadAndConfigurePlugin(ldr, v, res) - if err != nil { - return nil, err - } - t, ok := c.(resmap.Validator) - if !ok { - return nil, fmt.Errorf("plugin %s not a validator", res.OrgId()) - } - return t, nil -} - func relativePluginPath(id resid.ResId) string { return filepath.Join( id.Group, diff --git a/api/internal/target/kusttarget.go b/api/internal/target/kusttarget.go index 9337e5057..275ebc990 100644 --- a/api/internal/target/kusttarget.go +++ b/api/internal/target/kusttarget.go @@ -264,7 +264,7 @@ func (kt *KustTarget) runTransformers(ra *accumulator.ResAccumulator) error { return err } r = append(r, lts...) - lts, err = kt.configureExternalTransformers() + lts, err = kt.configureExternalTransformers(kt.kustomization.Transformers) if err != nil { return err } @@ -273,9 +273,10 @@ func (kt *KustTarget) runTransformers(ra *accumulator.ResAccumulator) error { return ra.Transform(t) } -func (kt *KustTarget) configureExternalTransformers() ([]resmap.Transformer, error) { +func (kt *KustTarget) configureExternalTransformers(transformers []string) ([]resmap.Transformer, error) { ra := accumulator.MakeEmptyAccumulator() - ra, err := kt.accumulateResources(ra, kt.kustomization.Transformers) + ra, err := kt.accumulateResources(ra, transformers) + if err != nil { return nil, err } @@ -283,14 +284,14 @@ func (kt *KustTarget) configureExternalTransformers() ([]resmap.Transformer, err } func (kt *KustTarget) runValidators(ra *accumulator.ResAccumulator) error { - validators, err := kt.configureExternalValidators() + validators, err := kt.configureExternalTransformers(kt.kustomization.Validators) if err != nil { return err } for _, v := range validators { // Validators shouldn't modify the resource map orignal := ra.ResMap().DeepCopy() - err = v.Validate(ra.ResMap()) + err = v.Transform(ra.ResMap()) if err != nil { return err } @@ -304,15 +305,14 @@ func (kt *KustTarget) runValidators(ra *accumulator.ResAccumulator) error { } func (kt *KustTarget) removeValidatedByLabel(rm resmap.ResMap) { - var validatedByLabelName string = "validated-by" resources := rm.Resources() for _, r := range resources { labels := r.GetLabels() - if _, found := labels[validatedByLabelName]; !found { + if _, found := labels[konfig.ValidatedByLabelKey]; !found { continue } - delete(labels, validatedByLabelName) + delete(labels, konfig.ValidatedByLabelKey) if len(labels) == 0 { r.SetLabels(nil) } else { @@ -321,15 +321,6 @@ func (kt *KustTarget) removeValidatedByLabel(rm resmap.ResMap) { } } -func (kt *KustTarget) configureExternalValidators() ([]resmap.Validator, error) { - ra := accumulator.MakeEmptyAccumulator() - ra, err := kt.accumulateResources(ra, kt.kustomization.Validators) - if err != nil { - return nil, err - } - return kt.pLdr.LoadValidators(kt.ldr, kt.validator, ra.ResMap()) -} - // accumulateResources fills the given resourceAccumulator // with resources read from the given list of paths. func (kt *KustTarget) accumulateResources( diff --git a/api/konfig/general.go b/api/konfig/general.go index 80a7d9be1..de0292415 100644 --- a/api/konfig/general.go +++ b/api/konfig/general.go @@ -36,4 +36,7 @@ const ( // An environment variable to turn on/off adding the ManagedByLabelKey EnableManagedbyLabelEnv = "KUSTOMIZE_ENABLE_MANAGEDBY_LABEL" + + // Label key that indicates the resources are validated by a validator + ValidatedByLabelKey = "validated-by" ) diff --git a/api/resmap/resmap.go b/api/resmap/resmap.go index db971b462..91e87df66 100644 --- a/api/resmap/resmap.go +++ b/api/resmap/resmap.go @@ -30,12 +30,6 @@ type Generator interface { Generate() (ResMap, error) } -// A Validator checkes the ResMap and return an error -// if it's not valid. -type Validator interface { - Validate(m ResMap) error -} - // Something that's configurable accepts an // instance of PluginHelpers and a raw config // object (YAML in []byte form). @@ -79,11 +73,6 @@ type TransformerPlugin interface { Configurable } -type ValidatorPlugin interface { - Validator - Configurable -} - // ResMap is an interface describing operations on the // core kustomize data structure, a list of Resources. // diff --git a/examples/multibases/validatorPlugin.md b/examples/validatorPlugin.md similarity index 100% rename from examples/multibases/validatorPlugin.md rename to examples/validatorPlugin.md