diff --git a/functions/examples/validator-kubeval/LICENSE_TEMPLATE b/functions/examples/validator-kubeval/LICENSE_TEMPLATE new file mode 100644 index 000000000..0c2b3b655 --- /dev/null +++ b/functions/examples/validator-kubeval/LICENSE_TEMPLATE @@ -0,0 +1,2 @@ +Copyright {{.Year}} {{.Holder}} +SPDX-License-Identifier: Apache-2.0 diff --git a/functions/examples/validator-kubeval/Makefile b/functions/examples/validator-kubeval/Makefile new file mode 100644 index 000000000..b8030c006 --- /dev/null +++ b/functions/examples/validator-kubeval/Makefile @@ -0,0 +1,42 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +.PHONY: generate license fix vet fmt test build tidy image + +GOBIN := $(shell go env GOPATH)/bin + +build: + (cd image && go build -v -o $(GOBIN)/config-function .) + +all: generate license build fix vet fmt test lint tidy + +fix: + (cd image && go fix ./...) + +fmt: + (cd image && go fmt ./...) + +generate: + (which $(GOBIN)/mdtogo || go get sigs.k8s.io/kustomize/cmd/mdtogo) + (cd image && GOBIN=$(GOBIN) go generate ./...) + +license: + (which $(GOPATH)/bin/addlicense || go get github.com/google/addlicense) + $(GOPATH)/bin/addlicense -y 2019 -c "The Kubernetes Authors." -f LICENSE_TEMPLATE . + +tidy: + (cd image && go mod tidy) + +lint: + (which $(GOBIN)/golangci-lint || go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.19.1) + (cd image && $(GOBIN)/golangci-lint run ./...) + +test: + (cd image && go test -cover ./...) + +vet: + (cd image && go vet ./...) + +image: + docker build image -t gcr.io/kustomize-functions/example-validator-kubeval:v0.1.0 + docker push gcr.io/kustomize-functions/example-validator-kubeval:v0.1.0 diff --git a/functions/examples/validator-kubeval/README.md b/functions/examples/validator-kubeval/README.md new file mode 100644 index 000000000..aabcb87f3 --- /dev/null +++ b/functions/examples/validator-kubeval/README.md @@ -0,0 +1,44 @@ +# Validation + +This is an example of implementing a validation function against +[kubeval](https://github.com/instrumenta/kubeval). + +## Function implementation + +The function is implemented as an [image](image), and built using `make image`. + +The function is implemented as a go program, which reads a collection of input +Resource configuration, passing each to kubeval. + +### Function configuration + +A number of settings can be modified for `kubeval` in the function `spec`. See +the `API` struct definition in [main.go](image/main.go) for documentation. + +## Function invocation + +The function is invoked by authoring a [local Resource](local-resource) +with `metadata.configFn` and running: + + kustomize config run local-resource/ + +This exists non-zero if kubeval detects an invalid Resource. + +## Running the Example + +Run the validator with: + + kustomize config run local-resource/ + +This will return an error: + + Resource invalid: (Kind: Service, Name: svc) + prots: Additional property prots is not allowed + Error: exit status 1 + +Now fix the typo in [example-use.yaml](local-resource/example-use.yaml) and +run: + + kustomize config run local-resource/ + +This will return success (no output). diff --git a/functions/examples/validator-kubeval/image/Dockerfile b/functions/examples/validator-kubeval/image/Dockerfile new file mode 100644 index 000000000..7181aa545 --- /dev/null +++ b/functions/examples/validator-kubeval/image/Dockerfile @@ -0,0 +1,17 @@ +FROM alpine:latest as schemas +RUN apk --no-cache add git +RUN git clone --depth 1 https://github.com/instrumenta/kubernetes-json-schema.git + +FROM golang:1.13-stretch as function +ENV CGO_ENABLED=0 +WORKDIR /go/src/ +COPY go.mod . +COPY go.sum . +RUN go mod download +COPY main.go . +RUN go build -v -o /usr/local/bin/config-function ./ + +FROM alpine:latest +COPY --from=schemas /kubernetes-json-schema/v1.16.0-standalone-strict /schemas/v1.16.0-standalone-strict +COPY --from=function /usr/local/bin/config-function /usr/local/bin/config-function +CMD ["config-function"] diff --git a/functions/examples/validator-kubeval/image/go.mod b/functions/examples/validator-kubeval/image/go.mod new file mode 100644 index 000000000..9cc966038 --- /dev/null +++ b/functions/examples/validator-kubeval/image/go.mod @@ -0,0 +1,8 @@ +module sigs.k8s.io/kustomize/functions/examples/validator-kubeval + +go 1.13 + +require ( + github.com/instrumenta/kubeval v0.0.0-20190918223246-8d013ec9fc56 + sigs.k8s.io/kustomize/kyaml v0.0.0-20191212230447-6309af43a718 +) diff --git a/functions/examples/validator-kubeval/image/go.sum b/functions/examples/validator-kubeval/image/go.sum new file mode 100644 index 000000000..83c84467c --- /dev/null +++ b/functions/examples/validator-kubeval/image/go.sum @@ -0,0 +1,68 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357 h1:Rem2+U35z1QtPQc6r+WolF7yXiefXqDKyk+lN2pE164= +github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0 h1:j30noezaCfvNLcdMYSvHLv81DxYRSt1grlpseG67vhU= +github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/instrumenta/kubeval v0.0.0-20190918223246-8d013ec9fc56 h1:kKOrEaxR9KvCDdnQqjiBxbaeJg/goLvJvW0lno6aWm4= +github.com/instrumenta/kubeval v0.0.0-20190918223246-8d013ec9fc56/go.mod h1:bpiMYvNpVxWjdJsS0hDRu9TrobT5GfWCZwJseGUstxE= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o= +github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v0.0.0-20180724185102-c2dbbc24a979/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/afero v1.1.1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/cobra v0.0.0-20180820174524-ff0d02e85550 h1:LB9SHuuXO8gnsHtexOQSpsJrrAHYA35lvHUaE74kznU= +github.com/spf13/cobra v0.0.0-20180820174524-ff0d02e85550/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20180821114517-d929dcbb1086 h1:iU+nPfqRqK8ShQqnpZLv8cZ9oklo6NFUcmX1JT5Rudg= +github.com/spf13/pflag v0.0.0-20180821114517-d929dcbb1086/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.1.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609 h1:BcMExZAULPkihVZ7UJXK7t8rwGqisXFw75tILnafhBY= +github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +golang.org/x/sys v0.0.0-20180821044426-4ea2f632f6e9 h1:0RHCP7KEw0rDuVXXaT2gfV77uu6lTKa5aItB+EoFbQk= +golang.org/x/sys v0.0.0-20180821044426-4ea2f632f6e9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.0.0-20180810153555-6e3c4e7365dd/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d h1:LCPbGQ34PMrwad11aMZ+dbz5SAsq/0ySjRwQ8I9Qwd8= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/kustomize/kyaml v0.0.0-20191212230447-6309af43a718 h1:Kbn9yTkj4eMwY/LQLXnwDm4eTLqQsaOL81iSsr/UhQU= +sigs.k8s.io/kustomize/kyaml v0.0.0-20191212230447-6309af43a718/go.mod h1:rywm/rcR5LmCBghz9956tE45OdUPChFoXVVs+WmhMTI= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/functions/examples/validator-kubeval/image/main.go b/functions/examples/validator-kubeval/image/main.go new file mode 100644 index 000000000..cc178c187 --- /dev/null +++ b/functions/examples/validator-kubeval/image/main.go @@ -0,0 +1,134 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Package main implements a validator function run by `kustomize config run` +package main + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/instrumenta/kubeval/kubeval" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func main() { + rw := &kio.ByteReadWriter{ + Reader: os.Stdin, + Writer: os.Stdout, + OmitReaderAnnotations: true, + KeepReaderAnnotations: true, + } + p := kio.Pipeline{ + Inputs: []kio.Reader{rw}, // read the inputs into a slice + Filters: []kio.Filter{kubevalFilter{rw: rw}}, + Outputs: []kio.Writer{rw}, // copy the inputs to the output + } + if err := p.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} + +// kubevalFilter implements kio.Filter +type kubevalFilter struct { + rw *kio.ByteReadWriter +} + +// define the input API schema as a struct +type API struct { + Spec struct { + // Strict disallows additional properties not in schema if set. + Strict bool `yaml:"strict"` + + // IgnoreMissingSchemas skips validation for resource + // definitions without a schema. + IgnoreMissingSchemas bool `yaml:"ignoreMissingSchemas"` + + // KubernetesVersion is the version of Kubernetes to validate + // against (default "master"). + KubernetesVersion string `yaml:"kubernetesVersion"` + + // SchemaLocation is the base URL used to download schemas. + SchemaLocation string `yaml:"schemaLocation"` + } `yaml:"spec"` +} + +// Filter checks each resource for validity, otherwise returning an error. +func (f kubevalFilter) Filter(in []*yaml.RNode) ([]*yaml.RNode, error) { + api := f.parseAPI() + config := kubeval.NewDefaultConfig() + config.Strict = api.Spec.Strict + config.IgnoreMissingSchemas = api.Spec.IgnoreMissingSchemas + config.KubernetesVersion = api.Spec.KubernetesVersion + config.SchemaLocation = api.Spec.SchemaLocation + + // validate each Resource + for _, r := range in { + if err := validate(r.MustString(), config); err != nil { + meta, merr := r.GetMeta() + if merr != nil { + return nil, merr + } + fmt.Fprintf( + os.Stderr, + "Resource invalid: (Kind: %s, Name: %s)\n", + meta.Kind, meta.Name, + ) + return nil, err + } + } + return in, nil +} + +func validate(r string, config *kubeval.Config) error { + results, err := kubeval.Validate([]byte(r), config) + if err != nil { + return err + } + + return checkResults(results) +} + +func checkResults(results []kubeval.ValidationResult) error { + if len(results) == 0 { + return nil + } + + errs := []string{} + for _, r := range results { + for _, e := range r.Errors { + // Workaround a bug where the + // "config.kubernetes.io/index" annotation value is a + // number (invalid), and is still set on the Resource + // regardless of OmitReaderAnnotations. + // TODO: Remove this once the above issues are + // resolved. + if e.String() == "metadata.annotations: Invalid type. Expected: [string,null], given: integer" { + continue + } + errs = append(errs, e.String()) + } + } + + if len(errs) > 0 { + return errors.New(strings.Join(errs, "\n")) + } + + return nil +} + +// parseAPI parses the functionConfig into an API struct. +func (f *kubevalFilter) parseAPI() API { + // parse the input function config -- TODO: simplify this + var api API + if err := yaml.Unmarshal([]byte(f.rw.FunctionConfig.MustString()), &api); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + return api +} diff --git a/functions/examples/validator-kubeval/local-resource/example-use.yaml b/functions/examples/validator-kubeval/local-resource/example-use.yaml new file mode 100644 index 000000000..3ec77bae7 --- /dev/null +++ b/functions/examples/validator-kubeval/local-resource/example-use.yaml @@ -0,0 +1,33 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: examples.config.kubernetes.io/v1beta1 +kind: Kubeval +metadata: + configFn: + container: + image: gcr.io/kustomize-functions/example-validator-kubeval:v0.1.0 +spec: + strict: true + ignoreMissingSchemas: true + + # TODO: Remove these once function container network/volumes features are + # stabilized. + # Relevant issues: + # - https://github.com/kubernetes-sigs/kustomize/issues/1901 + # - https://github.com/kubernetes-sigs/kustomize/issues/1902 + kubernetesVersion: "1.16.0" + schemaLocation: "file:///schemas" +--- +apiVersion: v1 +kind: Service +metadata: + name: svc +spec: + clusterIP: None + publishNotReadyAddresses: true + prots: # Fix this typo ("ports") to pass validation. + - port: 2380 + name: etcd-server-ssl + - port: 2379 + name: etcd-client-ssl