From 667a6698d957fcb185e0d838bce2516863985e99 Mon Sep 17 00:00:00 2001 From: Ken Sipe Date: Tue, 11 Feb 2020 14:04:12 -0600 Subject: [PATCH] initial project move from kudo into its own project Signed-off-by: Ken Sipe --- .gitignore | 53 +- .golangci.yml | 31 + Makefile | 71 ++ go.mod | 48 + go.sum | 636 ++++++++++ hack/boilerplate.go.txt | 14 + hack/install-golangcilint.sh | 9 + hack/run-integration-tests.sh | 19 + hack/update_codegen.sh | 36 + hack/verify-generate.sh | 25 + pkg/apis/apis.go | 29 + pkg/apis/testharness/v1beta1/doc.go | 21 + pkg/apis/testharness/v1beta1/test_types.go | 112 ++ .../v1beta1/zz_generated.deepcopy.go | 182 +++ pkg/test/case.go | 249 ++++ pkg/test/case_test.go | 299 +++++ pkg/test/harness.go | 438 +++++++ pkg/test/harness_integration_test.go | 23 + pkg/test/harness_test.go | 83 ++ pkg/test/kind.go | 102 ++ pkg/test/kind_integration_test.go | 100 ++ pkg/test/step.go | 477 ++++++++ pkg/test/step_integration_test.go | 343 ++++++ .../step_integration_test_data/00-apply.yaml | 4 + .../step_integration_test_data/00-assert.yaml | 4 + pkg/test/step_test.go | 323 +++++ pkg/test/test_data/cli-test/00-assert.yaml | 4 + .../test_data/cli-test/00-create-pod.yaml | 4 + pkg/test/test_data/cli-test/01-assert.yaml | 6 + pkg/test/test_data/cli-test/01-patch.yaml | 7 + .../test_data/cli-test/test_data/pod.yaml | 9 + pkg/test/test_data/crd-in-step/00-assert.yaml | 18 + pkg/test/test_data/crd-in-step/00-crd.yaml | 14 + pkg/test/test_data/crd-in-step/01-assert.yaml | 7 + .../crd-in-step/01-create-mycrd.yaml | 7 + .../test_data/create-or-update/00-assert.yaml | 17 + .../test_data/create-or-update/00-crd.yaml | 49 + .../test_data/create-or-update/01-assert.yaml | 47 + .../01-create-deployment.yaml | 21 + .../create-or-update/01-create-service.yaml | 11 + .../01-create-unknown-crd.yaml | 13 + .../test_data/create-or-update/02-assert.yaml | 47 + .../02-update-deployment.yaml | 6 + .../create-or-update/02-update-service.yaml | 11 + .../02-update-unknown-crd.yaml | 11 + .../test_data/delete-in-step/00-assert.yaml | 14 + .../test_data/delete-in-step/00-create.yaml | 18 + .../test_data/delete-in-step/01-assert.yaml | 22 + .../test_data/delete-in-step/01-create.yaml | 25 + pkg/test/test_data/list-pods/00-assert.yaml | 9 + pkg/test/test_data/list-pods/00-pod.yaml | 10 + .../test_data/with-overrides/00-assert.yaml | 6 + .../with-overrides/00-test-step.yaml | 14 + .../test_data/with-overrides/01-assert.yaml | 10 + .../with-overrides/01-test-assert.yaml | 16 + .../with-overrides/02-directory/assert.yaml | 6 + .../with-overrides/02-directory/pod.yaml | 8 + .../with-overrides/02-directory/pod2.yaml | 8 + .../test_data/with-overrides/03-assert.yaml | 6 + pkg/test/test_data/with-overrides/03-pod.yaml | 14 + .../test_data/with-overrides/03-pod2.yaml | 9 + pkg/test/test_data/with-overrides/README.md | 0 pkg/test/utils/docker.go | 16 + pkg/test/utils/kubernetes.go | 1089 +++++++++++++++++ pkg/test/utils/kubernetes_integration_test.go | 109 ++ pkg/test/utils/kubernetes_test.go | 419 +++++++ pkg/test/utils/logger.go | 48 + pkg/test/utils/subset.go | 91 ++ pkg/test/utils/subset_test.go | 126 ++ pkg/test/utils/testing.go | 89 ++ tools.go | 14 + 71 files changed, 6235 insertions(+), 1 deletion(-) create mode 100644 .golangci.yml create mode 100644 Makefile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/boilerplate.go.txt create mode 100755 hack/install-golangcilint.sh create mode 100755 hack/run-integration-tests.sh create mode 100755 hack/update_codegen.sh create mode 100755 hack/verify-generate.sh create mode 100644 pkg/apis/apis.go create mode 100644 pkg/apis/testharness/v1beta1/doc.go create mode 100644 pkg/apis/testharness/v1beta1/test_types.go create mode 100644 pkg/apis/testharness/v1beta1/zz_generated.deepcopy.go create mode 100644 pkg/test/case.go create mode 100644 pkg/test/case_test.go create mode 100644 pkg/test/harness.go create mode 100644 pkg/test/harness_integration_test.go create mode 100644 pkg/test/harness_test.go create mode 100644 pkg/test/kind.go create mode 100644 pkg/test/kind_integration_test.go create mode 100644 pkg/test/step.go create mode 100644 pkg/test/step_integration_test.go create mode 100644 pkg/test/step_integration_test_data/00-apply.yaml create mode 100644 pkg/test/step_integration_test_data/00-assert.yaml create mode 100644 pkg/test/step_test.go create mode 100644 pkg/test/test_data/cli-test/00-assert.yaml create mode 100644 pkg/test/test_data/cli-test/00-create-pod.yaml create mode 100644 pkg/test/test_data/cli-test/01-assert.yaml create mode 100644 pkg/test/test_data/cli-test/01-patch.yaml create mode 100644 pkg/test/test_data/cli-test/test_data/pod.yaml create mode 100644 pkg/test/test_data/crd-in-step/00-assert.yaml create mode 100644 pkg/test/test_data/crd-in-step/00-crd.yaml create mode 100644 pkg/test/test_data/crd-in-step/01-assert.yaml create mode 100644 pkg/test/test_data/crd-in-step/01-create-mycrd.yaml create mode 100644 pkg/test/test_data/create-or-update/00-assert.yaml create mode 100644 pkg/test/test_data/create-or-update/00-crd.yaml create mode 100644 pkg/test/test_data/create-or-update/01-assert.yaml create mode 100644 pkg/test/test_data/create-or-update/01-create-deployment.yaml create mode 100644 pkg/test/test_data/create-or-update/01-create-service.yaml create mode 100644 pkg/test/test_data/create-or-update/01-create-unknown-crd.yaml create mode 100644 pkg/test/test_data/create-or-update/02-assert.yaml create mode 100644 pkg/test/test_data/create-or-update/02-update-deployment.yaml create mode 100644 pkg/test/test_data/create-or-update/02-update-service.yaml create mode 100644 pkg/test/test_data/create-or-update/02-update-unknown-crd.yaml create mode 100644 pkg/test/test_data/delete-in-step/00-assert.yaml create mode 100644 pkg/test/test_data/delete-in-step/00-create.yaml create mode 100644 pkg/test/test_data/delete-in-step/01-assert.yaml create mode 100644 pkg/test/test_data/delete-in-step/01-create.yaml create mode 100644 pkg/test/test_data/list-pods/00-assert.yaml create mode 100644 pkg/test/test_data/list-pods/00-pod.yaml create mode 100644 pkg/test/test_data/with-overrides/00-assert.yaml create mode 100644 pkg/test/test_data/with-overrides/00-test-step.yaml create mode 100644 pkg/test/test_data/with-overrides/01-assert.yaml create mode 100644 pkg/test/test_data/with-overrides/01-test-assert.yaml create mode 100644 pkg/test/test_data/with-overrides/02-directory/assert.yaml create mode 100644 pkg/test/test_data/with-overrides/02-directory/pod.yaml create mode 100644 pkg/test/test_data/with-overrides/02-directory/pod2.yaml create mode 100644 pkg/test/test_data/with-overrides/03-assert.yaml create mode 100644 pkg/test/test_data/with-overrides/03-pod.yaml create mode 100644 pkg/test/test_data/with-overrides/03-pod2.yaml create mode 100644 pkg/test/test_data/with-overrides/README.md create mode 100644 pkg/test/utils/docker.go create mode 100644 pkg/test/utils/kubernetes.go create mode 100644 pkg/test/utils/kubernetes_integration_test.go create mode 100644 pkg/test/utils/kubernetes_test.go create mode 100644 pkg/test/utils/logger.go create mode 100644 pkg/test/utils/subset.go create mode 100644 pkg/test/utils/subset_test.go create mode 100644 pkg/test/utils/testing.go create mode 100644 tools.go diff --git a/.gitignore b/.gitignore index f1c181ec..8c54605d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,15 @@ +### Project specific +bin/ +vendor/ +hack/code-gen +hack/controller-gen +test/.git-credentials +reports/ +config/manager_image_patch.yaml-e + +### mac +.DS_Store + # Binaries for programs and plugins *.exe *.exe~ @@ -5,8 +17,47 @@ *.so *.dylib -# Test binary, build with `go test -c` + +### Go tooling +# Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out + + +### Goland IDEA +.idea/ + +### gorelease +dist/ + +### VS Code +.vscode/ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + + +### vim +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ +kubeconfig +.kube diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..bcc3d3e2 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,31 @@ +linters: + auto-fix: false + enable: + - errcheck + - goimports + - golint + - gosec + - misspell + - scopelint + - unconvert + - unparam + - interfacer + - nakedret + - gocyclo + - dupl + - goconst + - lll +run: + skip-dirs: + # autogenerated clientset by client-gen + - pkg/client +linters-settings: + errcheck: + check-type-assertions: true + lll: + line-length: 250 + dupl: + threshold: 400 + goimports: + # Don't use 'github.com/kudobuilder/kudo', it'll result in unreliable output! + local-prefixes: github.com/kudobuilder diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..5ce5df28 --- /dev/null +++ b/Makefile @@ -0,0 +1,71 @@ +SHELL=/bin/bash -o pipefail + +export GO111MODULE=on + +.PHONY: all +all: test + +# Run unit tests +.PHONY: test +test: +ifdef _INTELLIJ_FORCE_SET_GOFLAGS +# Run tests from a Goland terminal. Goland already set '-mod=readonly' + go test ./pkg/... -v -coverprofile cover.out +else + go test ./pkg/... -v -mod=readonly -coverprofile cover.out +endif + +.PHONY: integration-test +# Run integration tests +integration-test: + ./hack/run-integration-tests.sh + + +.PHONY: lint +lint: +ifeq (, $(shell which golangci-lint)) + ./hack/install-golangcilint.sh +endif + golangci-lint run + +.PHONY: download +download: + go mod download + +.PHONY: prebuild +prebuild: generate lint + +.PHONY: generate +# Generate code +generate: +ifeq (, $(shell which controller-gen)) + go get sigs.k8s.io/controller-tools/cmd/controller-gen@$$(go list -f '{{.Version}}' -m sigs.k8s.io/controller-tools) +endif + controller-gen crd paths=./pkg/apis/... output:crd:dir=config/crds output:stdout + ./hack/update_codegen.sh + +.PHONY: generate-clean +generate-clean: + rm -rf hack/code-gen + +.PHONY: clean +# Clean all +clean: test-clean + +.PHONY: imports +# used to update imports on project. NOT a linter. +imports: +ifeq (, $(shell which golangci-lint)) + ./hack/install-golangcilint.sh +endif + golangci-lint run --disable-all -E goimports --fix + +.PHONY: todo +# Show to-do items per file. +todo: + @grep \ + --exclude-dir=hack \ + --exclude=Makefile \ + --text \ + --color \ + -nRo -E ' TODO:.*|SkipNow' . diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..24b1fa35 --- /dev/null +++ b/go.mod @@ -0,0 +1,48 @@ +module github.com/kudobuilder/kuttl + +go 1.13 + +require ( + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/containerd/containerd v1.2.9 // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/docker v1.4.2-0.20190916154449-92cc603036dd + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 + github.com/go-bindata/go-bindata v3.1.2+incompatible + github.com/gogo/protobuf v1.3.1 // indirect + github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/gorilla/context v1.1.1 // indirect + github.com/gorilla/mux v1.6.2 // indirect + github.com/imdario/mergo v0.3.7 // indirect + github.com/json-iterator/go v1.1.8 // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/onsi/ginkgo v1.10.1 // indirect + github.com/onsi/gomega v1.7.1 // indirect + github.com/opencontainers/go-digest v1.0.0-rc1 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 + github.com/prometheus/client_golang v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.4.0 + github.com/thoas/go-funk v0.5.0 + go.uber.org/atomic v1.4.0 // indirect + go.uber.org/zap v1.10.0 // indirect + golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 // indirect + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect + golang.org/x/tools v0.0.0-20191025023517-2077df36852e // indirect + gopkg.in/yaml.v2 v2.2.7 + k8s.io/api v0.16.6 + k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783 + k8s.io/apimachinery v0.16.6 + k8s.io/client-go v0.16.6 + k8s.io/code-generator v0.16.6 + k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a // indirect + sigs.k8s.io/controller-runtime v0.4.0 + sigs.k8s.io/controller-tools v0.2.4 + sigs.k8s.io/kind v0.6.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..75a65c85 --- /dev/null +++ b/go.sum @@ -0,0 +1,636 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0 h1:Kx+AUU2Te+A3JIyYn6Dfs+cFgx5XorQKuIXrZGoq/SI= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0 h1:Ww5g4zThfD/6cLb4z6xxgeyDa7QDkizMkJKe0ysZXp0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 h1:lsxEuwrXEAokXB9qhlbKWPpo3KMLZQ5WB5WLQRW1uq0= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alessio/shellescape v0.0.0-20190409004728-b115ca0f9053 h1:H/GMMKYPkEIC3DF/JWQz8Pdd+Feifov2EIgGfNpeogI= +github.com/alessio/shellescape v0.0.0-20190409004728-b115ca0f9053/go.mod h1:xW8sBma2LE3QxFSzCnH9qe6gAE2yO9GvQaWwX89HxbE= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/blang/semver v3.5.0+incompatible h1:CGxCgetQ64DKk7rdZ++Vfnb1+ogGNnB17OJKJXD2Cfs= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/containerd v1.2.9 h1:6tyNjBmAMG47QuFPIT9LgiiexoVxC6qpTGR+eD0R0Z8= +github.com/containerd/containerd v1.2.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/coreos/bbolt v1.3.1-coreos.6 h1:uTXKg9gY70s9jMAKdfljFQcuh4e/BXOM+V+d00KFj3A= +github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.15+incompatible h1:+9RjdC18gMxNQVvSiXvObLu29mOFmkgdsB4cRTlV+EE= +github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea h1:n2Ltr3SrfQlf/9nOna1DoGKxLx3qTSI8Ttl6Xrqp6mw= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v1.4.2-0.20190916154449-92cc603036dd h1:kDIT0qjvLHbdL86aa+VteVpVZOR7coIyIejM/o3CwOo= +github.com/docker/docker v1.4.2-0.20190916154449-92cc603036dd/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY= +github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE= +github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/zapr v0.1.0 h1:h+WVe9j6HAA01niTJPA/kKH0i7e0rLZBCwauQFcRE54= +github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2 h1:ophLETFestFZHk3ji7niPEL4d466QjW+0Tdg5VyDq7E= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2 h1:a2kIyV3w+OS3S97zxUndRVD46+FhGOUBDFY7nmu4CsY= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2 h1:A9+F4Dc/MCNB5jibxf6rRvOvR/iFgQdyNx9eIhnGqq0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2 h1:rf5ArTHmIJxyV5Oiks+Su0mUens1+AjpkPoWr5xFRcI= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0 h1:sU6pp4dSV2sGlNKKyHxZzi1m1kG4WnYtWcJ+HYbygjE= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2 h1:SStNd1jRcYtfKCN7R0laGNs80WYYvn5CbBjM2sOmCrE= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0 h1:0Dn9qy1G9+UJfRU7TR8bmdGxb4uifB7HNrJjOnV0yPk= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2 h1:jvO6bCMBEilGwMfHhrd61zIID4oIFdwb76V17SM88dE= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2 h1:ky5l57HjyVRrsJfd2+Ro5Z9PjGuKbsmftwyMtk8H7js= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/flect v0.1.5 h1:xpKq9ap8MbYfhuPCF0dBH854Gp9CxZjr/IocxELFflo= +github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 h1:u4bArs140e9+AfE52mFHOXVFnOSBJBRlzTHrOPLOIhE= +github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS0VQKc= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk= +github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= +github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7 h1:6TSoaYExHper8PYsJu23GWVNOyYRCSnIFyxKgLSZ54w= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79 h1:lR9ssWAqp9qL0bALxqEEkuudiP1eweOdv9jsRK3e7lE= +github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.3.0 h1:HJtP6RRwj2EpPCD/mhAWzSvLL/dFTdPm1UrWwanoFos= +github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= +github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +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/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +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 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.4.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.3.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.3 h1:09wy7WZk4AqO03yH85Ex1X+Uo3vDsil3Fa9AgF8Emss= +github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +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/thoas/go-funk v0.5.0 h1:XXFUVqX6xnIDqXxENFHBFS1X5AoT0EDs7HJq2krRfD8= +github.com/thoas/go-funk v0.5.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 h1:MPPkRncZLN9Kh4MEFmbnK4h3BD7AUmskWv2+EeZJCCs= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180112015858-5ccada7d0a7b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 h1:rOhMmluY6kLMhdnrivzec6lLgaVbMHMn2ISQXJeJ5EM= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd h1:3x5uuvBgE6oaXJjCOvpCC1IpgJogqQ+PqGGU3ZxAgII= +golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138 h1:H3uGjxCR/6Ds0Mjgyp7LMK81+LvmbvWWEnJhzk1Pi9E= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191025023517-2077df36852e h1:ejUPpxsbZzyShOEURCSvFIT0ltnmBW92Vsc3i8QRcw8= +golang.org/x/tools v0.0.0-20191025023517-2077df36852e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.0.1 h1:xyiBuvkD2g5n7cYzx6u2sxQvsAy4QJsZFCzGVdzOXZ0= +gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485 h1:OB/uP/Puiu5vS5QMRPrXCDWUPb+kt8f1KW8oQzFejQw= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= +google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +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.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966 h1:B0J02caTR6tpSJozBJyiAzT6CtBzjclw4pgm9gg8Ys0= +gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20191106092431-e228e37189d3 h1:ho4SukHOmqjp7XHH7nPNx7GcgDK6ObVflhAQAT7MvpE= +gopkg.in/yaml.v3 v3.0.0-20191106092431-e228e37189d3/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a h1:/8zB6iBfHCl1qAnEAWwGPNrUvapuy6CPla1VM0k8hQw= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.0.0-20190918155943-95b840bb6a1f h1:8FRUST8oUkEI45WYKyD8ed7Ad0Kg5v11zHyPkEVb2xo= +k8s.io/api v0.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48= +k8s.io/api v0.16.6 h1:FyTv/Z4RBlddLGJXRjGXE40QtYGHOjdZKRFSW5rU1oE= +k8s.io/api v0.16.6/go.mod h1:naJcEPKsa3oqutLPPMxA2oLSqV4KxGDLU6IgkqHqgFE= +k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783 h1:V6ndwCPoao1yZ52agqOKaUAl7DYWVGiXjV7ePA2i610= +k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783/go.mod h1:xvae1SZB3E17UpV59AWc271W/Ph25N+bjPyR63X6tPY= +k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655 h1:CS1tBQz3HOXiseWZu6ZicKX361CZLT97UFnnPx0aqBw= +k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= +k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb h1:ZUNsbuPdXWrj0rZziRfCWcFg9ZP31OKkziqCbiphznI= +k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ= +k8s.io/apimachinery v0.16.6 h1:A4RWH7F3jhkD6YyBpsm/2yXiFUEf9fiNqrz7bxZHPMc= +k8s.io/apimachinery v0.16.6/go.mod h1:mhhO3hoLkWO+2eCvqjPtH2Ly92l9nJDwsswzWKpkN2w= +k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= +k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53q3Uz5OSfgsv4uxpScmmyYOOlk= +k8s.io/client-go v0.16.6 h1:OR6ZaSlIn9dUdpiN4r5mAvMv5aCupUJUiDJZdrrvmhw= +k8s.io/client-go v0.16.6/go.mod h1:xIQ44uaAH4SD1EHMtCHsB9By7D0qblbv1ADeGyXpZUQ= +k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE= +k8s.io/code-generator v0.16.6 h1:wykZVkEDQd/BcOZxcQMEW6uFgoM4ZmvjIhxkbmEJ3WA= +k8s.io/code-generator v0.16.6/go.mod h1:2aiDuxDU7RQK2PVypXAXHo6+YwOlF33iezHQbSmKSA4= +k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090/go.mod h1:933PBGtQFJky3TEwYx4aEPZ4IxqhWh3R6DCmzqIn1hA= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6 h1:4s3/R4+OYYYUKptXPhZKjQ04WJ6EhQQVFdjOFvCazDk= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190822140433-26a664648505 h1:ZY6yclUKVbZ+SdWnkfY+Je5vrMpKOxmGeKRbsXVmqYM= +k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0 h1:0VPpR+sizsiivjIfIAQH/rl8tan6jvWkS7lU+0di3lE= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ= +k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf h1:EYm5AW/UUDbnmnI+gK0TJDVK9qPLhM+sRHYanNKw0EQ= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +sigs.k8s.io/controller-runtime v0.4.0 h1:wATM6/m+3w8lj8FXNaO6Fs/rq/vqoOjO1Q116Z9NPsg= +sigs.k8s.io/controller-runtime v0.4.0/go.mod h1:ApC79lpY3PHW9xj/w9pj+lYkLgwAAUZwfXkME1Lajns= +sigs.k8s.io/controller-tools v0.2.4 h1:la1h46EzElvWefWLqfsXrnsO3lZjpkI0asTpX6h8PLA= +sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA= +sigs.k8s.io/kind v0.6.1 h1:13zGO85bX34eyJFQWilTWyuqncRAhCaHoffF9vPuYbg= +sigs.k8s.io/kind v0.6.1/go.mod h1:dhW5h0O4PRVq8B6eExphIa9mBnrlBzxEz3R/P1YcYj0= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca h1:6dsH6AYQWbyZmtttJNe8Gq1cXOeS1BdV3eW37zHilAQ= +sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA= +sigs.k8s.io/testing_frameworks v0.1.2 h1:vK0+tvjF0BZ/RYFeZ1E6BYBwHJJXhjuZ3TdsEKH+UQM= +sigs.k8s.io/testing_frameworks v0.1.2/go.mod h1:ToQrwSC3s8Xf/lADdZp3Mktcql9CG0UAmdJG9th5i0w= +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/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 00000000..b92001fb --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,14 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ \ No newline at end of file diff --git a/hack/install-golangcilint.sh b/hack/install-golangcilint.sh new file mode 100755 index 00000000..63ace553 --- /dev/null +++ b/hack/install-golangcilint.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +GOLANGCILINT_VERSION=${GOLANGCILINT_VERSION:-v1.23.1} + +curl -sSfL "https://raw.githubusercontent.com/golangci/golangci-lint/${GOLANGCILINT_VERSION}/install.sh" | sh -s -- -b "$(go env GOPATH)/bin" "${GOLANGCILINT_VERSION}" diff --git a/hack/run-integration-tests.sh b/hack/run-integration-tests.sh new file mode 100755 index 00000000..7cf1a4fa --- /dev/null +++ b/hack/run-integration-tests.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail +set -o xtrace + +INTEGRATION_OUTPUT_JUNIT=${INTEGRATION_OUTPUT_JUNIT:-false} + +if [ "$INTEGRATION_OUTPUT_JUNIT" == true ] +then + echo "Running integration tests with junit output" + mkdir -p reports/ + go get github.com/jstemmer/go-junit-report + go test -tags integration ./pkg/... -v -mod=readonly -coverprofile cover-integration.out 2>&1 |tee /dev/fd/2 |go-junit-report -set-exit-code > reports/integration_report.xml +else + echo "Running integration tests without junit output" + go test -tags integration ./pkg/... -v -mod=readonly -coverprofile cover-integration.out +fi diff --git a/hack/update_codegen.sh b/hack/update_codegen.sh new file mode 100755 index 00000000..a014c5ff --- /dev/null +++ b/hack/update_codegen.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +# The following solution for making code generation work with go modules is +# borrowed and modified from https://github.com/heptio/contour/pull/1010. +# it has been modified to enable caching. +export GO111MODULE=on +VERSION=$(go list -m all | grep k8s.io/code-generator | rev | cut -d"-" -f1 | cut -d" " -f1 | rev) +REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel)}" +CODE_GEN_DIR="${REPO_ROOT}/hack/code-gen/$VERSION" + +if [[ -d ${CODE_GEN_DIR} ]]; then + echo "Using cached code generator version: $VERSION" +else + git clone https://github.com/kubernetes/code-generator.git "${CODE_GEN_DIR}" + git -C "${CODE_GEN_DIR}" reset --hard "${VERSION}" +fi + +# Fake being in a $GOPATH until kubernetes fully supports modules +# https://github.com/kudobuilder/kudo/issues/1252 +FAKE_GOPATH="$(mktemp -d)" +trap 'chmod -R u+rwX ${FAKE_GOPATH} && rm -rf ${FAKE_GOPATH}' EXIT +FAKE_REPOPATH="${FAKE_GOPATH}/src/github.com/kudobuilder/kuttl" +mkdir -p "$(dirname "${FAKE_REPOPATH}")" && ln -s "${REPO_ROOT}" "${FAKE_REPOPATH}" +export GOPATH="${FAKE_GOPATH}" +cd "${FAKE_REPOPATH}" + +"${CODE_GEN_DIR}"/generate-groups.sh \ + deepcopy \ + github.com/kudobuilder/kuttl/pkg/client \ + github.com/kudobuilder/kuttl/pkg/apis \ + "testharness:v1beta1" \ + --go-header-file hack/boilerplate.go.txt # must be last for some reason diff --git a/hack/verify-generate.sh b/hack/verify-generate.sh new file mode 100755 index 00000000..a3190eab --- /dev/null +++ b/hack/verify-generate.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -o nounset +set -o pipefail +# intentionally not setting 'set -o errexit' because we want to print custom error messages + +# make sure make generate can be invoked +make generate +RETVAL=$? +if [[ ${RETVAL} != 0 ]]; then + echo "Invoking 'make generate' ends with non-zero exit code." + exit 1 +fi + +git diff --exit-code --quiet +RETVAL=$? + +if [[ ${RETVAL} != 0 ]]; then + echo "Running 'make generate' produces changes to the current git status. Maybe you forgot to check-in your updated generated files?" + echo "The current diff: `git diff`" + exit 1 +fi + +echo "Verifying 'make generate' was successful! ヽ(•‿•)ノ" +exit 0 \ No newline at end of file diff --git a/pkg/apis/apis.go b/pkg/apis/apis.go new file mode 100644 index 00000000..f90af050 --- /dev/null +++ b/pkg/apis/apis.go @@ -0,0 +1,29 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package apis contains Kubernetes API groups. +package apis + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// AddToSchemes may be used to add all resources defined in the project to a Scheme +var AddToSchemes runtime.SchemeBuilder + +// AddToScheme adds all Resources to the Scheme +func AddToScheme(s *runtime.Scheme) error { + return AddToSchemes.AddToScheme(s) +} diff --git a/pkg/apis/testharness/v1beta1/doc.go b/pkg/apis/testharness/v1beta1/doc.go new file mode 100644 index 00000000..4e615bae --- /dev/null +++ b/pkg/apis/testharness/v1beta1/doc.go @@ -0,0 +1,21 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta1 contains API Schema definitions for the testharness API group +// +k8s:openapi-gen=false +// +k8s:deepcopy-gen=package +// +groupName=kudo.dev +// +kubebuilder:skip +package v1beta1 diff --git a/pkg/apis/testharness/v1beta1/test_types.go b/pkg/apis/testharness/v1beta1/test_types.go new file mode 100644 index 00000000..5340b236 --- /dev/null +++ b/pkg/apis/testharness/v1beta1/test_types.go @@ -0,0 +1,112 @@ +package v1beta1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TestSuite configures which tests should be loaded. +type TestSuite struct { + // The type meta object, should always be a GVK of kudo.dev/v1beta1/TestSuite. + metav1.TypeMeta `json:",inline"` + // Set labels or the test suite name. + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Path to CRDs to install before running tests. + CRDDir string `json:"crdDir"` + // Paths to directories containing manifests to install before running tests. + ManifestDirs []string `json:"manifestDirs"` + // Directories containing test cases to run. + TestDirs []string `json:"testDirs"` + // Whether or not to start a local etcd and kubernetes API server for the tests. + StartControlPlane bool `json:"startControlPlane"` + // Whether or not to start a local kind cluster for the tests. + StartKIND bool `json:"startKIND"` + // Path to the KIND configuration file to use. + KINDConfig string `json:"kindConfig"` + // KIND context to use. + KINDContext string `json:"kindContext"` + // If set, each node defined in the kind configuration will have a docker named volume mounted into it to persist + // pulled container images across test runs. + KINDNodeCache bool `json:"kindNodeCache"` + // Containers to load to each KIND node prior to running the tests. + KINDContainers []string `json:"kindContainers"` + // Whether or not to start the KUDO controller for the tests. + StartKUDO bool `json:"startKUDO"` + // If set, do not delete the resources after running the tests (implies SkipClusterDelete). + SkipDelete bool `json:"skipDelete"` + // If set, do not delete the mocked control plane or kind cluster. + SkipClusterDelete bool `json:"skipClusterDelete"` + // Override the default timeout of 30 seconds (in seconds). + // +kubebuilder:validation:Format:=int64 + Timeout int `json:"timeout"` + // The maximum number of tests to run at once (default: 8). + // +kubebuilder:validation:Format:=int64 + Parallel int `json:"parallel"` + // The directory to output artifacts to (current working directory if not specified). + ArtifactsDir string `json:"artifactsDir"` + // Kubectl commands to run before running any tests. + Kubectl []string `json:"kubectl"` + // Commands to run prior to running the tests. + Commands []Command `json:"commands"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TestStep settings to apply to a test step. +type TestStep struct { + // The type meta object, should always be a GVK of kudo.dev/v1beta1/TestStep. + metav1.TypeMeta `json:",inline"` + // Override the default metadata. Set labels or override the test step name. + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +kubebuilder:validation:Format:=int64 + Index int `json:"index,omitempty"` + // Objects to delete at the beginning of the test step. + Delete []ObjectReference `json:"delete,omitempty"` + + // Indicates that this is a unit test - safe to run without a real Kubernetes cluster. + UnitTest bool `json:"unitTest"` + + // Kubectl commands to run at the start of the test + Kubectl []string `json:"kubectl"` + + // Commands to run prior at the beginning of the test step. + Commands []Command `json:"commands"` + + // Allowed environment labels + // Disallowed environment labels +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TestAssert represents the settings needed to verify the result of a test step. +type TestAssert struct { + // The type meta object, should always be a GVK of kudo.dev/v1beta1/TestAssert. + metav1.TypeMeta `json:",inline"` + // Override the default timeout of 30 seconds (in seconds). + Timeout int `json:"timeout"` +} + +// ObjectReference is a Kubernetes object reference with added labels to allow referencing +// objects by label. +type ObjectReference struct { + corev1.ObjectReference `json:",inline"` + // Labels to match on. + Labels map[string]string `json:"labels"` +} + +// Command describes a command to run as a part of a test step or suite. +type Command struct { + // The command and argument to run as a string. + Command string `json:"command"` + // If set, the `--namespace` flag will be appended to the command with the namespace to use. + Namespaced bool `json:"namespaced"` + // If set, failures will be ignored. + IgnoreFailure bool `json:"ignoreFailure"` +} + +// DefaultKINDContext defines the default kind context to use. +const DefaultKINDContext = "kind" diff --git a/pkg/apis/testharness/v1beta1/zz_generated.deepcopy.go b/pkg/apis/testharness/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 00000000..72b2f857 --- /dev/null +++ b/pkg/apis/testharness/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,182 @@ +// +build !ignore_autogenerated + +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1beta1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Command) DeepCopyInto(out *Command) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Command. +func (in *Command) DeepCopy() *Command { + if in == nil { + return nil + } + out := new(Command) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectReference) DeepCopyInto(out *ObjectReference) { + *out = *in + out.ObjectReference = in.ObjectReference + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectReference. +func (in *ObjectReference) DeepCopy() *ObjectReference { + if in == nil { + return nil + } + out := new(ObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestAssert) DeepCopyInto(out *TestAssert) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestAssert. +func (in *TestAssert) DeepCopy() *TestAssert { + if in == nil { + return nil + } + out := new(TestAssert) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestAssert) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestStep) DeepCopyInto(out *TestStep) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Delete != nil { + in, out := &in.Delete, &out.Delete + *out = make([]ObjectReference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Kubectl != nil { + in, out := &in.Kubectl, &out.Kubectl + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Commands != nil { + in, out := &in.Commands, &out.Commands + *out = make([]Command, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestStep. +func (in *TestStep) DeepCopy() *TestStep { + if in == nil { + return nil + } + out := new(TestStep) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestStep) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestSuite) DeepCopyInto(out *TestSuite) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.ManifestDirs != nil { + in, out := &in.ManifestDirs, &out.ManifestDirs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TestDirs != nil { + in, out := &in.TestDirs, &out.TestDirs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.KINDContainers != nil { + in, out := &in.KINDContainers, &out.KINDContainers + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Kubectl != nil { + in, out := &in.Kubectl, &out.Kubectl + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Commands != nil { + in, out := &in.Commands, &out.Commands + *out = make([]Command, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestSuite. +func (in *TestSuite) DeepCopy() *TestSuite { + if in == nil { + return nil + } + out := new(TestSuite) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestSuite) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/pkg/test/case.go b/pkg/test/case.go new file mode 100644 index 00000000..8edc38da --- /dev/null +++ b/pkg/test/case.go @@ -0,0 +1,249 @@ +package test + +import ( + "context" + "fmt" + "io/ioutil" + "path/filepath" + "regexp" + "sort" + "strconv" + "testing" + + petname "github.com/dustinkirkland/golang-petname" + corev1 "k8s.io/api/core/v1" + eventsbeta1 "k8s.io/api/events/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/discovery" + "sigs.k8s.io/controller-runtime/pkg/client" + + testutils "github.com/kudobuilder/kuttl/pkg/test/utils" +) + +var testStepRegex = regexp.MustCompile(`^(\d+)-([^.]+)(.yaml)?$`) + +// Case contains all of the test steps and the Kubernetes client and other global configuration +// for a test. +type Case struct { + Steps []*Step + Name string + Dir string + SkipDelete bool + Timeout int + + Client func(forceNew bool) (client.Client, error) + DiscoveryClient func() (discovery.DiscoveryInterface, error) + + Logger testutils.Logger +} + +// DeleteNamespace deletes a namespace in Kubernetes after we are done using it. +func (t *Case) DeleteNamespace(namespace string) error { + t.Logger.Log("Deleting namespace:", namespace) + + cl, err := t.Client(false) + if err != nil { + return err + } + + return cl.Delete(context.TODO(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + }, + }) +} + +// CreateNamespace creates a namespace in Kubernetes to use for a test. +func (t *Case) CreateNamespace(namespace string) error { + t.Logger.Log("Creating namespace:", namespace) + + cl, err := t.Client(false) + if err != nil { + return err + } + + return cl.Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + }, + }) +} + +// byFirstTimestamp sorts a slice of events by first timestamp, using their involvedObject's name as a tie breaker. +type byFirstTimestamp []eventsbeta1.Event + +func (o byFirstTimestamp) Len() int { return len(o) } +func (o byFirstTimestamp) Swap(i, j int) { o[i], o[j] = o[j], o[i] } + +func (o byFirstTimestamp) Less(i, j int) bool { + if o[i].ObjectMeta.CreationTimestamp.Equal(&o[j].ObjectMeta.CreationTimestamp) { + return o[i].Name < o[j].Name + } + return o[i].ObjectMeta.CreationTimestamp.Before(&o[j].ObjectMeta.CreationTimestamp) +} + +// CollectEvents gathers all events from namespace and prints it out to log +func (t *Case) CollectEvents(namespace string) { + cl, err := t.Client(false) + if err != nil { + t.Logger.Log("Failed to collect events for %s in ns %s: %v", t.Name, namespace, err) + return + } + + eventsList := &eventsbeta1.EventList{} + + err = cl.List(context.TODO(), eventsList, client.InNamespace(namespace)) + if err != nil { + t.Logger.Logf("Failed to collect events for %s in ns %s: %v", t.Name, namespace, err) + return + } + + events := eventsList.Items + sort.Sort(byFirstTimestamp(events)) + + t.Logger.Logf("%s events from ns %s:", t.Name, namespace) + printEvents(events, t.Logger) +} + +func printEvents(events []eventsbeta1.Event, logger conversion.DebugLogger) { + for _, e := range events { + // time type reason kind message + logger.Logf("%s\t%s\t%s\t%s", e.ObjectMeta.CreationTimestamp, e.Type, e.Reason, e.Note) + } +} + +// Run runs a test case including all of its steps. +func (t *Case) Run(test *testing.T) { + test.Parallel() + + ns := fmt.Sprintf("kudo-test-%s", petname.Generate(2, "-")) + + if err := t.CreateNamespace(ns); err != nil { + test.Fatal(err) + } + + if !t.SkipDelete { + defer func() { + if err := t.DeleteNamespace(ns); err != nil { + test.Error(err) + } + }() + } + + for _, testStep := range t.Steps { + testStep.Client = t.Client + testStep.DiscoveryClient = t.DiscoveryClient + testStep.Logger = t.Logger.WithPrefix(testStep.String()) + + if !t.SkipDelete { + defer func() { + if err := testStep.Clean(ns); err != nil { + test.Error(err) + } + }() + } + + if errs := testStep.Run(ns); len(errs) > 0 { + for _, err := range errs { + test.Error(err) + } + + test.Error(fmt.Errorf("failed in step %s", testStep.String())) + break + } + } + + t.CollectEvents(ns) +} + +// CollectTestStepFiles collects a map of test steps and their associated files +// from a directory. +func (t *Case) CollectTestStepFiles() (map[int64][]string, error) { + testStepFiles := map[int64][]string{} + + files, err := ioutil.ReadDir(t.Dir) + if err != nil { + return nil, err + } + + for _, file := range files { + matches := testStepRegex.FindStringSubmatch(file.Name()) + + if len(matches) < 2 { + t.Logger.Log("Ignoring", file.Name(), "as it does not match file name regexp:", testStepRegex.String()) + continue + } + + index, err := strconv.ParseInt(matches[1], 10, 32) + if err != nil { + return nil, err + } + + if testStepFiles[index] == nil { + testStepFiles[index] = []string{} + } + + testStepPath := filepath.Join(t.Dir, file.Name()) + + if file.IsDir() { + testStepDir, err := ioutil.ReadDir(testStepPath) + if err != nil { + return nil, err + } + + for _, testStepFile := range testStepDir { + testStepFiles[index] = append(testStepFiles[index], filepath.Join( + testStepPath, testStepFile.Name(), + )) + } + } else { + testStepFiles[index] = append(testStepFiles[index], testStepPath) + } + } + + return testStepFiles, nil +} + +// LoadTestSteps loads all of the test steps for a test case. +func (t *Case) LoadTestSteps() error { + testStepFiles, err := t.CollectTestStepFiles() + if err != nil { + return err + } + + testSteps := []*Step{} + + for index, files := range testStepFiles { + testStep := &Step{ + Timeout: t.Timeout, + Index: int(index), + Dir: t.Dir, + Asserts: []runtime.Object{}, + Apply: []runtime.Object{}, + Errors: []runtime.Object{}, + } + + for _, file := range files { + if err := testStep.LoadYAML(file); err != nil { + return err + } + } + + testSteps = append(testSteps, testStep) + } + + sort.Slice(testSteps, func(i, j int) bool { + return testSteps[i].Index < testSteps[j].Index + }) + + t.Steps = testSteps + return nil +} diff --git a/pkg/test/case_test.go b/pkg/test/case_test.go new file mode 100644 index 00000000..4b31de75 --- /dev/null +++ b/pkg/test/case_test.go @@ -0,0 +1,299 @@ +package test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + harness "github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1" + testutils "github.com/kudobuilder/kuttl/pkg/test/utils" +) + +// Verify the test state as loaded from disk. +// Each test provides a path to a set of test steps and their rendered result. +func TestLoadTestSteps(t *testing.T) { + for _, tt := range []struct { + path string + testSteps []Step + }{ + { + "test_data/with-overrides/", + []Step{ + { + Name: "with-test-step-name-override", + Index: 0, + Step: &harness.TestStep{ + ObjectMeta: metav1.ObjectMeta{ + Name: "with-test-step-name-override", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "TestStep", + APIVersion: "kudo.dev/v1beta1", + }, + Index: 0, + }, + Apply: []runtime.Object{ + testutils.WithSpec(t, testutils.NewPod("test", ""), map[string]interface{}{ + "restartPolicy": "Never", + "containers": []map[string]interface{}{ + { + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }), + }, + Asserts: []runtime.Object{ + testutils.WithStatus(t, testutils.NewPod("test", ""), map[string]interface{}{ + "qosClass": "BestEffort", + }), + }, + Errors: []runtime.Object{}, + }, + { + Name: "test-assert", + Index: 1, + Step: &harness.TestStep{ + TypeMeta: metav1.TypeMeta{ + Kind: "TestStep", + APIVersion: "kudo.dev/v1beta1", + }, + Index: 1, + Delete: []harness.ObjectReference{ + { + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Pod", + Name: "test", + }, + }, + }, + }, + Assert: &harness.TestAssert{ + TypeMeta: metav1.TypeMeta{ + Kind: "TestAssert", + APIVersion: "kudo.dev/v1beta1", + }, + Timeout: 20, + }, + Apply: []runtime.Object{ + testutils.WithSpec(t, testutils.NewPod("test2", ""), map[string]interface{}{ + "restartPolicy": "Never", + "containers": []map[string]interface{}{ + { + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }), + }, + Asserts: []runtime.Object{ + testutils.WithStatus(t, testutils.NewPod("test2", ""), map[string]interface{}{ + "qosClass": "BestEffort", + }), + }, + Errors: []runtime.Object{}, + }, + { + Name: "pod", + Index: 2, + Apply: []runtime.Object{ + testutils.WithSpec(t, testutils.NewPod("test4", ""), map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }), + testutils.WithSpec(t, testutils.NewPod("test3", ""), map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }), + }, + Asserts: []runtime.Object{ + testutils.WithStatus(t, testutils.NewPod("test3", ""), map[string]interface{}{ + "qosClass": "BestEffort", + }), + }, + Errors: []runtime.Object{}, + }, + { + Name: "name-overridden", + Index: 3, + Step: &harness.TestStep{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name-overridden", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "TestStep", + APIVersion: "kudo.dev/v1beta1", + }, + Index: 3, + }, + Apply: []runtime.Object{ + testutils.WithSpec(t, testutils.NewPod("test6", ""), map[string]interface{}{ + "restartPolicy": "Never", + "containers": []map[string]interface{}{ + { + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }), + testutils.WithSpec(t, testutils.NewPod("test5", ""), map[string]interface{}{ + "restartPolicy": "Never", + "containers": []map[string]interface{}{ + { + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }), + }, + Asserts: []runtime.Object{ + testutils.WithSpec(t, testutils.NewPod("test5", ""), map[string]interface{}{ + "restartPolicy": "Never", + }), + }, + Errors: []runtime.Object{}, + }, + }, + }, + { + "test_data/list-pods", + []Step{ + { + Name: "pod", + Index: 0, + Apply: []runtime.Object{ + &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "name": "pod-1", + "labels": map[string]interface{}{ + "app": "nginx", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }, + }, + }, + }, + Asserts: []runtime.Object{ + &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "nginx", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }, + }, + }, + }, + Errors: []runtime.Object{}, + }, + }, + }, + } { + tt := tt + + t.Run(tt.path, func(t *testing.T) { + test := &Case{Dir: tt.path, Logger: testutils.NewTestLogger(t, tt.path)} + + err := test.LoadTestSteps() + assert.Nil(t, err) + + testStepsVal := []Step{} + for _, testStep := range test.Steps { + testStepsVal = append(testStepsVal, *testStep) + } + + assert.Equal(t, len(tt.testSteps), len(testStepsVal)) + for index := range tt.testSteps { + tt.testSteps[index].Dir = tt.path + assert.Equal(t, tt.testSteps[index].Apply, testStepsVal[index].Apply) + assert.Equal(t, tt.testSteps[index].Asserts, testStepsVal[index].Asserts) + assert.Equal(t, tt.testSteps[index].Errors, testStepsVal[index].Errors) + assert.Equal(t, tt.testSteps[index].Step, testStepsVal[index].Step) + assert.Equal(t, tt.testSteps[index].Dir, testStepsVal[index].Dir) + assert.Equal(t, tt.testSteps[index], testStepsVal[index]) + } + }) + } +} + +func TestCollectTestStepFiles(t *testing.T) { + for _, tt := range []struct { + path string + expected map[int64][]string + }{ + { + "test_data/with-overrides", + map[int64][]string{ + int64(0): { + "test_data/with-overrides/00-assert.yaml", + "test_data/with-overrides/00-test-step.yaml", + }, + int64(1): { + "test_data/with-overrides/01-assert.yaml", + "test_data/with-overrides/01-test-assert.yaml", + }, + int64(2): { + "test_data/with-overrides/02-directory/assert.yaml", + "test_data/with-overrides/02-directory/pod.yaml", + "test_data/with-overrides/02-directory/pod2.yaml", + }, + int64(3): { + "test_data/with-overrides/03-assert.yaml", + "test_data/with-overrides/03-pod.yaml", + "test_data/with-overrides/03-pod2.yaml", + }, + }, + }, + { + "test_data/list-pods", + map[int64][]string{ + int64(0): { + "test_data/list-pods/00-assert.yaml", + "test_data/list-pods/00-pod.yaml", + }, + }, + }, + } { + tt := tt + + t.Run(tt.path, func(t *testing.T) { + test := &Case{Dir: tt.path, Logger: testutils.NewTestLogger(t, tt.path)} + testStepFiles, err := test.CollectTestStepFiles() + assert.Nil(t, err) + assert.Equal(t, tt.expected, testStepFiles) + }) + } +} diff --git a/pkg/test/harness.go b/pkg/test/harness.go new file mode 100644 index 00000000..c84ea03e --- /dev/null +++ b/pkg/test/harness.go @@ -0,0 +1,438 @@ +package test + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "math/rand" + "os" + "path/filepath" + "sync" + "testing" + "time" + + volumetypes "github.com/docker/docker/api/types/volume" + docker "github.com/docker/docker/client" + yaml "gopkg.in/yaml.v2" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/envtest" + kindConfig "sigs.k8s.io/kind/pkg/apis/config/v1alpha3" + + harness "github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1" + testutils "github.com/kudobuilder/kuttl/pkg/test/utils" +) + +// Harness loads and runs tests based on the configuration provided. +type Harness struct { + TestSuite harness.TestSuite + T *testing.T + + logger testutils.Logger + managerStopCh chan struct{} + config *rest.Config + docker testutils.DockerClient + client client.Client + dclient discovery.DiscoveryInterface + env *envtest.Environment + kind *kind + kubeConfigPath string + clientLock sync.Mutex + configLock sync.Mutex +} + +// LoadTests loads all of the tests in a given directory. +func (h *Harness) LoadTests(dir string) ([]*Case, error) { + dir, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + + files, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + tests := []*Case{} + + timeout := h.GetTimeout() + h.T.Logf("Going to run test suite with timeout of %d seconds for each step", timeout) + + for _, file := range files { + if !file.IsDir() { + continue + } + + tests = append(tests, &Case{ + Timeout: timeout, + Steps: []*Step{}, + Name: file.Name(), + Dir: filepath.Join(dir, file.Name()), + SkipDelete: h.TestSuite.SkipDelete, + }) + } + + return tests, nil +} + +// GetLogger returns an initialized test logger. +func (h *Harness) GetLogger() testutils.Logger { + if h.logger == nil { + h.logger = testutils.NewTestLogger(h.T, "") + } + + return h.logger +} + +// GetTimeout returns the configured timeout for the test suite. +func (h *Harness) GetTimeout() int { + timeout := 30 + if h.TestSuite.Timeout != 0 { + timeout = h.TestSuite.Timeout + } + return timeout +} + +// RunKIND starts a KIND cluster. +func (h *Harness) RunKIND() (*rest.Config, error) { + if h.kind == nil { + var err error + + h.kubeConfigPath, err = ioutil.TempDir("", "kudo") + if err != nil { + return nil, err + } + + kind := newKind(h.TestSuite.KINDContext, h.explicitPath()) + h.kind = &kind + + if h.kind.IsRunning() { + return clientcmd.BuildConfigFromFlags("", h.explicitPath()) + } + + kindCfg := &kindConfig.Cluster{} + + if h.TestSuite.KINDConfig != "" { + var err error + kindCfg, err = loadKindConfig(h.TestSuite.KINDConfig) + if err != nil { + return nil, err + } + } + + dockerClient, err := h.DockerClient() + if err != nil { + return nil, err + } + + // Determine the correct API version to use with the user's Docker client. + dockerClient.NegotiateAPIVersion(context.TODO()) + + h.addNodeCaches(dockerClient, kindCfg) + + if err := h.kind.Run(kindCfg); err != nil { + return nil, err + } + + if err := h.kind.AddContainers(dockerClient, h.TestSuite.KINDContainers); err != nil { + return nil, err + } + } + + return clientcmd.BuildConfigFromFlags("", h.explicitPath()) +} + +func (h *Harness) addNodeCaches(dockerClient testutils.DockerClient, kindCfg *kindConfig.Cluster) { + if !h.TestSuite.KINDNodeCache { + return + } + + // add a default node if there are none specified. + if len(kindCfg.Nodes) == 0 { + kindCfg.Nodes = append(kindCfg.Nodes, kindConfig.Node{}) + } + + if h.TestSuite.KINDContext == "" { + h.TestSuite.KINDContext = harness.DefaultKINDContext + } + + for index := range kindCfg.Nodes { + volume, err := dockerClient.VolumeCreate(context.TODO(), volumetypes.VolumeCreateBody{ + Driver: "local", + Name: fmt.Sprintf("%s-%d", h.TestSuite.KINDContext, index), + }) + if err != nil { + h.T.Log("error creating volume for node", err) + continue + } + + h.T.Log("node mount point", volume.Mountpoint) + kindCfg.Nodes[index].ExtraMounts = append(kindCfg.Nodes[index].ExtraMounts, kindConfig.Mount{ + ContainerPath: "/var/lib/containerd", + HostPath: volume.Mountpoint, + }) + } +} + +// RunTestEnv starts a Kubernetes API server and etcd server for use in the +// tests and returns the Kubernetes configuration. +func (h *Harness) RunTestEnv() (*rest.Config, error) { + started := time.Now() + + testenv, err := testutils.StartTestEnvironment() + if err != nil { + return nil, err + } + + h.T.Log("started test environment (kube-apiserver and etcd) in", time.Since(started)) + h.env = testenv.Environment + + return testenv.Config, nil +} + +// Config returns the current Kubernetes configuration - either from the environment +// or from the created temporary control plane. +func (h *Harness) Config() (*rest.Config, error) { + h.configLock.Lock() + defer h.configLock.Unlock() + + if h.config != nil { + return h.config, nil + } + + var err error + + if h.TestSuite.StartControlPlane { + h.T.Log("Running tests with a mocked control plane (kube-apiserver and etcd).") + h.config, err = h.RunTestEnv() + } else if h.TestSuite.StartKIND { + h.T.Log("Running tests with KIND.") + h.config, err = h.RunKIND() + } else { + h.T.Log("Running tests using configured kubeconfig.") + h.config, err = config.GetConfig() + } + + if err != nil { + return h.config, err + } + + f, err := os.Create("kubeconfig") + if err != nil { + return h.config, err + } + + defer f.Close() + + return h.config, testutils.Kubeconfig(h.config, f) +} + +// Client returns the current Kubernetes client for the test harness. +func (h *Harness) Client(forceNew bool) (client.Client, error) { + h.clientLock.Lock() + defer h.clientLock.Unlock() + + if h.client != nil && !forceNew { + return h.client, nil + } + + cfg, err := h.Config() + if err != nil { + return nil, err + } + + h.client, err = testutils.NewRetryClient(cfg, client.Options{ + Scheme: testutils.Scheme(), + }) + return h.client, err +} + +// DiscoveryClient returns the current Kubernetes discovery client for the test harness. +func (h *Harness) DiscoveryClient() (discovery.DiscoveryInterface, error) { + h.clientLock.Lock() + defer h.clientLock.Unlock() + + if h.dclient != nil { + return h.dclient, nil + } + + cfg, err := h.Config() + if err != nil { + return nil, err + } + + h.dclient, err = discovery.NewDiscoveryClientForConfig(cfg) + return h.dclient, err +} + +// DockerClient returns the Docker client to use for the test harness. +func (h *Harness) DockerClient() (testutils.DockerClient, error) { + if h.docker != nil { + return h.docker, nil + } + + var err error + h.docker, err = docker.NewClientWithOpts(docker.FromEnv) + return h.docker, err +} + +// RunTests should be called from within a Go test (t) and launches all of the KUDO integration +// tests at dir. +func (h *Harness) RunTests() { + tests := []*Case{} + + for _, testDir := range h.TestSuite.TestDirs { + tempTests, err := h.LoadTests(testDir) + if err != nil { + h.T.Fatal(err) + } + tests = append(tests, tempTests...) + } + + h.T.Run("harness", func(t *testing.T) { + for _, test := range tests { + test := test + + test.Client = h.Client + test.DiscoveryClient = h.DiscoveryClient + + t.Run(test.Name, func(t *testing.T) { + test.Logger = testutils.NewTestLogger(t, test.Name) + + if err := test.LoadTestSteps(); err != nil { + t.Fatal(err) + } + + test.Run(t) + }) + } + }) +} + +// Run the test harness - start KUDO and the control plane and install the operators, if necessary +// and then run the tests. +func (h *Harness) Run() { + rand.Seed(time.Now().UTC().UnixNano()) + + defer h.Stop() + + cl, err := h.Client(false) + if err != nil { + h.T.Fatal(err) + } + + dClient, err := h.DiscoveryClient() + if err != nil { + h.T.Fatal(err) + } + + // Install CRDs + crdKind := testutils.NewResource("apiextensions.k8s.io/v1beta1", "CustomResourceDefinition", "", "") + crds, err := testutils.InstallManifests(context.TODO(), cl, dClient, h.TestSuite.CRDDir, crdKind) + if err != nil { + h.T.Fatal(err) + } + + if err := testutils.WaitForCRDs(dClient, crds); err != nil { + h.T.Fatal(err) + } + + // Create a new client to bust the client's CRD cache. + cl, err = h.Client(true) + if err != nil { + h.T.Fatal(err) + } + + // Install required manifests. + for _, manifestDir := range h.TestSuite.ManifestDirs { + if _, err := testutils.InstallManifests(context.TODO(), cl, dClient, manifestDir); err != nil { + h.T.Fatal(err) + } + } + + if err := testutils.RunCommands(h.GetLogger(), "default", "", h.TestSuite.Commands, ""); err != nil { + h.T.Fatal(err) + } + + if err := testutils.RunKubectlCommands(h.GetLogger(), "default", h.TestSuite.Kubectl, ""); err != nil { + h.T.Fatal(err) + } + + h.RunTests() +} + +// Stop the test environment and KUDO, clean up the harness. +func (h *Harness) Stop() { + if h.managerStopCh != nil { + close(h.managerStopCh) + h.managerStopCh = nil + } + + if h.kind != nil { + logDir := filepath.Join(h.TestSuite.ArtifactsDir, fmt.Sprintf("kind-logs-%d", time.Now().Unix())) + + h.T.Log("collecting cluster logs to", logDir) + + if err := h.kind.CollectLogs(logDir); err != nil { + h.T.Log("error collecting kind cluster logs", err) + } + } + + if h.TestSuite.SkipClusterDelete || h.TestSuite.SkipDelete { + cwd, _ := os.Getwd() + kubeconfig := filepath.Join(cwd, "kubeconfig") + + h.T.Log("skipping cluster tear down") + h.T.Log(fmt.Sprintf("to connect to the cluster, run: export KUBECONFIG=\"%s\"", kubeconfig)) + + return + } + + if h.env != nil { + h.T.Log("tearing down mock control plane") + if err := h.env.Stop(); err != nil { + h.T.Log("error tearing down mock control plane", err) + } + + h.env = nil + } + + if h.kind != nil { + h.T.Log("tearing down kind cluster") + if err := h.kind.Stop(); err != nil { + h.T.Log("error tearing down kind cluster", err) + } + + if err := os.RemoveAll(h.kubeConfigPath); err != nil { + h.T.Log("error removing temporary directory", err) + } + + h.kind = nil + } +} + +func (h *Harness) explicitPath() string { + return filepath.Join(h.kubeConfigPath, "kubeconfig") +} + +func loadKindConfig(path string) (*kindConfig.Cluster, error) { + raw, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + cluster := &kindConfig.Cluster{} + + decoder := yaml.NewDecoder(bytes.NewReader(raw)) + decoder.SetStrict(true) + + if err := decoder.Decode(cluster); err != nil { + return nil, err + } + + return cluster, nil +} diff --git a/pkg/test/harness_integration_test.go b/pkg/test/harness_integration_test.go new file mode 100644 index 00000000..d682950b --- /dev/null +++ b/pkg/test/harness_integration_test.go @@ -0,0 +1,23 @@ +// +build integration + +package test + +import ( + "testing" + + harness "github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1" +) + +func TestHarnessRunIntegration(t *testing.T) { + harness := Harness{ + TestSuite: harness.TestSuite{ + TestDirs: []string{ + "./test_data/", + }, + StartKUDO: false, + StartControlPlane: true, + }, + T: t, + } + harness.Run() +} diff --git a/pkg/test/harness_test.go b/pkg/test/harness_test.go new file mode 100644 index 00000000..603356f5 --- /dev/null +++ b/pkg/test/harness_test.go @@ -0,0 +1,83 @@ +package test + +import ( + "context" + "fmt" + "io" + "testing" + + dockertypes "github.com/docker/docker/api/types" + volumetypes "github.com/docker/docker/api/types/volume" + "github.com/stretchr/testify/assert" + kindConfig "sigs.k8s.io/kind/pkg/apis/config/v1alpha3" +) + +func TestGetTimeout(t *testing.T) { + h := Harness{} + assert.Equal(t, 30, h.GetTimeout()) + + h.TestSuite.Timeout = 45 + assert.Equal(t, 45, h.GetTimeout()) +} + +type dockerMock struct { + ImageWriter *io.PipeWriter + imageReader *io.PipeReader +} + +func newDockerMock() *dockerMock { + reader, writer := io.Pipe() + + return &dockerMock{ + ImageWriter: writer, + imageReader: reader, + } +} + +func (d *dockerMock) VolumeCreate(ctx context.Context, body volumetypes.VolumeCreateBody) (dockertypes.Volume, error) { + return dockertypes.Volume{ + Mountpoint: fmt.Sprintf("/var/lib/docker/data/%s", body.Name), + }, nil +} + +func (d *dockerMock) NegotiateAPIVersion(ctx context.Context) {} + +func (d *dockerMock) ImageSave(ctx context.Context, imageIDs []string) (io.ReadCloser, error) { + return d.imageReader, nil +} + +func TestAddNodeCaches(t *testing.T) { + h := Harness{ + T: t, + docker: newDockerMock(), + } + + kindCfg := &kindConfig.Cluster{} + h.addNodeCaches(h.docker, kindCfg) + assert.Nil(t, kindCfg.Nodes) + + h.TestSuite.KINDNodeCache = true + h.addNodeCaches(h.docker, kindCfg) + assert.NotNil(t, kindCfg.Nodes) + assert.Equal(t, 1, len(kindCfg.Nodes)) + assert.NotNil(t, kindCfg.Nodes[0].ExtraMounts) + assert.Equal(t, 1, len(kindCfg.Nodes[0].ExtraMounts)) + assert.Equal(t, "/var/lib/containerd", kindCfg.Nodes[0].ExtraMounts[0].ContainerPath) + assert.Equal(t, "/var/lib/docker/data/kind-0", kindCfg.Nodes[0].ExtraMounts[0].HostPath) + + kindCfg = &kindConfig.Cluster{ + Nodes: []kindConfig.Node{ + {}, + {}, + }, + } + + h.addNodeCaches(h.docker, kindCfg) + assert.NotNil(t, kindCfg.Nodes) + assert.Equal(t, 2, len(kindCfg.Nodes)) + assert.NotNil(t, kindCfg.Nodes[0].ExtraMounts) + assert.Equal(t, 1, len(kindCfg.Nodes[0].ExtraMounts)) + assert.Equal(t, "/var/lib/containerd", kindCfg.Nodes[0].ExtraMounts[0].ContainerPath) + assert.Equal(t, "/var/lib/docker/data/kind-0", kindCfg.Nodes[0].ExtraMounts[0].HostPath) + assert.Equal(t, "/var/lib/docker/data/kind-1", kindCfg.Nodes[1].ExtraMounts[0].HostPath) +} diff --git a/pkg/test/kind.go b/pkg/test/kind.go new file mode 100644 index 00000000..679ef3f8 --- /dev/null +++ b/pkg/test/kind.go @@ -0,0 +1,102 @@ +package test + +import ( + "context" + + "sigs.k8s.io/kind/pkg/apis/config/v1alpha3" + "sigs.k8s.io/kind/pkg/cluster" + "sigs.k8s.io/kind/pkg/cluster/nodes" + "sigs.k8s.io/kind/pkg/cluster/nodeutils" + + testutils "github.com/kudobuilder/kuttl/pkg/test/utils" +) + +// kind provides a thin abstraction layer for a KIND cluster. +type kind struct { + Provider *cluster.Provider + context string + explicitPath string +} + +func newKind(kindContext string, explicitPath string) kind { + provider := cluster.NewProvider() + + return kind{ + Provider: provider, + context: kindContext, + explicitPath: explicitPath, + } +} + +// Run starts a KIND cluster from a given configuration. +func (k *kind) Run(config *v1alpha3.Cluster) error { + return k.Provider.Create( + k.context, + cluster.CreateWithV1Alpha3Config(config), + cluster.CreateWithKubeconfigPath(k.explicitPath), + ) +} + +// IsRunning checks if a KIND cluster is already running for the current context. +func (k *kind) IsRunning() bool { + contexts, err := k.Provider.List() + if err != nil { + panic(err) + } + + for _, context := range contexts { + if context == k.context { + return true + } + } + + return false +} + +// AddContainers loads the named Docker containers into a KIND cluster. +// The cluster must be running for this to work. +func (k *kind) AddContainers(docker testutils.DockerClient, containers []string) error { + if !k.IsRunning() { + panic("KIND cluster isn't running") + } + + nodes, err := k.Provider.ListNodes(k.context) + if err != nil { + return err + } + + for _, node := range nodes { + for _, container := range containers { + if err := loadContainer(docker, node, container); err != nil { + return err + } + } + } + + return nil +} + +// CollectLogs saves the cluster logs to a directory. +func (k *kind) CollectLogs(dir string) error { + return k.Provider.CollectLogs(k.context, dir) +} + +// Stop stops the KIND cluster. +func (k *kind) Stop() error { + return k.Provider.Delete(k.context, k.explicitPath) +} + +func loadContainer(docker testutils.DockerClient, node nodes.Node, container string) error { + image, err := docker.ImageSave(context.TODO(), []string{container}) + if err != nil { + return err + } + + defer image.Close() + + if err := nodeutils.LoadImageArchive(node, image); err != nil { + return err + } + + return nil +} diff --git a/pkg/test/kind_integration_test.go b/pkg/test/kind_integration_test.go new file mode 100644 index 00000000..85c65fc4 --- /dev/null +++ b/pkg/test/kind_integration_test.go @@ -0,0 +1,100 @@ +//+build integration + +package test + +import ( + "bufio" + "bytes" + "context" + "strings" + "testing" + + "github.com/docker/docker/api/types" + dockerClient "github.com/docker/docker/client" + "github.com/thoas/go-funk" + "sigs.k8s.io/kind/pkg/apis/config/v1alpha3" + "sigs.k8s.io/kind/pkg/cluster/nodes" +) + +const ( + kindTestContext = "test" + testImage = "docker.io/library/busybox:latest" +) + +// Tests that Docker images are added to the nodes of a KIND cluster with the +// 'AddContainers' method. +func TestAddContainers(t *testing.T) { + ctx := context.Background() + + kind := newKind(kindTestContext, "kubeconfig") + + config := v1alpha3.Cluster{} + + if err := kind.Run(&config); err != nil { + t.Fatalf("failed to start KIND cluster: %v", err) + } + + defer func() { + if err := kind.Stop(); err != nil { + t.Fatalf("failed to stop KIND cluster: %v", err) + } + }() + + docker, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv) + if err != nil { + t.Fatalf("failed to create Docker client: %v", err) + } + + docker.NegotiateAPIVersion(ctx) + + if !kind.IsRunning() { + t.Error("KIND isn't running") + } + + reader, err := docker.ImagePull(ctx, testImage, types.ImagePullOptions{}) + if err != nil { + t.Errorf("failed to pull test image: %v", err) + } + + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + t.Log(scanner.Text()) + } + + if err := reader.Close(); err != nil { + t.Errorf("failed to close image pull output: %v", err) + } + + if err := kind.AddContainers(docker, []string{testImage}); err != nil { + t.Errorf("failed to add container to KIND cluster: %v", err) + } + + nodes, err := kind.Provider.ListNodes(kindTestContext) + if err != nil { + t.Fatalf("failed to list nodes of KIND cluster: %v", err) + } + + for _, node := range nodes { + images, err := nodeImages(node) + if err != nil { + t.Errorf("failed to list node images: %v", err) + } + + if !funk.ContainsString(images, testImage) { + t.Errorf("failed to find image %s on node %s", testImage, node.String()) + } + } +} + +func nodeImages(node nodes.Node) ([]string, error) { + var stdout bytes.Buffer + + cmd := node.Command("ctr", "--namespace=k8s.io", "images", "list", "-q") + cmd.SetStdout(&stdout) + + if err := cmd.Run(); err != nil { + return []string{}, err + } + + return strings.Split(stdout.String(), "\n"), nil +} diff --git a/pkg/test/step.go b/pkg/test/step.go new file mode 100644 index 00000000..b7d11ace --- /dev/null +++ b/pkg/test/step.go @@ -0,0 +1,477 @@ +package test + +import ( + "context" + "fmt" + "path/filepath" + "regexp" + "time" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/discovery" + "sigs.k8s.io/controller-runtime/pkg/client" + + harness "github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1" + testutils "github.com/kudobuilder/kuttl/pkg/test/utils" +) + +var fileNameRegex = regexp.MustCompile(`^(\d+-)?([^.]+)(.yaml)?$`) + +// A Step contains the name of the test step, its index in the test, +// and all of the test step's settings (including objects to apply and assert on). +type Step struct { + Name string + Index int + + Dir string + + Step *harness.TestStep + Assert *harness.TestAssert + + Asserts []runtime.Object + Apply []runtime.Object + Errors []runtime.Object + + Timeout int + + Client func(forceNew bool) (client.Client, error) + DiscoveryClient func() (discovery.DiscoveryInterface, error) + + Logger testutils.Logger +} + +// Clean deletes all resources defined in the Apply list. +func (s *Step) Clean(namespace string) error { + cl, err := s.Client(false) + if err != nil { + return err + } + + dClient, err := s.DiscoveryClient() + if err != nil { + return err + } + + for _, obj := range s.Apply { + _, _, err := testutils.Namespaced(dClient, obj, namespace) + if err != nil { + return err + } + + if err := cl.Delete(context.TODO(), obj); err != nil && !k8serrors.IsNotFound(err) { + return err + } + } + + return nil +} + +// DeleteExisting deletes any resources in the TestStep.Delete list prior to running the tests. +func (s *Step) DeleteExisting(namespace string) error { + cl, err := s.Client(false) + if err != nil { + return err + } + + dClient, err := s.DiscoveryClient() + if err != nil { + return err + } + + toDelete := []runtime.Object{} + + if s.Step == nil { + return nil + } + + for _, ref := range s.Step.Delete { + gvk := ref.GroupVersionKind() + + obj := testutils.NewResource(gvk.GroupVersion().String(), gvk.Kind, ref.Name, "") + + objNs := namespace + if ref.Namespace != "" { + objNs = ref.Namespace + } + + _, objNs, err := testutils.Namespaced(dClient, obj, objNs) + if err != nil { + return err + } + + if ref.Name == "" { + u := &unstructured.UnstructuredList{} + u.SetGroupVersionKind(gvk) + + listOptions := []client.ListOption{} + + if ref.Labels != nil { + listOptions = append(listOptions, client.MatchingLabels(ref.Labels)) + } + + if objNs != "" { + listOptions = append(listOptions, client.InNamespace(objNs)) + } + + err := cl.List(context.TODO(), u, listOptions...) + if err != nil { + return fmt.Errorf("listing matching resources: %w", err) + } + + for index := range u.Items { + toDelete = append(toDelete, &u.Items[index]) + } + } else { + // Otherwise just append the object specified. + toDelete = append(toDelete, obj.DeepCopyObject()) + } + } + + for _, obj := range toDelete { + err := cl.Delete(context.TODO(), obj.DeepCopyObject()) + if err != nil && !k8serrors.IsNotFound(err) { + return err + } + } + + // Wait for resources to be deleted. + return wait.PollImmediate(100*time.Millisecond, time.Duration(s.GetTimeout())*time.Second, func() (done bool, err error) { + for _, obj := range toDelete { + err = cl.Get(context.TODO(), testutils.ObjectKey(obj), obj.DeepCopyObject()) + if err == nil || !k8serrors.IsNotFound(err) { + return false, err + } + } + + return true, nil + }) +} + +// Create applies all resources defined in the Apply list. +func (s *Step) Create(namespace string) []error { + cl, err := s.Client(true) + if err != nil { + return []error{err} + } + + dClient, err := s.DiscoveryClient() + if err != nil { + return []error{err} + } + + errors := []error{} + + for _, obj := range s.Apply { + _, _, err := testutils.Namespaced(dClient, obj, namespace) + if err != nil { + errors = append(errors, err) + continue + } + + if updated, err := testutils.CreateOrUpdate(context.TODO(), cl, obj, true); err != nil { + errors = append(errors, err) + } else { + action := "created" + if updated { + action = "updated" + } + s.Logger.Log(testutils.ResourceID(obj), action) + } + } + + return errors +} + +// GetTimeout gets the timeout defined for the test step. +func (s *Step) GetTimeout() int { + timeout := s.Timeout + if s.Assert != nil && s.Assert.Timeout != 0 { + timeout = s.Assert.Timeout + } + return timeout +} + +func list(cl client.Client, gvk schema.GroupVersionKind, namespace string) ([]unstructured.Unstructured, error) { + list := unstructured.UnstructuredList{} + list.SetGroupVersionKind(gvk) + + listOptions := []client.ListOption{} + if namespace != "" { + listOptions = append(listOptions, client.InNamespace(namespace)) + } + + if err := cl.List(context.TODO(), &list, listOptions...); err != nil { + return []unstructured.Unstructured{}, err + } + + return list.Items, nil +} + +// CheckResource checks if the expected resource's state in Kubernetes is correct. +func (s *Step) CheckResource(expected runtime.Object, namespace string) []error { + cl, err := s.Client(false) + if err != nil { + return []error{err} + } + + dClient, err := s.DiscoveryClient() + if err != nil { + return []error{err} + } + + testErrors := []error{} + + name, namespace, err := testutils.Namespaced(dClient, expected, namespace) + if err != nil { + return append(testErrors, err) + } + + gvk := expected.GetObjectKind().GroupVersionKind() + + actuals := []unstructured.Unstructured{} + + if name != "" { + actual := unstructured.Unstructured{} + actual.SetGroupVersionKind(gvk) + + err = cl.Get(context.TODO(), client.ObjectKey{ + Namespace: namespace, + Name: name, + }, &actual) + + actuals = append(actuals, actual) + } else { + actuals, err = list(cl, gvk, namespace) + if len(actuals) == 0 { + testErrors = append(testErrors, fmt.Errorf("no resources matched of kind: %s", gvk.String())) + } + } + if err != nil { + return append(testErrors, err) + } + + expectedObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(expected) + if err != nil { + return append(testErrors, err) + } + + for _, actual := range actuals { + actual := actual + + tmpTestErrors := []error{} + + if err := testutils.IsSubset(expectedObj, actual.UnstructuredContent()); err != nil { + diff, diffErr := testutils.PrettyDiff(expected, &actual) + if diffErr == nil { + tmpTestErrors = append(tmpTestErrors, fmt.Errorf(diff)) + } else { + tmpTestErrors = append(tmpTestErrors, diffErr) + } + + tmpTestErrors = append(tmpTestErrors, fmt.Errorf("resource %s: %s", testutils.ResourceID(expected), err)) + } + + if len(tmpTestErrors) == 0 { + return tmpTestErrors + } + + testErrors = append(testErrors, tmpTestErrors...) + } + + return testErrors +} + +// CheckResourceAbsent checks if the expected resource's state is absent in Kubernetes. +func (s *Step) CheckResourceAbsent(expected runtime.Object, namespace string) error { + cl, err := s.Client(false) + if err != nil { + return err + } + + dClient, err := s.DiscoveryClient() + if err != nil { + return err + } + + name, namespace, err := testutils.Namespaced(dClient, expected, namespace) + if err != nil { + return err + } + + gvk := expected.GetObjectKind().GroupVersionKind() + + var actuals []unstructured.Unstructured + + if name != "" { + actual := unstructured.Unstructured{} + actual.SetGroupVersionKind(gvk) + + if err := cl.Get(context.TODO(), client.ObjectKey{ + Namespace: namespace, + Name: name, + }, &actual); err != nil { + if k8serrors.IsNotFound(err) { + return nil + } + + return err + } + + actuals = []unstructured.Unstructured{actual} + } else { + actuals, err = list(cl, gvk, namespace) + if err != nil { + return err + } + } + + expectedObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(expected) + if err != nil { + return err + } + + for _, actual := range actuals { + if err := testutils.IsSubset(expectedObj, actual.UnstructuredContent()); err == nil { + return fmt.Errorf("resource matched of kind: %s", gvk.String()) + } + } + + return nil +} + +// Check checks if the resources defined in Asserts and Errors are in the correct state. +func (s *Step) Check(namespace string) []error { + testErrors := []error{} + + for _, expected := range s.Asserts { + testErrors = append(testErrors, s.CheckResource(expected, namespace)...) + } + + for _, expected := range s.Errors { + if testError := s.CheckResourceAbsent(expected, namespace); testError != nil { + testErrors = append(testErrors, testError) + } + } + + return testErrors +} + +// Run runs a KUDO test step: +// 1. Apply all desired objects to Kubernetes. +// 2. Wait for all of the states defined in the test step's asserts to be true.' +func (s *Step) Run(namespace string) []error { + s.Logger.Log("starting test step", s.String()) + + if err := s.DeleteExisting(namespace); err != nil { + return []error{err} + } + + testErrors := []error{} + + if s.Step != nil { + if errors := testutils.RunCommands(s.Logger, namespace, "", s.Step.Commands, s.Dir); errors != nil { + testErrors = append(testErrors, errors...) + } + + if errors := testutils.RunKubectlCommands(s.Logger, namespace, s.Step.Kubectl, s.Dir); errors != nil { + testErrors = append(testErrors, errors...) + } + } + + testErrors = append(testErrors, s.Create(namespace)...) + + if len(testErrors) != 0 { + return testErrors + } + + for i := 0; i < s.GetTimeout(); i++ { + testErrors = s.Check(namespace) + + if len(testErrors) == 0 { + break + } + + time.Sleep(time.Second) + } + + if len(testErrors) == 0 { + s.Logger.Log("test step completed", s.String()) + } else { + s.Logger.Log("test step failed", s.String()) + } + + return testErrors +} + +// String implements the string interface, returning the name of the test step. +func (s *Step) String() string { + return fmt.Sprintf("%d-%s", s.Index, s.Name) +} + +// LoadYAML loads the resources from a YAML file for a test step: +// * If the YAML file is called "assert", then it contains objects to +// add to the test step's list of assertions. +// * If the YAML file is called "errors", then it contains objects that, +// if seen, mark a test immediately failed. +// * All other YAML files are considered resources to create. +func (s *Step) LoadYAML(file string) error { + objects, err := testutils.LoadYAML(file) + if err != nil { + return fmt.Errorf("loading %s: %s", file, err) + } + + matches := fileNameRegex.FindStringSubmatch(filepath.Base(file)) + fname := matches[2] + + switch fname { + case "assert": + s.Asserts = append(s.Asserts, objects...) + case "errors": + s.Errors = append(s.Errors, objects...) + default: + if s.Name == "" { + s.Name = fname + } + s.Apply = append(s.Apply, objects...) + } + + asserts := []runtime.Object{} + + for _, obj := range s.Asserts { + if obj.GetObjectKind().GroupVersionKind().Kind == "TestAssert" { + if testAssert, ok := obj.(*harness.TestAssert); ok { + s.Assert = testAssert + } else { + return fmt.Errorf("failed to load TestAssert object from %s: it contains an object of type %T", file, obj) + } + } else { + asserts = append(asserts, obj) + } + } + + apply := []runtime.Object{} + + for _, obj := range s.Apply { + if obj.GetObjectKind().GroupVersionKind().Kind == "TestStep" { + if testStep, ok := obj.(*harness.TestStep); ok { + s.Step = testStep + } else { + return fmt.Errorf("failed to load TestStep object from %s: it contains an object of type %T", file, obj) + } + s.Step.Index = s.Index + if s.Step.Name != "" { + s.Name = s.Step.Name + } + } else { + apply = append(apply, obj) + } + } + + s.Apply = apply + s.Asserts = asserts + return nil +} diff --git a/pkg/test/step_integration_test.go b/pkg/test/step_integration_test.go new file mode 100644 index 00000000..1a7c1bc8 --- /dev/null +++ b/pkg/test/step_integration_test.go @@ -0,0 +1,343 @@ +// +build integration + +package test + +import ( + "context" + "fmt" + "log" + "math/rand" + "os" + "testing" + "time" + + petname "github.com/dustinkirkland/golang-petname" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/discovery" + "sigs.k8s.io/controller-runtime/pkg/client" + + harness "github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1" + testutils "github.com/kudobuilder/kuttl/pkg/test/utils" +) + +var testenv testutils.TestEnvironment + +func TestMain(m *testing.M) { + var err error + + testenv, err = testutils.StartTestEnvironment() + if err != nil { + log.Fatal(err) + } + + exitCode := m.Run() + testenv.Environment.Stop() + os.Exit(exitCode) +} + +func TestCheckResourceIntegration(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + + for _, test := range []struct { + testName string + actual []runtime.Object + expected runtime.Object + shouldError bool + }{ + { + testName: "match object by labels, first in list matches", + actual: []runtime.Object{ + testutils.WithSpec(t, testutils.WithLabels(t, testutils.NewPod("labels-match-pod", ""), map[string]string{ + "app": "nginx", + }), map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }), + testutils.WithSpec(t, testutils.WithLabels(t, testutils.NewPod("bb", ""), map[string]string{ + "app": "not-match", + }), map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }), + }, + expected: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "nginx", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }, + }, + }, + }, + { + testName: "match object by labels, last in list matches", + actual: []runtime.Object{ + testutils.WithSpec(t, testutils.WithLabels(t, testutils.NewPod("last-in-list", ""), map[string]string{ + "app": "not-match", + }), map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }), + testutils.WithSpec(t, testutils.WithLabels(t, testutils.NewPod("bb", ""), map[string]string{ + "app": "nginx", + }), map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }), + }, + expected: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "nginx", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }, + }, + }, + }, + { + testName: "match object by labels, does not exist", + actual: []runtime.Object{ + testutils.WithSpec(t, testutils.WithLabels(t, testutils.NewPod("hello", ""), map[string]string{ + "app": "NOT-A-MATCH", + }), map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }), + }, + expected: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "nginx", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }, + }, + }, + shouldError: true, + }, + { + testName: "match object by labels, field mismatch", + actual: []runtime.Object{ + testutils.WithSpec(t, testutils.WithLabels(t, testutils.NewPod("hello", ""), map[string]string{ + "app": "nginx", + }), map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "otherimage:latest", + "name": "nginx", + }, + }, + }), + }, + expected: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "nginx", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }, + }, + }, + shouldError: true, + }, + { + testName: "step should fail if there are no objects of the same type in the namespace", + actual: []runtime.Object{}, + expected: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "nginx", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }, + }, + }, + shouldError: true, + }, + } { + t.Run(test.testName, func(t *testing.T) { + namespace := fmt.Sprintf("kudo-test-%s", petname.Generate(2, "-")) + + err := testenv.Client.Create(context.TODO(), testutils.NewResource("v1", "Namespace", namespace, "")) + if !k8serrors.IsAlreadyExists(err) { + // we are ignoring already exists here because in tests we by default use retry client so this can happen + assert.Nil(t, err) + } + + for _, actual := range test.actual { + _, _, err := testutils.Namespaced(testenv.DiscoveryClient, actual, namespace) + assert.Nil(t, err) + + assert.Nil(t, testenv.Client.Create(context.TODO(), actual)) + } + + step := Step{ + Logger: testutils.NewTestLogger(t, ""), + Client: func(bool) (client.Client, error) { return testenv.Client, nil }, + DiscoveryClient: func() (discovery.DiscoveryInterface, error) { return testenv.DiscoveryClient, nil }, + } + + errors := step.CheckResource(test.expected, namespace) + + if test.shouldError { + assert.NotEqual(t, []error{}, errors) + } else { + assert.Equal(t, []error{}, errors) + } + }) + } +} + +// Verify that the DeleteExisting method properly cleans up resources that are matched on labels during a test step. +func TestStepDeleteExistingLabelMatch(t *testing.T) { + namespace := "world" + + podSpec := map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "otherimage:latest", + "name": "nginx", + }, + }, + } + + podToDelete := testutils.WithSpec(t, testutils.WithLabels(t, testutils.NewPod("aa-delete-me", "world"), map[string]string{ + "hello": "world", + }), podSpec) + + podToKeep := testutils.WithSpec(t, testutils.WithLabels(t, testutils.NewPod("bb-dont-delete-me", "world"), map[string]string{ + "bye": "moon", + }), podSpec) + + podToDelete2 := testutils.WithSpec(t, testutils.WithLabels(t, testutils.NewPod("cc-delete-me", "world"), map[string]string{ + "hello": "world", + }), podSpec) + + step := Step{ + Logger: testutils.NewTestLogger(t, ""), + Step: &harness.TestStep{ + Delete: []harness.ObjectReference{ + { + ObjectReference: corev1.ObjectReference{ + Kind: "Pod", + APIVersion: "v1", + }, + Labels: map[string]string{ + "hello": "world", + }, + }, + }, + }, + Client: func(bool) (client.Client, error) { return testenv.Client, nil }, + DiscoveryClient: func() (discovery.DiscoveryInterface, error) { return testenv.DiscoveryClient, nil }, + } + + assert.Nil(t, testenv.Client.Create(context.TODO(), podToKeep)) + assert.Nil(t, testenv.Client.Create(context.TODO(), podToDelete)) + assert.Nil(t, testenv.Client.Create(context.TODO(), podToDelete2)) + + assert.Nil(t, testenv.Client.Get(context.TODO(), testutils.ObjectKey(podToKeep), podToKeep)) + assert.Nil(t, testenv.Client.Get(context.TODO(), testutils.ObjectKey(podToDelete), podToDelete)) + assert.Nil(t, testenv.Client.Get(context.TODO(), testutils.ObjectKey(podToDelete2), podToDelete2)) + + assert.Nil(t, step.DeleteExisting(namespace)) + + assert.Nil(t, testenv.Client.Get(context.TODO(), testutils.ObjectKey(podToKeep), podToKeep)) + assert.True(t, k8serrors.IsNotFound(testenv.Client.Get(context.TODO(), testutils.ObjectKey(podToDelete), podToDelete))) + assert.True(t, k8serrors.IsNotFound(testenv.Client.Get(context.TODO(), testutils.ObjectKey(podToDelete2), podToDelete2))) +} + +func TestCheckedTypeAssertions(t *testing.T) { + tests := []struct { + name string + typeName string + }{ + {"assert", "TestAssert"}, + {"apply", "TestStep"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + step := Step{} + path := fmt.Sprintf("step_integration_test_data/00-%s.yaml", test.name) + assert.EqualError(t, step.LoadYAML(path), + fmt.Sprintf("failed to load %s object from %s: it contains an object of type *unstructured.Unstructured", + test.typeName, path)) + }) + } +} diff --git a/pkg/test/step_integration_test_data/00-apply.yaml b/pkg/test/step_integration_test_data/00-apply.yaml new file mode 100644 index 00000000..f5c9a301 --- /dev/null +++ b/pkg/test/step_integration_test_data/00-apply.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: TestStep +metadata: + name: problem diff --git a/pkg/test/step_integration_test_data/00-assert.yaml b/pkg/test/step_integration_test_data/00-assert.yaml new file mode 100644 index 00000000..957cc05d --- /dev/null +++ b/pkg/test/step_integration_test_data/00-assert.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: TestAssert +metadata: + name: problem diff --git a/pkg/test/step_test.go b/pkg/test/step_test.go new file mode 100644 index 00000000..bc263c86 --- /dev/null +++ b/pkg/test/step_test.go @@ -0,0 +1,323 @@ +package test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/discovery" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + harness "github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1" + testutils "github.com/kudobuilder/kuttl/pkg/test/utils" +) + +const ( + testNamespace = "world" +) + +// Verify the test state as loaded from disk. +// Each test provides a path to a set of test steps and their rendered result. +func TestStepClean(t *testing.T) { + + pod := testutils.NewPod("hello", "") + + podWithNamespace := testutils.WithNamespace(pod, testNamespace) + pod2WithNamespace := testutils.NewPod("hello2", testNamespace) + pod2WithDiffNamespace := testutils.NewPod("hello2", "different-namespace") + + cl := fake.NewFakeClient(pod, pod2WithNamespace, pod2WithDiffNamespace) + + step := Step{ + Apply: []runtime.Object{ + pod.DeepCopyObject(), pod2WithDiffNamespace.DeepCopyObject(), testutils.NewPod("does-not-exist", ""), + }, + Client: func(bool) (client.Client, error) { return cl, nil }, + DiscoveryClient: func() (discovery.DiscoveryInterface, error) { return testutils.FakeDiscoveryClient(), nil }, + } + + assert.Nil(t, step.Clean(testNamespace)) + + assert.True(t, k8serrors.IsNotFound(cl.Get(context.TODO(), testutils.ObjectKey(podWithNamespace), podWithNamespace))) + assert.Nil(t, cl.Get(context.TODO(), testutils.ObjectKey(pod2WithNamespace), pod2WithNamespace)) + assert.True(t, k8serrors.IsNotFound(cl.Get(context.TODO(), testutils.ObjectKey(pod2WithDiffNamespace), pod2WithDiffNamespace))) +} + +// Verify the test state as loaded from disk. +// Each test provides a path to a set of test steps and their rendered result. +func TestStepCreate(t *testing.T) { + + pod := testutils.NewPod("hello", "") + podWithNamespace := testutils.NewPod("hello2", "different-namespace") + clusterScopedResource := testutils.NewResource("v1", "Namespace", "my-namespace", "") + podToUpdate := testutils.NewPod("update-me", "") + specToApply := map[string]interface{}{ + "replicas": int64(2), + } + updateToApply := testutils.WithSpec(t, podToUpdate, specToApply) + + cl := fake.NewFakeClient(testutils.WithNamespace(podToUpdate, testNamespace)) + + step := Step{ + Logger: testutils.NewTestLogger(t, ""), + Apply: []runtime.Object{ + pod.DeepCopyObject(), podWithNamespace.DeepCopyObject(), clusterScopedResource, updateToApply, + }, + Client: func(bool) (client.Client, error) { return cl, nil }, + DiscoveryClient: func() (discovery.DiscoveryInterface, error) { return testutils.FakeDiscoveryClient(), nil }, + } + + assert.Equal(t, []error{}, step.Create(testNamespace)) + + assert.Nil(t, cl.Get(context.TODO(), testutils.ObjectKey(pod), pod)) + assert.Nil(t, cl.Get(context.TODO(), testutils.ObjectKey(clusterScopedResource), clusterScopedResource)) + + updatedPod := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Pod"}} + assert.Nil(t, cl.Get(context.TODO(), testutils.ObjectKey(podToUpdate), updatedPod)) + assert.Equal(t, specToApply, updatedPod.Object["spec"]) + + assert.Nil(t, cl.Get(context.TODO(), testutils.ObjectKey(podWithNamespace), podWithNamespace)) + actual := testutils.NewPod("hello2", testNamespace) + assert.True(t, k8serrors.IsNotFound(cl.Get(context.TODO(), testutils.ObjectKey(actual), actual))) +} + +// Verify that the DeleteExisting method properly cleans up resources during a test step. +func TestStepDeleteExisting(t *testing.T) { + + podToDelete := testutils.NewPod("delete-me", testNamespace) + podToDeleteDefaultNS := testutils.NewPod("also-delete-me", "default") + podToKeep := testutils.NewPod("keep-me", testNamespace) + + cl := fake.NewFakeClient(podToDelete, podToKeep, podToDeleteDefaultNS) + + step := Step{ + Logger: testutils.NewTestLogger(t, ""), + Step: &harness.TestStep{ + Delete: []harness.ObjectReference{ + { + ObjectReference: corev1.ObjectReference{ + Kind: "Pod", + APIVersion: "v1", + Name: "delete-me", + }, + }, + { + ObjectReference: corev1.ObjectReference{ + Kind: "Pod", + APIVersion: "v1", + Name: "also-delete-me", + Namespace: "default", + }, + }, + }, + }, + Client: func(bool) (client.Client, error) { return cl, nil }, + DiscoveryClient: func() (discovery.DiscoveryInterface, error) { return testutils.FakeDiscoveryClient(), nil }, + } + + assert.Nil(t, cl.Get(context.TODO(), testutils.ObjectKey(podToKeep), podToKeep)) + assert.Nil(t, cl.Get(context.TODO(), testutils.ObjectKey(podToDelete), podToDelete)) + assert.Nil(t, cl.Get(context.TODO(), testutils.ObjectKey(podToDeleteDefaultNS), podToDeleteDefaultNS)) + + assert.Nil(t, step.DeleteExisting(testNamespace)) + + assert.Nil(t, cl.Get(context.TODO(), testutils.ObjectKey(podToKeep), podToKeep)) + assert.True(t, k8serrors.IsNotFound(cl.Get(context.TODO(), testutils.ObjectKey(podToDelete), podToDelete))) + assert.True(t, k8serrors.IsNotFound(cl.Get(context.TODO(), testutils.ObjectKey(podToDeleteDefaultNS), podToDeleteDefaultNS))) +} + +func TestCheckResource(t *testing.T) { + for _, test := range []struct { + testName string + actual runtime.Object + expected runtime.Object + shouldError bool + }{ + { + testName: "resource matches", + actual: testutils.NewPod("hello", ""), + expected: testutils.NewPod("hello", ""), + }, + { + testName: "resource mis-match", + actual: testutils.NewPod("hello", ""), + expected: testutils.WithSpec(t, testutils.NewPod("hello", ""), map[string]interface{}{"invalid": "key"}), + shouldError: true, + }, + { + testName: "resource subset match", + actual: testutils.WithSpec(t, testutils.NewPod("hello", ""), map[string]interface{}{ + "ignored": "key", + "seen": "key", + }), + expected: testutils.WithSpec(t, testutils.NewPod("hello", ""), map[string]interface{}{ + "seen": "key", + }), + }, + { + testName: "resource does not exist", + actual: testutils.NewPod("other", ""), + expected: testutils.NewPod("hello", ""), + shouldError: true, + }, + } { + test := test + + t.Run(test.testName, func(t *testing.T) { + fakeDiscovery := testutils.FakeDiscoveryClient() + namespace := testNamespace + + _, _, err := testutils.Namespaced(fakeDiscovery, test.actual, namespace) + assert.Nil(t, err) + + step := Step{ + Logger: testutils.NewTestLogger(t, ""), + Client: func(bool) (client.Client, error) { return fake.NewFakeClient(test.actual), nil }, + DiscoveryClient: func() (discovery.DiscoveryInterface, error) { return fakeDiscovery, nil }, + } + + errors := step.CheckResource(test.expected, namespace) + + if test.shouldError { + assert.NotEqual(t, []error{}, errors) + } else { + assert.Equal(t, []error{}, errors) + } + }) + } +} + +func TestCheckResourceAbsent(t *testing.T) { + for _, test := range []struct { + name string + actual runtime.Object + expected runtime.Object + shouldError bool + }{ + { + name: "resource matches", + actual: testutils.NewPod("hello", ""), + expected: testutils.NewPod("hello", ""), + shouldError: true, + }, + { + name: "resource mis-match", + actual: testutils.NewPod("hello", ""), + expected: testutils.WithSpec(t, testutils.NewPod("hello", ""), map[string]interface{}{"invalid": "key"}), + }, + { + name: "resource does not exist", + actual: testutils.NewPod("other", ""), + expected: testutils.NewPod("hello", ""), + }, + } { + test := test + + t.Run(test.name, func(t *testing.T) { + fakeDiscovery := testutils.FakeDiscoveryClient() + + _, _, err := testutils.Namespaced(fakeDiscovery, test.actual, testNamespace) + assert.Nil(t, err) + + step := Step{ + Logger: testutils.NewTestLogger(t, ""), + Client: func(bool) (client.Client, error) { return fake.NewFakeClient(test.actual), nil }, + DiscoveryClient: func() (discovery.DiscoveryInterface, error) { return fakeDiscovery, nil }, + } + + error := step.CheckResourceAbsent(test.expected, testNamespace) + + if test.shouldError { + assert.NotNil(t, error) + } else { + assert.Nil(t, error) + } + }) + } +} + +func TestRun(t *testing.T) { + for _, test := range []struct { + testName string + shouldError bool + Step Step + updateMethod func(*testing.T, client.Client) + }{ + { + "successful run", false, Step{ + Apply: []runtime.Object{ + testutils.NewPod("hello", ""), + }, + Asserts: []runtime.Object{ + testutils.NewPod("hello", ""), + }, + }, nil, + }, + { + "failed run", true, Step{ + Apply: []runtime.Object{ + testutils.NewPod("hello", ""), + }, + Asserts: []runtime.Object{ + testutils.WithStatus(t, testutils.NewPod("hello", ""), map[string]interface{}{ + "phase": "Ready", + }), + }, + }, nil, + }, + { + "delayed run", false, Step{ + Apply: []runtime.Object{ + testutils.NewPod("hello", ""), + }, + Asserts: []runtime.Object{ + testutils.WithStatus(t, testutils.NewPod("hello", ""), map[string]interface{}{ + "phase": "Ready", + }), + }, + }, func(t *testing.T, client client.Client) { + // mock kubelet to set the pod status + assert.Nil(t, client.Update(context.TODO(), testutils.WithStatus(t, testutils.NewPod("hello", testNamespace), map[string]interface{}{ + "phase": "Ready", + }))) + }, + }, + } { + test := test + + t.Run(test.testName, func(t *testing.T) { + test.Step.Assert = &harness.TestAssert{ + Timeout: 1, + } + + cl := fake.NewFakeClient() + + test.Step.Client = func(bool) (client.Client, error) { return cl, nil } + test.Step.DiscoveryClient = func() (discovery.DiscoveryInterface, error) { return testutils.FakeDiscoveryClient(), nil } + test.Step.Logger = testutils.NewTestLogger(t, "") + + if test.updateMethod != nil { + test.Step.Assert.Timeout = 10 + + go func() { + time.Sleep(time.Second * 2) + test.updateMethod(t, cl) + }() + } + + errors := test.Step.Run(testNamespace) + + if test.shouldError { + assert.NotEqual(t, []error{}, errors) + } else { + assert.Equal(t, []error{}, errors) + } + }) + } +} diff --git a/pkg/test/test_data/cli-test/00-assert.yaml b/pkg/test/test_data/cli-test/00-assert.yaml new file mode 100644 index 00000000..8314c5c5 --- /dev/null +++ b/pkg/test/test_data/cli-test/00-assert.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Pod +metadata: + name: cli-test-pod diff --git a/pkg/test/test_data/cli-test/00-create-pod.yaml b/pkg/test/test_data/cli-test/00-create-pod.yaml new file mode 100644 index 00000000..a360c73e --- /dev/null +++ b/pkg/test/test_data/cli-test/00-create-pod.yaml @@ -0,0 +1,4 @@ +apiVersion: kudo.dev/v1beta1 +kind: TestStep +kubectl: +- apply -f ./test_data/pod.yaml diff --git a/pkg/test/test_data/cli-test/01-assert.yaml b/pkg/test/test_data/cli-test/01-assert.yaml new file mode 100644 index 00000000..57fff92f --- /dev/null +++ b/pkg/test/test_data/cli-test/01-assert.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Pod +metadata: + name: cli-test-pod + labels: + test: "true" diff --git a/pkg/test/test_data/cli-test/01-patch.yaml b/pkg/test/test_data/cli-test/01-patch.yaml new file mode 100644 index 00000000..76cdaa8e --- /dev/null +++ b/pkg/test/test_data/cli-test/01-patch.yaml @@ -0,0 +1,7 @@ +apiVersion: kudo.dev/v1beta1 +kind: TestStep +commands: +- command: kubectl label pod cli-test-pod test=true + namespaced: true +- command: kubectl command is bad + ignoreFailure: true diff --git a/pkg/test/test_data/cli-test/test_data/pod.yaml b/pkg/test/test_data/cli-test/test_data/pod.yaml new file mode 100644 index 00000000..5aeef1a4 --- /dev/null +++ b/pkg/test/test_data/cli-test/test_data/pod.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: cli-test-pod +spec: + containers: + - name: cli-test + image: alpine + restartPolicy: Never diff --git a/pkg/test/test_data/crd-in-step/00-assert.yaml b/pkg/test/test_data/crd-in-step/00-assert.yaml new file mode 100644 index 00000000..bf0b7943 --- /dev/null +++ b/pkg/test/test_data/crd-in-step/00-assert.yaml @@ -0,0 +1,18 @@ +# Expect the CRD to exist +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: mycrds.mycrd.k8s.io +status: + acceptedNames: + kind: MyCRD + listKind: MyCRDList + plural: mycrds + singular: mycrd + storedVersions: + - v1beta1 + conditions: + - type: NamesAccepted + status: "True" + - type: Established + status: "True" diff --git a/pkg/test/test_data/crd-in-step/00-crd.yaml b/pkg/test/test_data/crd-in-step/00-crd.yaml new file mode 100644 index 00000000..2a9b8d08 --- /dev/null +++ b/pkg/test/test_data/crd-in-step/00-crd.yaml @@ -0,0 +1,14 @@ +# Create a CRD +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: mycrds.mycrd.k8s.io +spec: + group: mycrd.k8s.io + version: v1beta1 + names: + kind: MyCRD + listKind: MyCRDList + plural: mycrds + singular: mycrd + scope: Namespaced diff --git a/pkg/test/test_data/crd-in-step/01-assert.yaml b/pkg/test/test_data/crd-in-step/01-assert.yaml new file mode 100644 index 00000000..3da40ded --- /dev/null +++ b/pkg/test/test_data/crd-in-step/01-assert.yaml @@ -0,0 +1,7 @@ +# Make sure that the instance we made gets created +apiVersion: mycrd.k8s.io/v1beta1 +kind: MyCRD +metadata: + name: my-crd +spec: + hi: abcdef diff --git a/pkg/test/test_data/crd-in-step/01-create-mycrd.yaml b/pkg/test/test_data/crd-in-step/01-create-mycrd.yaml new file mode 100644 index 00000000..14f87073 --- /dev/null +++ b/pkg/test/test_data/crd-in-step/01-create-mycrd.yaml @@ -0,0 +1,7 @@ +# Try to use the CRD we just created. +apiVersion: mycrd.k8s.io/v1beta1 +kind: MyCRD +metadata: + name: my-crd +spec: + hi: abcdef diff --git a/pkg/test/test_data/create-or-update/00-assert.yaml b/pkg/test/test_data/create-or-update/00-assert.yaml new file mode 100644 index 00000000..ce42f33b --- /dev/null +++ b/pkg/test/test_data/create-or-update/00-assert.yaml @@ -0,0 +1,17 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: prowjobs.prow.k8s.io +status: + acceptedNames: + kind: ProwJob + # InitialNamesAccepted will change from False -> True once the CRD is ready. + conditions: + - reason: NoConflicts + status: "True" + type: NamesAccepted + - reason: InitialNamesAccepted + status: "True" + type: Established + storedVersions: + - v1 diff --git a/pkg/test/test_data/create-or-update/00-crd.yaml b/pkg/test/test_data/create-or-update/00-crd.yaml new file mode 100644 index 00000000..9ff1d562 --- /dev/null +++ b/pkg/test/test_data/create-or-update/00-crd.yaml @@ -0,0 +1,49 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: prowjobs.prow.k8s.io +spec: + group: prow.k8s.io + version: v1 + names: + kind: ProwJob + singular: prowjob + plural: prowjobs + scope: Namespaced + additionalPrinterColumns: + - name: Job + type: string + description: The name of the job being run. + JSONPath: .spec.job + - name: BuildId + type: string + description: The ID of the job being run. + JSONPath: .status.build_id + - name: Type + type: string + description: The type of job being run. + JSONPath: .spec.type + - name: Org + type: string + description: The org for which the job is running. + JSONPath: .spec.refs.org + - name: Repo + type: string + description: The repo for which the job is running. + JSONPath: .spec.refs.repo + - name: Pulls + type: string + description: The pulls for which the job is running. + JSONPath: ".spec.refs.pulls[*].number" + - name: StartTime + type: date + description: When the job started running. + JSONPath: .status.startTime + - name: CompletionTime + type: date + description: When the job finished running. + JSONPath: .status.completionTime + - name: State + description: The state of the job. + type: string + JSONPath: .status.state diff --git a/pkg/test/test_data/create-or-update/01-assert.yaml b/pkg/test/test_data/create-or-update/01-assert.yaml new file mode 100644 index 00000000..136a4eab --- /dev/null +++ b/pkg/test/test_data/create-or-update/01-assert.yaml @@ -0,0 +1,47 @@ +apiVersion: v1 +kind: Service +metadata: + name: svc +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 443 + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.7.9 + ports: + - containerPort: 80 +--- +apiVersion: prow.k8s.io/v1 +kind: ProwJob +metadata: + name: my-job +spec: + agent: kubernetes + cluster: default + pod_spec: + containers: + - command: + - test + image: alpine + imagePullPolicy: Always diff --git a/pkg/test/test_data/create-or-update/01-create-deployment.yaml b/pkg/test/test_data/create-or-update/01-create-deployment.yaml new file mode 100644 index 00000000..f7f95dee --- /dev/null +++ b/pkg/test/test_data/create-or-update/01-create-deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.7.9 + ports: + - containerPort: 80 diff --git a/pkg/test/test_data/create-or-update/01-create-service.yaml b/pkg/test/test_data/create-or-update/01-create-service.yaml new file mode 100644 index 00000000..12e8ff2d --- /dev/null +++ b/pkg/test/test_data/create-or-update/01-create-service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: svc +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 443 + type: ClusterIP diff --git a/pkg/test/test_data/create-or-update/01-create-unknown-crd.yaml b/pkg/test/test_data/create-or-update/01-create-unknown-crd.yaml new file mode 100644 index 00000000..37cdadde --- /dev/null +++ b/pkg/test/test_data/create-or-update/01-create-unknown-crd.yaml @@ -0,0 +1,13 @@ +apiVersion: prow.k8s.io/v1 +kind: ProwJob +metadata: + name: my-job +spec: + agent: kubernetes + cluster: default + pod_spec: + containers: + - command: + - test + image: alpine + imagePullPolicy: Always diff --git a/pkg/test/test_data/create-or-update/02-assert.yaml b/pkg/test/test_data/create-or-update/02-assert.yaml new file mode 100644 index 00000000..0a1dbff7 --- /dev/null +++ b/pkg/test/test_data/create-or-update/02-assert.yaml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 2 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.7.9 + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: svc +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 80 + type: ClusterIP +--- +apiVersion: prow.k8s.io/v1 +kind: ProwJob +metadata: + name: my-job +spec: + agent: kubernetes + cluster: default + pod_spec: + containers: + - command: + - test + image: alpine:1234 + imagePullPolicy: Always diff --git a/pkg/test/test_data/create-or-update/02-update-deployment.yaml b/pkg/test/test_data/create-or-update/02-update-deployment.yaml new file mode 100644 index 00000000..c18d0568 --- /dev/null +++ b/pkg/test/test_data/create-or-update/02-update-deployment.yaml @@ -0,0 +1,6 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 2 diff --git a/pkg/test/test_data/create-or-update/02-update-service.yaml b/pkg/test/test_data/create-or-update/02-update-service.yaml new file mode 100644 index 00000000..b7c8c3e6 --- /dev/null +++ b/pkg/test/test_data/create-or-update/02-update-service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: svc +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 80 + type: ClusterIP diff --git a/pkg/test/test_data/create-or-update/02-update-unknown-crd.yaml b/pkg/test/test_data/create-or-update/02-update-unknown-crd.yaml new file mode 100644 index 00000000..f3b49133 --- /dev/null +++ b/pkg/test/test_data/create-or-update/02-update-unknown-crd.yaml @@ -0,0 +1,11 @@ +apiVersion: prow.k8s.io/v1 +kind: ProwJob +metadata: + name: my-job +spec: + pod_spec: + containers: + - image: alpine:1234 + command: + - test + imagePullPolicy: Always diff --git a/pkg/test/test_data/delete-in-step/00-assert.yaml b/pkg/test/test_data/delete-in-step/00-assert.yaml new file mode 100644 index 00000000..db747133 --- /dev/null +++ b/pkg/test/test_data/delete-in-step/00-assert.yaml @@ -0,0 +1,14 @@ +# Verify that the pods were created. +apiVersion: v1 +kind: Pod +metadata: + name: hello +status: + phase: Pending +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello2 +status: + phase: Pending diff --git a/pkg/test/test_data/delete-in-step/00-create.yaml b/pkg/test/test_data/delete-in-step/00-create.yaml new file mode 100644 index 00000000..f9c930bb --- /dev/null +++ b/pkg/test/test_data/delete-in-step/00-create.yaml @@ -0,0 +1,18 @@ +# Create some pods. Pod specs are immutable, so we can use these to verify that the resources were deleted. +apiVersion: v1 +kind: Pod +metadata: + name: hello +spec: + containers: + - image: alpine + name: test +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello2 +spec: + containers: + - image: alpine + name: test diff --git a/pkg/test/test_data/delete-in-step/01-assert.yaml b/pkg/test/test_data/delete-in-step/01-assert.yaml new file mode 100644 index 00000000..84e12994 --- /dev/null +++ b/pkg/test/test_data/delete-in-step/01-assert.yaml @@ -0,0 +1,22 @@ +# Verify that the pods are created. +apiVersion: v1 +kind: Pod +metadata: + name: hello +spec: + containers: + - image: alpine2 + name: test +status: + phase: Pending +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello2 +spec: + containers: + - image: alpine2 + name: test +status: + phase: Pending diff --git a/pkg/test/test_data/delete-in-step/01-create.yaml b/pkg/test/test_data/delete-in-step/01-create.yaml new file mode 100644 index 00000000..6074d6b0 --- /dev/null +++ b/pkg/test/test_data/delete-in-step/01-create.yaml @@ -0,0 +1,25 @@ +# Delete all pods and then make sure that we can make pods with the same name but different specs (this would fail if they were +# not deleted). +apiVersion: kudo.dev/v1beta1 +kind: TestStep +delete: +- apiVersion: v1 + kind: Pod +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello +spec: + containers: + - image: alpine2 + name: test +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello2 +spec: + containers: + - image: alpine2 + name: test diff --git a/pkg/test/test_data/list-pods/00-assert.yaml b/pkg/test/test_data/list-pods/00-assert.yaml new file mode 100644 index 00000000..a51a3b68 --- /dev/null +++ b/pkg/test/test_data/list-pods/00-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + labels: + app: nginx +spec: + containers: + - name: nginx + image: nginx:1.7.9 diff --git a/pkg/test/test_data/list-pods/00-pod.yaml b/pkg/test/test_data/list-pods/00-pod.yaml new file mode 100644 index 00000000..e564c7d9 --- /dev/null +++ b/pkg/test/test_data/list-pods/00-pod.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-1 + labels: + app: nginx +spec: + containers: + - name: nginx + image: nginx:1.7.9 diff --git a/pkg/test/test_data/with-overrides/00-assert.yaml b/pkg/test/test_data/with-overrides/00-assert.yaml new file mode 100644 index 00000000..b89c1ebe --- /dev/null +++ b/pkg/test/test_data/with-overrides/00-assert.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test +status: + qosClass: BestEffort diff --git a/pkg/test/test_data/with-overrides/00-test-step.yaml b/pkg/test/test_data/with-overrides/00-test-step.yaml new file mode 100644 index 00000000..4e28de3b --- /dev/null +++ b/pkg/test/test_data/with-overrides/00-test-step.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test +spec: + restartPolicy: Never + containers: + - name: nginx + image: nginx:1.7.9 +--- +apiVersion: kudo.dev/v1beta1 +kind: TestStep +metadata: + name: with-test-step-name-override diff --git a/pkg/test/test_data/with-overrides/01-assert.yaml b/pkg/test/test_data/with-overrides/01-assert.yaml new file mode 100644 index 00000000..75a5bc68 --- /dev/null +++ b/pkg/test/test_data/with-overrides/01-assert.yaml @@ -0,0 +1,10 @@ +apiVersion: kudo.dev/v1beta1 +kind: TestAssert +timeout: 20 +--- +apiVersion: v1 +kind: Pod +metadata: + name: test2 +status: + qosClass: BestEffort diff --git a/pkg/test/test_data/with-overrides/01-test-assert.yaml b/pkg/test/test_data/with-overrides/01-test-assert.yaml new file mode 100644 index 00000000..cf7b8541 --- /dev/null +++ b/pkg/test/test_data/with-overrides/01-test-assert.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test2 +spec: + restartPolicy: Never + containers: + - name: nginx + image: nginx:1.7.9 +--- +apiVersion: kudo.dev/v1beta1 +kind: TestStep +delete: +- apiVersion: v1 + kind: Pod + name: test diff --git a/pkg/test/test_data/with-overrides/02-directory/assert.yaml b/pkg/test/test_data/with-overrides/02-directory/assert.yaml new file mode 100644 index 00000000..4074c3d7 --- /dev/null +++ b/pkg/test/test_data/with-overrides/02-directory/assert.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test3 +status: + qosClass: BestEffort diff --git a/pkg/test/test_data/with-overrides/02-directory/pod.yaml b/pkg/test/test_data/with-overrides/02-directory/pod.yaml new file mode 100644 index 00000000..8f3072f3 --- /dev/null +++ b/pkg/test/test_data/with-overrides/02-directory/pod.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test4 +spec: + containers: + - name: nginx + image: nginx:1.7.9 diff --git a/pkg/test/test_data/with-overrides/02-directory/pod2.yaml b/pkg/test/test_data/with-overrides/02-directory/pod2.yaml new file mode 100644 index 00000000..a5fc0fd7 --- /dev/null +++ b/pkg/test/test_data/with-overrides/02-directory/pod2.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test3 +spec: + containers: + - name: nginx + image: nginx:1.7.9 diff --git a/pkg/test/test_data/with-overrides/03-assert.yaml b/pkg/test/test_data/with-overrides/03-assert.yaml new file mode 100644 index 00000000..d8a5e0e6 --- /dev/null +++ b/pkg/test/test_data/with-overrides/03-assert.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test5 +spec: + restartPolicy: Never diff --git a/pkg/test/test_data/with-overrides/03-pod.yaml b/pkg/test/test_data/with-overrides/03-pod.yaml new file mode 100644 index 00000000..0a3bd8d9 --- /dev/null +++ b/pkg/test/test_data/with-overrides/03-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: kudo.dev/v1beta1 +kind: TestStep +metadata: + name: name-overridden +--- +apiVersion: v1 +kind: Pod +metadata: + name: test6 +spec: + restartPolicy: Never + containers: + - name: nginx + image: nginx:1.7.9 diff --git a/pkg/test/test_data/with-overrides/03-pod2.yaml b/pkg/test/test_data/with-overrides/03-pod2.yaml new file mode 100644 index 00000000..4ea75c44 --- /dev/null +++ b/pkg/test/test_data/with-overrides/03-pod2.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test5 +spec: + restartPolicy: Never + containers: + - name: nginx + image: nginx:1.7.9 diff --git a/pkg/test/test_data/with-overrides/README.md b/pkg/test/test_data/with-overrides/README.md new file mode 100644 index 00000000..e69de29b diff --git a/pkg/test/utils/docker.go b/pkg/test/utils/docker.go new file mode 100644 index 00000000..5349b731 --- /dev/null +++ b/pkg/test/utils/docker.go @@ -0,0 +1,16 @@ +package utils + +import ( + "context" + "io" + + dockertypes "github.com/docker/docker/api/types" + volumetypes "github.com/docker/docker/api/types/volume" +) + +// DockerClient is a wrapper interface for the Docker library to support unit testing. +type DockerClient interface { + NegotiateAPIVersion(context.Context) + VolumeCreate(context.Context, volumetypes.VolumeCreateBody) (dockertypes.Volume, error) + ImageSave(context.Context, []string) (io.ReadCloser, error) +} diff --git a/pkg/test/utils/kubernetes.go b/pkg/test/utils/kubernetes.go new file mode 100644 index 00000000..df17115b --- /dev/null +++ b/pkg/test/utils/kubernetes.go @@ -0,0 +1,1089 @@ +package utils + +// Contains methods helpful for interacting with and manipulating Kubernetes resources from YAML. + +import ( + "bufio" + "bytes" + "context" + ejson "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/google/shlex" + "github.com/pmezard/go-difflib/difflib" + "github.com/spf13/pflag" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + batchv1beta1 "k8s.io/api/batch/v1beta1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/apimachinery/pkg/types" + apijson "k8s.io/apimachinery/pkg/util/json" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" + coretesting "k8s.io/client-go/testing" + api "k8s.io/client-go/tools/clientcmd/api/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/kudobuilder/kuttl/pkg/apis" + harness "github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1" +) + +// ensure that we only add to the scheme once. +var schemeLock sync.Once + +// IsJSONSyntaxError returns true if the error is a JSON syntax error. +func IsJSONSyntaxError(err error) bool { + _, ok := err.(*ejson.SyntaxError) + return ok +} + +// ValidateErrors accepts an error as its first argument and passes it to each function in the errValidationFuncs slice, +// if any of the methods returns true, the method returns nil, otherwise it returns the original error. +func ValidateErrors(err error, errValidationFuncs ...func(error) bool) error { + for _, errFunc := range errValidationFuncs { + if errFunc(err) { + return nil + } + } + + return err +} + +// Retry retries a method until the context expires or the method returns an unvalidated error. +func Retry(ctx context.Context, fn func(context.Context) error, errValidationFuncs ...func(error) bool) error { + var lastErr error + errCh := make(chan error) + doneCh := make(chan struct{}) + + // do { } while (err != nil): https://stackoverflow.com/a/32844744/10892393 + for ok := true; ok; ok = lastErr != nil { + // run the function in a goroutine and close it once it is finished so that + // we can use select to wait for both the function return and the context deadline. + + go func() { + if err := fn(ctx); err != nil { + errCh <- err + } else { + doneCh <- struct{}{} + } + }() + + select { + // the callback finished + case <-doneCh: + lastErr = nil + case err := <-errCh: + // check if we tolerate the error, return it if not. + if e := ValidateErrors(err, errValidationFuncs...); e != nil { + return e + } + lastErr = err + // timeout exceeded + case <-ctx.Done(): + if lastErr == nil { + // there's no previous error, so just return the timeout error + return ctx.Err() + } + + // return the most recent error + return lastErr + } + } + + return lastErr +} + +// RetryClient implements the Client interface, with retries built in. +type RetryClient struct { + Client client.Client + dynamic dynamic.Interface + discovery discovery.DiscoveryInterface +} + +// RetryStatusWriter implements the StatusWriter interface, with retries built in. +type RetryStatusWriter struct { + StatusWriter client.StatusWriter +} + +// NewRetryClient initializes a new Kubernetes client that automatically retries on network-related errors. +func NewRetryClient(cfg *rest.Config, opts client.Options) (*RetryClient, error) { + dynamicClient, err := dynamic.NewForConfig(cfg) + if err != nil { + return nil, err + } + + discovery, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + return nil, err + } + + if opts.Mapper == nil { + opts.Mapper, err = apiutil.NewDynamicRESTMapper(cfg) + if err != nil { + return nil, err + } + } + + client, err := client.New(cfg, opts) + return &RetryClient{Client: client, dynamic: dynamicClient, discovery: discovery}, err +} + +// Create saves the object obj in the Kubernetes cluster. +func (r *RetryClient) Create(ctx context.Context, obj runtime.Object, opts ...client.CreateOption) error { + return Retry(ctx, func(ctx context.Context) error { + return r.Client.Create(ctx, obj, opts...) + }, IsJSONSyntaxError) +} + +// Delete deletes the given obj from Kubernetes cluster. +func (r *RetryClient) Delete(ctx context.Context, obj runtime.Object, opts ...client.DeleteOption) error { + return Retry(ctx, func(ctx context.Context) error { + return r.Client.Delete(ctx, obj, opts...) + }, IsJSONSyntaxError) +} + +// DeleteAllOf deletes the given obj from Kubernetes cluster. +func (r *RetryClient) DeleteAllOf(ctx context.Context, obj runtime.Object, opts ...client.DeleteAllOfOption) error { + return Retry(ctx, func(ctx context.Context) error { + return r.Client.DeleteAllOf(ctx, obj, opts...) + }, IsJSONSyntaxError) +} + +// Update updates the given obj in the Kubernetes cluster. obj must be a +// struct pointer so that obj can be updated with the content returned by the Server. +func (r *RetryClient) Update(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error { + return Retry(ctx, func(ctx context.Context) error { + return r.Client.Update(ctx, obj, opts...) + }, IsJSONSyntaxError) +} + +// Patch patches the given obj in the Kubernetes cluster. obj must be a +// struct pointer so that obj can be updated with the content returned by the Server. +func (r *RetryClient) Patch(ctx context.Context, obj runtime.Object, patch client.Patch, opts ...client.PatchOption) error { + return Retry(ctx, func(ctx context.Context) error { + return r.Client.Patch(ctx, obj, patch, opts...) + }, IsJSONSyntaxError) +} + +// Get retrieves an obj for the given object key from the Kubernetes Cluster. +// obj must be a struct pointer so that obj can be updated with the response +// returned by the Server. +func (r *RetryClient) Get(ctx context.Context, key client.ObjectKey, obj runtime.Object) error { + return Retry(ctx, func(ctx context.Context) error { + return r.Client.Get(ctx, key, obj) + }, IsJSONSyntaxError) +} + +// List retrieves list of objects for a given namespace and list options. On a +// successful call, Items field in the list will be populated with the +// result returned from the server. +func (r *RetryClient) List(ctx context.Context, list runtime.Object, opts ...client.ListOption) error { + return Retry(ctx, func(ctx context.Context) error { + return r.Client.List(ctx, list, opts...) + }, IsJSONSyntaxError) +} + +// Watch watches a specific object and returns all events for it. +func (r *RetryClient) Watch(ctx context.Context, obj runtime.Object) (watch.Interface, error) { + meta, err := meta.Accessor(obj) + if err != nil { + return nil, err + } + + gvk := obj.GetObjectKind().GroupVersionKind() + + groupResources, err := restmapper.GetAPIGroupResources(r.discovery) + if err != nil { + return nil, err + } + + mapping, err := restmapper.NewDiscoveryRESTMapper(groupResources).RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return nil, err + } + + return r.dynamic.Resource(mapping.Resource).Watch(metav1.SingleObject(metav1.ObjectMeta{ + Name: meta.GetName(), + Namespace: meta.GetNamespace(), + })) +} + +// Status returns a client which can update status subresource for kubernetes objects. +func (r *RetryClient) Status() client.StatusWriter { + return &RetryStatusWriter{ + StatusWriter: r.Client.Status(), + } +} + +// Update updates the given obj in the Kubernetes cluster. obj must be a +// struct pointer so that obj can be updated with the content returned by the Server. +func (r *RetryStatusWriter) Update(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error { + return Retry(ctx, func(ctx context.Context) error { + return r.StatusWriter.Update(ctx, obj, opts...) + }, IsJSONSyntaxError) +} + +// Patch patches the given obj in the Kubernetes cluster. obj must be a +// struct pointer so that obj can be updated with the content returned by the Server. +func (r *RetryStatusWriter) Patch(ctx context.Context, obj runtime.Object, patch client.Patch, opts ...client.PatchOption) error { + return Retry(ctx, func(ctx context.Context) error { + return r.StatusWriter.Patch(ctx, obj, patch, opts...) + }, IsJSONSyntaxError) +} + +// Scheme returns an initialized Kubernetes Scheme. +func Scheme() *runtime.Scheme { + schemeLock.Do(func() { + if err := apis.AddToScheme(scheme.Scheme); err != nil { + fmt.Printf("failed to add API resources to the scheme: %v", err) + os.Exit(-1) + } + if err := apiextensions.AddToScheme(scheme.Scheme); err != nil { + fmt.Printf("failed to add API extension resources to the scheme: %v", err) + os.Exit(-1) + } + }) + + return scheme.Scheme +} + +// ResourceID returns a human readable identifier indicating the object kind, name, and namespace. +func ResourceID(obj runtime.Object) string { + m, err := meta.Accessor(obj) + if err != nil { + return "" + } + + gvk := obj.GetObjectKind().GroupVersionKind() + + return fmt.Sprintf("%s:%s/%s", gvk.Kind, m.GetNamespace(), m.GetName()) +} + +// Namespaced sets the namespace on an object to namespace, if it is a namespace scoped resource. +// If the resource is cluster scoped, then it is ignored and the namespace is not set. +// If it is a namespaced resource and a namespace is already set, then the namespace is unchanged. +func Namespaced(dClient discovery.DiscoveryInterface, obj runtime.Object, namespace string) (string, string, error) { + m, err := meta.Accessor(obj) + if err != nil { + return "", "", err + } + + if m.GetNamespace() != "" { + return m.GetName(), m.GetNamespace(), nil + } + + resource, err := GetAPIResource(dClient, obj.GetObjectKind().GroupVersionKind()) + if err != nil { + + return "", "", err + } + + if !resource.Namespaced { + return m.GetName(), "", nil + } + + m.SetNamespace(namespace) + return m.GetName(), namespace, nil +} + +// PrettyDiff creates a unified diff highlighting the differences between two Kubernetes resources +func PrettyDiff(expected runtime.Object, actual runtime.Object) (string, error) { + expectedBuf := &bytes.Buffer{} + actualBuf := &bytes.Buffer{} + + if err := MarshalObject(expected, expectedBuf); err != nil { + return "", err + } + + if err := MarshalObject(actual, actualBuf); err != nil { + return "", err + } + + diffed := difflib.UnifiedDiff{ + A: difflib.SplitLines(expectedBuf.String()), + B: difflib.SplitLines(actualBuf.String()), + FromFile: ResourceID(expected), + ToFile: ResourceID(actual), + Context: 3, + } + + return difflib.GetUnifiedDiffString(diffed) +} + +// ConvertUnstructured converts an unstructured object to the known struct. If the type is not known, then +// the unstructured object is returned unmodified. +func ConvertUnstructured(in runtime.Object) (runtime.Object, error) { + unstruct, err := runtime.DefaultUnstructuredConverter.ToUnstructured(in) + if err != nil { + return nil, fmt.Errorf("error converting %s to unstructured error: %w", ResourceID(in), err) + } + + var converted runtime.Object + + kind := in.GetObjectKind().GroupVersionKind().Kind + group := in.GetObjectKind().GroupVersionKind().Group + + kudoGroup := "kudo.dev" + if group == kudoGroup && kind == "TestStep" { + converted = &harness.TestStep{} + } else if group == kudoGroup && kind == "TestAssert" { + converted = &harness.TestAssert{} + } else if group == kudoGroup && kind == "TestSuite" { + converted = &harness.TestSuite{} + } else { + return in, nil + } + + err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstruct, converted) + if err != nil { + return nil, fmt.Errorf("error converting %s from unstructured error: %w", ResourceID(in), err) + } + + return converted, nil +} + +// PatchObject updates expected with the Resource Version from actual. +// In the future, PatchObject may perform a strategic merge of actual into expected. +func PatchObject(actual, expected runtime.Object) error { + actualMeta, err := meta.Accessor(actual) + if err != nil { + return err + } + + expectedMeta, err := meta.Accessor(expected) + if err != nil { + return err + } + + expectedMeta.SetResourceVersion(actualMeta.GetResourceVersion()) + return nil +} + +// CleanObjectForMarshalling removes unnecessary object metadata that should not be included in serialization and diffs. +func CleanObjectForMarshalling(o runtime.Object) (runtime.Object, error) { + copied := o.DeepCopyObject() + + meta, err := meta.Accessor(copied) + if err != nil { + return nil, err + } + + meta.SetResourceVersion("") + meta.SetCreationTimestamp(metav1.Time{}) + meta.SetSelfLink("") + meta.SetUID(types.UID("")) + meta.SetGeneration(0) + + annotations := meta.GetAnnotations() + delete(annotations, "deployment.kubernetes.io/revision") + + if len(annotations) > 0 { + meta.SetAnnotations(annotations) + } else { + meta.SetAnnotations(nil) + } + + return copied, nil +} + +// MarshalObject marshals a Kubernetes object to a YAML string. +func MarshalObject(o runtime.Object, w io.Writer) error { + copied, err := CleanObjectForMarshalling(o) + if err != nil { + return err + } + + return json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil).Encode(copied, w) +} + +// MarshalObjectJSON marshals a Kubernetes object to a JSON string. +func MarshalObjectJSON(o runtime.Object, w io.Writer) error { + copied, err := CleanObjectForMarshalling(o) + if err != nil { + return err + } + + return json.NewSerializer(json.DefaultMetaFactory, nil, nil, false).Encode(copied, w) +} + +// LoadYAML loads all objects from a YAML file. +func LoadYAML(path string) ([]runtime.Object, error) { + opened, err := os.Open(path) + if err != nil { + return nil, err + } + defer opened.Close() + + yamlReader := yaml.NewYAMLReader(bufio.NewReader(opened)) + + objects := []runtime.Object{} + + for { + data, err := yamlReader.Read() + if err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("error reading yaml %s: %w", path, err) + } + + unstructuredObj := &unstructured.Unstructured{} + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(data), len(data)) + + if err = decoder.Decode(unstructuredObj); err != nil { + return nil, fmt.Errorf("error decoding yaml %s: %w", path, err) + } + + obj, err := ConvertUnstructured(unstructuredObj) + if err != nil { + return nil, fmt.Errorf("error converting unstructured object %s (%s): %w", ResourceID(unstructuredObj), path, err) + } + + objects = append(objects, obj) + } + + return objects, nil +} + +// MatchesKind returns true if the Kubernetes kind of obj matches any of kinds. +func MatchesKind(obj runtime.Object, kinds ...runtime.Object) bool { + gvk := obj.GetObjectKind().GroupVersionKind() + + for _, kind := range kinds { + if kind.GetObjectKind().GroupVersionKind() == gvk { + return true + } + } + + return false +} + +// InstallManifests recurses over ManifestsDir to install all resources defined in YAML manifests. +func InstallManifests(ctx context.Context, client client.Client, dClient discovery.DiscoveryInterface, manifestsDir string, kinds ...runtime.Object) ([]runtime.Object, error) { + objects := []runtime.Object{} + + if manifestsDir == "" { + return objects, nil + } + + return objects, filepath.Walk(manifestsDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + extensions := map[string]bool{ + ".yaml": true, + ".yml": true, + ".json": true, + } + if !extensions[filepath.Ext(path)] { + return nil + } + + objs, err := LoadYAML(path) + if err != nil { + return err + } + + for _, obj := range objs { + if len(kinds) > 0 && !MatchesKind(obj, kinds...) { + continue + } + + objectKey := ObjectKey(obj) + if objectKey.Namespace == "" { + if _, _, err := Namespaced(dClient, obj, "default"); err != nil { + return err + } + } + + updated, err := CreateOrUpdate(ctx, client, obj, true) + if err != nil { + return fmt.Errorf("error creating resource %s: %w", ResourceID(obj), err) + } + + action := "created" + if updated { + action = "updated" + } + // TODO: use test logger instead of Go logger + log.Println(ResourceID(obj), action) + + objects = append(objects, obj) + } + + return nil + }) +} + +// ObjectKey returns an instantiated ObjectKey for the provided object. +func ObjectKey(obj runtime.Object) client.ObjectKey { + m, _ := meta.Accessor(obj) + return client.ObjectKey{ + Name: m.GetName(), + Namespace: m.GetNamespace(), + } +} + +// NewResource generates a Kubernetes object using the provided apiVersion, kind, name, and namespace. +func NewResource(apiVersion, kind, name, namespace string) runtime.Object { + meta := map[string]interface{}{ + "name": name, + } + + if namespace != "" { + meta["namespace"] = namespace + } + + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": meta, + }, + } +} + +// NewClusterRoleBinding Create a clusterrolebinding for the serviceAccount passed +func NewClusterRoleBinding(apiVersion, kind, name, namespace string, serviceAccount string, roleName string) runtime.Object { + + sa := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: roleName, + }, + Subjects: []rbacv1.Subject{{ + Kind: "ServiceAccount", + Name: serviceAccount, + Namespace: namespace, + }}, + } + + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": sa.ObjectMeta, + "subjects": sa.Subjects, + "roleRef": sa.RoleRef, + }, + } +} + +// NewPod creates a new pod object. +func NewPod(name, namespace string) runtime.Object { + return NewResource("v1", "Pod", name, namespace) +} + +// WithNamespace naively applies the namespace to the object. Used mainly in tests, otherwise +// use Namespaced. +func WithNamespace(obj runtime.Object, namespace string) runtime.Object { + obj = obj.DeepCopyObject() + + m, _ := meta.Accessor(obj) + m.SetNamespace(namespace) + + return obj +} + +// WithSpec applies the provided spec to the Kubernetes object. +func WithSpec(t *testing.T, obj runtime.Object, spec map[string]interface{}) runtime.Object { + res, err := WithKeyValue(obj, "spec", spec) + if err != nil { + t.Fatalf("failed to apply spec %v to object %v: %v", spec, obj, err) + } + return res +} + +// WithStatus applies the provided status to the Kubernetes object. +func WithStatus(t *testing.T, obj runtime.Object, status map[string]interface{}) runtime.Object { + res, err := WithKeyValue(obj, "status", status) + if err != nil { + t.Fatalf("failed to apply status %v to object %v: %v", status, obj, err) + } + return res +} + +// WithKeyValue sets key in the provided object to value. +func WithKeyValue(obj runtime.Object, key string, value map[string]interface{}) (runtime.Object, error) { + obj = obj.DeepCopyObject() + + content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + + content[key] = value + + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(content, obj); err != nil { + return nil, err + } + return obj, nil +} + +// WithLabels sets the labels on an object. +func WithLabels(t *testing.T, obj runtime.Object, labels map[string]string) runtime.Object { + obj = obj.DeepCopyObject() + + m, err := meta.Accessor(obj) + if err != nil { + t.Fatalf("failed to apply labels %v to object %v: %v", labels, obj, err) + } + m.SetLabels(labels) + + return obj +} + +// WithAnnotations sets the annotations on an object. +func WithAnnotations(obj runtime.Object, annotations map[string]string) runtime.Object { + obj = obj.DeepCopyObject() + + m, _ := meta.Accessor(obj) + m.SetAnnotations(annotations) + + return obj +} + +// FakeDiscoveryClient returns a fake discovery client that is populated with some types for use in +// unit tests. +func FakeDiscoveryClient() discovery.DiscoveryInterface { + return &fakediscovery.FakeDiscovery{ + Fake: &coretesting.Fake{ + Resources: []*metav1.APIResourceList{ + { + GroupVersion: corev1.SchemeGroupVersion.String(), + APIResources: []metav1.APIResource{ + {Name: "pod", Namespaced: true, Kind: "Pod"}, + {Name: "namespace", Namespaced: false, Kind: "Namespace"}, + {Name: "service", Namespaced: true, Kind: "Service"}, + }, + }, + { + GroupVersion: appsv1.SchemeGroupVersion.String(), + APIResources: []metav1.APIResource{ + {Name: "statefulset", Namespaced: true, Kind: "StatefulSet"}, + {Name: "deployment", Namespaced: true, Kind: "Deployment"}, + }, + }, + { + GroupVersion: batchv1.SchemeGroupVersion.String(), + APIResources: []metav1.APIResource{ + {Name: "job", Namespaced: true, Kind: "Job"}, + }, + }, + { + GroupVersion: batchv1beta1.SchemeGroupVersion.String(), + APIResources: []metav1.APIResource{ + {Name: "job", Namespaced: true, Kind: "CronJob"}, + }, + }, + { + GroupVersion: apiextensions.SchemeGroupVersion.String(), + APIResources: []metav1.APIResource{ + {Name: "customresourcedefinitions", Namespaced: false, Kind: "CustomResourceDefinition"}, + }, + }, + }, + }, + } +} + +// CreateOrUpdate will create obj if it does not exist and update if it it does. +// retryonerror indicates whether we retry in case of conflict +// Returns true if the object was updated and false if it was created. +func CreateOrUpdate(ctx context.Context, cl client.Client, obj runtime.Object, retryOnError bool) (updated bool, err error) { + orig := obj.DeepCopyObject() + + validators := []func(err error) bool{k8serrors.IsAlreadyExists} + + if retryOnError { + validators = append(validators, k8serrors.IsConflict) + } + + return updated, Retry(ctx, func(ctx context.Context) error { + expected := orig.DeepCopyObject() + actual := orig.DeepCopyObject() + + err := cl.Get(ctx, ObjectKey(actual), actual) + if err == nil { + if err = PatchObject(actual, expected); err != nil { + return err + } + + var expectedBytes []byte + expectedBytes, err = apijson.Marshal(expected) + if err != nil { + return err + } + + err = cl.Patch(ctx, actual, client.ConstantPatch(types.MergePatchType, expectedBytes)) + updated = true + } else if k8serrors.IsNotFound(err) { + err = cl.Create(ctx, obj) + updated = false + } + return err + }, validators...) +} + +// SetAnnotation sets the given key and value in the object's annotations, returning a copy. +func SetAnnotation(obj runtime.Object, key, value string) runtime.Object { + obj = obj.DeepCopyObject() + + meta, _ := meta.Accessor(obj) + + annotations := meta.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[key] = value + meta.SetAnnotations(annotations) + + return obj +} + +// GetAPIResource returns the APIResource object for a specific GroupVersionKind. +func GetAPIResource(dClient discovery.DiscoveryInterface, gvk schema.GroupVersionKind) (metav1.APIResource, error) { + resourceTypes, err := dClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) + if err != nil { + return metav1.APIResource{}, err + } + + for _, resource := range resourceTypes.APIResources { + if !strings.EqualFold(resource.Kind, gvk.Kind) { + continue + } + + return resource, nil + } + + return metav1.APIResource{}, errors.New("resource type not found") +} + +// WaitForDelete waits for the provide runtime objects to be deleted from cluster +func WaitForDelete(c *RetryClient, objs []runtime.Object) error { + // Wait for resources to be deleted. + return wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (done bool, err error) { + for _, obj := range objs { + err = c.Get(context.TODO(), ObjectKey(obj), obj.DeepCopyObject()) + if err == nil || !k8serrors.IsNotFound(err) { + return false, err + } + } + + return true, nil + }) +} + +// WaitForCRDs waits for the provided CRD types to be available in the Kubernetes API. +func WaitForCRDs(dClient discovery.DiscoveryInterface, crds []runtime.Object) error { + waitingFor := []schema.GroupVersionKind{} + crdKind := NewResource("apiextensions.k8s.io/v1beta1", "CustomResourceDefinition", "", "") + + for _, crdObj := range crds { + if !MatchesKind(crdObj, crdKind) { + return fmt.Errorf("the following passed object does not match %v: %v", crdKind, crdObj) + } + + switch crd := crdObj.(type) { + case *apiextensions.CustomResourceDefinition: + waitingFor = append(waitingFor, schema.GroupVersionKind{ + Group: crd.Spec.Group, + Version: crd.Spec.Version, + Kind: crd.Spec.Names.Kind, + }) + case *unstructured.Unstructured: + waitingFor = append(waitingFor, schema.GroupVersionKind{ + Group: crd.Object["spec"].(map[string]interface{})["group"].(string), + Version: crd.Object["spec"].(map[string]interface{})["version"].(string), + Kind: crd.Object["spec"].(map[string]interface{})["names"].(map[string]interface{})["kind"].(string), + }) + default: + return fmt.Errorf("the folllowing passed object is not a CRD: %v", crdObj) + } + } + + return wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (done bool, err error) { + for _, resource := range waitingFor { + _, err := GetAPIResource(dClient, resource) + if err != nil { + fmt.Printf("Waiting for resource %s... \n", resource) + return false, nil + } + } + + return true, nil + }) +} + +// Client is the controller-runtime Client interface with an added Watch method. +type Client interface { + client.Client + // Watch watches a specific object and returns all events for it. + Watch(ctx context.Context, obj runtime.Object) (watch.Interface, error) +} + +// TestEnvironment is a struct containing the envtest environment, Kubernetes config and clients. +type TestEnvironment struct { + Environment *envtest.Environment + Config *rest.Config + Client Client + DiscoveryClient discovery.DiscoveryInterface +} + +// StartTestEnvironment is a wrapper for controller-runtime's envtest that creates a Kubernetes API server and etcd +// suitable for use in tests. +func StartTestEnvironment() (env TestEnvironment, err error) { + env.Environment = &envtest.Environment{ + KubeAPIServerFlags: append(envtest.DefaultKubeAPIServerFlags, "--advertise-address={{ if .URL }}{{ .URL.Hostname }}{{ end }}"), + } + + // Retry up to three times for the test environment to start up in case there is a port collision (#510). + for i := 0; i < 3; i++ { + env.Config, err = env.Environment.Start() + if err == nil { + break + } + } + + if err != nil { + return + } + + env.Client, err = NewRetryClient(env.Config, client.Options{}) + if err != nil { + return + } + + env.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(env.Config) + return +} + +// GetArgs parses a command line string into its arguments and appends a namespace if it is not already set. +func GetArgs(ctx context.Context, command string, cmd harness.Command, namespace string) (*exec.Cmd, error) { + argSlice := []string{} + + argSplit, err := shlex.Split(cmd.Command) + if err != nil { + return nil, err + } + + if command != "" && argSplit[0] != command { + argSlice = append(argSlice, command) + } + + argSlice = append(argSlice, argSplit...) + + if cmd.Namespaced { + fs := pflag.NewFlagSet("", pflag.ContinueOnError) + fs.ParseErrorsWhitelist.UnknownFlags = true + + namespaceParsed := fs.StringP("namespace", "n", "", "") + if err := fs.Parse(argSplit); err != nil { + return nil, err + } + + if *namespaceParsed == "" { + argSlice = append(argSlice, "--namespace", namespace) + } + } + + //nolint:gosec // We're running a user provided command. This is insecure by definition + builtCmd := exec.Command(argSlice[0]) + builtCmd.Args = argSlice + return builtCmd, nil +} + +// RunCommand runs a command with args. +// args gets split on spaces (respecting quoted strings). +func RunCommand(ctx context.Context, namespace string, command string, cmd harness.Command, cwd string, stdout io.Writer, stderr io.Writer) error { + actualDir, err := os.Getwd() + if err != nil { + return err + } + + builtCmd, err := GetArgs(ctx, command, cmd, namespace) + if err != nil { + return err + } + + builtCmd.Dir = cwd + builtCmd.Stdout = stdout + builtCmd.Stderr = stderr + builtCmd.Env = []string{ + fmt.Sprintf("KUBECONFIG=%s/kubeconfig", actualDir), + fmt.Sprintf("PATH=%s/bin/:%s", actualDir, os.Getenv("PATH")), + } + + err = builtCmd.Run() + if err != nil { + if _, ok := err.(*exec.ExitError); ok && cmd.IgnoreFailure { + return nil + } + } + + return err +} + +// RunCommands runs a set of commands, returning any errors. +// If `command` is set, then `command` will be the command that is invoked (if a command specifies it already, it will not be prepended again). +func RunCommands(logger Logger, namespace string, command string, commands []harness.Command, workdir string) []error { + errs := []error{} + + if commands == nil { + return nil + } + + for _, cmd := range commands { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + + logger.Logf("Running command: %s %s", command, cmd) + + err := RunCommand(context.TODO(), namespace, command, cmd, workdir, stdout, stderr) + if err != nil { + errs = append(errs, err) + } + + logger.Log(stderr.String()) + logger.Log(stdout.String()) + } + + if len(errs) == 0 { + return nil + } + + return errs +} + +// RunKubectlCommands runs a set of kubectl commands, returning any errors. +func RunKubectlCommands(logger Logger, namespace string, commands []string, workdir string) []error { + apiCommands := []harness.Command{} + + for _, cmd := range commands { + apiCommands = append(apiCommands, harness.Command{ + Command: cmd, + Namespaced: true, + }) + } + + return RunCommands(logger, namespace, "kubectl", apiCommands, workdir) +} + +// Kubeconfig converts a rest.Config into a YAML kubeconfig and writes it to w +func Kubeconfig(cfg *rest.Config, w io.Writer) error { + var authProvider *api.AuthProviderConfig + var execConfig *api.ExecConfig + if cfg.AuthProvider != nil { + authProvider = &api.AuthProviderConfig{ + Name: cfg.AuthProvider.Name, + Config: cfg.AuthProvider.Config, + } + } + + if cfg.ExecProvider != nil { + execConfig = &api.ExecConfig{ + Command: cfg.ExecProvider.Command, + Args: cfg.ExecProvider.Args, + APIVersion: cfg.ExecProvider.APIVersion, + Env: []api.ExecEnvVar{}, + } + + for _, envVar := range cfg.ExecProvider.Env { + execConfig.Env = append(execConfig.Env, api.ExecEnvVar{ + Name: envVar.Name, + Value: envVar.Value, + }) + } + } + err := rest.LoadTLSFiles(cfg) + if err != nil { + return err + } + return json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil).Encode(&api.Config{ + CurrentContext: "cluster", + Clusters: []api.NamedCluster{ + { + Name: "cluster", + Cluster: api.Cluster{ + Server: cfg.Host, + CertificateAuthorityData: cfg.TLSClientConfig.CAData, + InsecureSkipTLSVerify: cfg.TLSClientConfig.Insecure, + }, + }, + }, + Contexts: []api.NamedContext{ + { + Name: "cluster", + Context: api.Context{ + Cluster: "cluster", + AuthInfo: "user", + }, + }, + }, + AuthInfos: []api.NamedAuthInfo{ + { + Name: "user", + AuthInfo: api.AuthInfo{ + ClientCertificateData: cfg.TLSClientConfig.CertData, + ClientKeyData: cfg.TLSClientConfig.KeyData, + Token: cfg.BearerToken, + Username: cfg.Username, + Password: cfg.Password, + Impersonate: cfg.Impersonate.UserName, + ImpersonateGroups: cfg.Impersonate.Groups, + ImpersonateUserExtra: cfg.Impersonate.Extra, + AuthProvider: authProvider, + Exec: execConfig, + }, + }, + }, + }, w) +} + +func GetDiscoveryClient(mgr manager.Manager) (*discovery.DiscoveryClient, error) { + // use manager rest config to create a discovery client + dc, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig()) + if err != nil || dc == nil { + return nil, fmt.Errorf("failed to create a discovery client: %v", err) + } + + return dc, nil +} diff --git a/pkg/test/utils/kubernetes_integration_test.go b/pkg/test/utils/kubernetes_integration_test.go new file mode 100644 index 00000000..da11a705 --- /dev/null +++ b/pkg/test/utils/kubernetes_integration_test.go @@ -0,0 +1,109 @@ +// +build integration + +package utils + +import ( + "context" + "fmt" + "log" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/watch" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var testenv TestEnvironment + +func TestMain(m *testing.M) { + var err error + + testenv, err = StartTestEnvironment() + if err != nil { + log.Fatal(err) + } + + exitCode := m.Run() + testenv.Environment.Stop() + os.Exit(exitCode) +} + +func TestCreateOrUpdate(t *testing.T) { + // Run the test a bunch of times to try to trigger a conflict and ensure that it handles conflicts properly. + for i := 0; i < 10; i++ { + depToUpdate := WithSpec(t, NewPod("update-me", fmt.Sprintf("default-%d", i)), map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "image": "nginx", + "name": "nginx", + }, + }, + }) + + _, err := CreateOrUpdate(context.TODO(), testenv.Client, SetAnnotation(depToUpdate, "test", "hi"), true) + assert.Nil(t, err) + + quit := make(chan bool) + + go func() { + for { + select { + case <-quit: + return + default: + CreateOrUpdate(context.TODO(), testenv.Client, SetAnnotation(depToUpdate, "test", fmt.Sprintf("%d", i)), false) + time.Sleep(time.Millisecond * 75) + } + } + }() + + time.Sleep(time.Millisecond * 50) + + _, err = CreateOrUpdate(context.TODO(), testenv.Client, SetAnnotation(depToUpdate, "test", "hello"), true) + assert.Nil(t, err) + + quit <- true + } +} + +func TestClientWatch(t *testing.T) { + pod := WithSpec(t, NewPod("my-pod", "default"), map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "image": "nginx", + "name": "nginx", + }, + }, + }) + gvk := pod.GetObjectKind().GroupVersionKind() + + events, err := testenv.Client.Watch(context.TODO(), pod) + assert.Nil(t, err) + + go func() { + assert.Nil(t, testenv.Client.Create(context.TODO(), pod)) + assert.Nil(t, testenv.Client.Update(context.TODO(), pod)) + assert.Nil(t, testenv.Client.Delete(context.TODO(), pod)) + }() + + eventCh := events.ResultChan() + + event := <-eventCh + assert.Equal(t, watch.EventType("ADDED"), event.Type) + assert.Equal(t, gvk, event.Object.GetObjectKind().GroupVersionKind()) + assert.Equal(t, client.ObjectKey{"default", "my-pod"}, ObjectKey(event.Object)) + + event = <-eventCh + assert.Equal(t, watch.EventType("MODIFIED"), event.Type) + assert.Equal(t, gvk, event.Object.GetObjectKind().GroupVersionKind()) + assert.Equal(t, client.ObjectKey{"default", "my-pod"}, ObjectKey(event.Object)) + + event = <-eventCh + assert.Equal(t, watch.EventType("DELETED"), event.Type) + assert.Equal(t, gvk, event.Object.GetObjectKind().GroupVersionKind()) + assert.Equal(t, client.ObjectKey{"default", "my-pod"}, ObjectKey(event.Object)) + + events.Stop() +} diff --git a/pkg/test/utils/kubernetes_test.go b/pkg/test/utils/kubernetes_test.go new file mode 100644 index 00000000..0fe8dee1 --- /dev/null +++ b/pkg/test/utils/kubernetes_test.go @@ -0,0 +1,419 @@ +package utils + +import ( + "context" + "errors" + "io/ioutil" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + harness "github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1" +) + +func TestNamespaced(t *testing.T) { + fake := FakeDiscoveryClient() + + for _, test := range []struct { + testName string + resource runtime.Object + namespace string + shouldError bool + }{ + { + testName: "namespaced resource", + resource: NewPod("hello", ""), + namespace: "set-the-namespace", + }, + { + testName: "namespace already set", + resource: NewPod("hello", "other"), + namespace: "other", + }, + { + testName: "not-namespaced resource", + resource: NewResource("v1", "Namespace", "hello", ""), + namespace: "", + }, + { + testName: "non-existent resource", + resource: NewResource("v1", "Blah", "hello", ""), + shouldError: true, + }, + } { + test := test + + t.Run(test.testName, func(t *testing.T) { + m, _ := meta.Accessor(test.resource) + + actualName, actualNamespace, err := Namespaced(fake, test.resource, "set-the-namespace") + + if test.shouldError { + assert.NotNil(t, err) + assert.Equal(t, "", actualName) + } else { + assert.Nil(t, err) + assert.Equal(t, m.GetName(), actualName) + } + + assert.Equal(t, test.namespace, actualNamespace) + assert.Equal(t, test.namespace, m.GetNamespace()) + }) + } +} + +func TestGETAPIResource(t *testing.T) { + fake := FakeDiscoveryClient() + + apiResource, err := GetAPIResource(fake, schema.GroupVersionKind{ + Kind: "Pod", + Version: "v1", + }) + assert.Nil(t, err) + assert.Equal(t, apiResource.Kind, "Pod") + + apiResource, err = GetAPIResource(fake, schema.GroupVersionKind{ + Kind: "NonExistentResourceType", + Version: "v1", + }) + assert.NotNil(t, err) + assert.Equal(t, err.Error(), "resource type not found") +} + +func TestRetry(t *testing.T) { + index := 0 + + assert.Nil(t, Retry(context.TODO(), func(context.Context) error { + index++ + if index == 1 { + return errors.New("ignore this error") + } + return nil + }, func(err error) bool { return false }, func(err error) bool { + return err.Error() == "ignore this error" + })) + + assert.Equal(t, 2, index) +} + +func TestRetryWithUnexpectedError(t *testing.T) { + index := 0 + + assert.Equal(t, errors.New("bad error"), Retry(context.TODO(), func(context.Context) error { + index++ + if index == 1 { + return errors.New("bad error") + } + return nil + }, func(err error) bool { return false }, func(err error) bool { + return err.Error() == "ignore this error" + })) + assert.Equal(t, 1, index) +} + +func TestRetryWithTimeout(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + assert.Equal(t, errors.New("error"), Retry(ctx, func(context.Context) error { + return errors.New("error") + }, func(err error) bool { return true })) +} + +func TestLoadYAML(t *testing.T) { + tmpfile, err := ioutil.TempFile("", "test.yaml") + assert.Nil(t, err) + defer tmpfile.Close() + + err = ioutil.WriteFile(tmpfile.Name(), []byte(` +apiVersion: v1 +kind: Pod +metadata: + labels: + app: nginx +spec: + containers: + - name: nginx + image: nginx:1.7.9 +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: nginx + name: hello +spec: + containers: + - name: nginx + image: nginx:1.7.9 +`), 0644) + if err != nil { + t.Fatal(err) + } + + objs, err := LoadYAML(tmpfile.Name()) + assert.Nil(t, err) + + assert.Equal(t, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "nginx", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }, + }, + }, objs[0]) + + assert.Equal(t, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "nginx", + }, + "name": "hello", + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx:1.7.9", + "name": "nginx", + }, + }, + }, + }, + }, objs[1]) +} + +func TestMatchesKind(t *testing.T) { + tmpfile, err := ioutil.TempFile("", "test.yaml") + assert.Nil(t, err) + defer tmpfile.Close() + + err = ioutil.WriteFile(tmpfile.Name(), []byte(` +apiVersion: v1 +kind: Pod +metadata: + name: hello +spec: + containers: + - name: nginx + image: nginx:1.7.9 +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: hello +`), 0644) + if err != nil { + t.Fatal(err) + } + + objs, err := LoadYAML(tmpfile.Name()) + assert.Nil(t, err) + + crd := NewResource("apiextensions.k8s.io/v1beta1", "CustomResourceDefinition", "", "") + pod := NewResource("v1", "Pod", "", "") + svc := NewResource("v1", "Service", "", "") + + assert.False(t, MatchesKind(objs[0], crd)) + assert.True(t, MatchesKind(objs[0], pod)) + assert.True(t, MatchesKind(objs[0], pod, crd)) + assert.True(t, MatchesKind(objs[0], crd, pod)) + assert.False(t, MatchesKind(objs[0], crd, svc)) + + assert.True(t, MatchesKind(objs[1], crd)) + assert.False(t, MatchesKind(objs[1], pod)) + assert.True(t, MatchesKind(objs[1], pod, crd)) + assert.True(t, MatchesKind(objs[1], crd, pod)) + assert.False(t, MatchesKind(objs[1], svc, pod)) +} + +func TestGetKubectlArgs(t *testing.T) { + for _, test := range []struct { + testName string + namespace string + args string + expected []string + }{ + { + testName: "namespace long, combined already set at end is not modified", + namespace: "default", + args: "kudo test --namespace=test-canary", + expected: []string{ + "kubectl", "kudo", "test", "--namespace=test-canary", + }, + }, + { + testName: "namespace long already set at end is not modified", + namespace: "default", + args: "kudo test --namespace test-canary", + expected: []string{ + "kubectl", "kudo", "test", "--namespace", "test-canary", + }, + }, + { + testName: "namespace short, combined already set at end is not modified", + namespace: "default", + args: "kudo test -n=test-canary", + expected: []string{ + "kubectl", "kudo", "test", "-n=test-canary", + }, + }, + { + testName: "namespace short already set at end is not modified", + namespace: "default", + args: "kudo test -n test-canary", + expected: []string{ + "kubectl", "kudo", "test", "-n", "test-canary", + }, + }, + { + testName: "namespace long, combined already set at beginning is not modified", + namespace: "default", + args: "--namespace=test-canary kudo test", + expected: []string{ + "kubectl", "--namespace=test-canary", "kudo", "test", + }, + }, + { + testName: "namespace long already set at beginning is not modified", + namespace: "default", + args: "--namespace test-canary kudo test", + expected: []string{ + "kubectl", "--namespace", "test-canary", "kudo", "test", + }, + }, + { + testName: "namespace short, combined already set at beginning is not modified", + namespace: "default", + args: "-n=test-canary kudo test", + expected: []string{ + "kubectl", "-n=test-canary", "kudo", "test", + }, + }, + { + testName: "namespace short already set at beginning is not modified", + namespace: "default", + args: "-n test-canary kudo test", + expected: []string{ + "kubectl", "-n", "test-canary", "kudo", "test", + }, + }, + { + testName: "namespace long, combined already set in middle is not modified", + namespace: "default", + args: "kudo --namespace=test-canary test", + expected: []string{ + "kubectl", "kudo", "--namespace=test-canary", "test", + }, + }, + { + testName: "namespace long already set in middle is not modified", + namespace: "default", + args: "kudo --namespace test-canary test", + expected: []string{ + "kubectl", "kudo", "--namespace", "test-canary", "test", + }, + }, + { + testName: "namespace short, combined already set in middle is not modified", + namespace: "default", + args: "kudo -n=test-canary test", + expected: []string{ + "kubectl", "kudo", "-n=test-canary", "test", + }, + }, + { + testName: "namespace short already set in middle is not modified", + namespace: "default", + args: "kudo -n test-canary test", + expected: []string{ + "kubectl", "kudo", "-n", "test-canary", "test", + }, + }, + { + testName: "namespace not set is appended", + namespace: "default", + args: "kudo test", + expected: []string{ + "kubectl", "kudo", "test", "--namespace", "default", + }, + }, + { + testName: "unknown arguments do not break parsing with namespace is not set", + namespace: "default", + args: "kudo test --config kudo-test.yaml", + expected: []string{ + "kubectl", "kudo", "test", "--config", "kudo-test.yaml", "--namespace", "default", + }, + }, + { + testName: "unknown arguments do not break parsing if namespace is set at beginning", + namespace: "default", + args: "--namespace=test-canary kudo test --config kudo-test.yaml", + expected: []string{ + "kubectl", "--namespace=test-canary", "kudo", "test", "--config", "kudo-test.yaml", + }, + }, + { + testName: "unknown arguments do not break parsing if namespace is set at middle", + namespace: "default", + args: "kudo --namespace=test-canary test --config kudo-test.yaml", + expected: []string{ + "kubectl", "kudo", "--namespace=test-canary", "test", "--config", "kudo-test.yaml", + }, + }, + { + testName: "unknown arguments do not break parsing if namespace is set at end", + namespace: "default", + args: "kudo test --config kudo-test.yaml --namespace=test-canary", + expected: []string{ + "kubectl", "kudo", "test", "--config", "kudo-test.yaml", "--namespace=test-canary", + }, + }, + { + testName: "quotes are respected when parsing", + namespace: "default", + args: "kudo \"test quoted\"", + expected: []string{ + "kubectl", "kudo", "test quoted", "--namespace", "default", + }, + }, + { + testName: "kubectl is not pre-pended if it is already present", + namespace: "default", + args: "kubectl kudo test", + expected: []string{ + "kubectl", "kudo", "test", "--namespace", "default", + }, + }, + } { + test := test + + t.Run(test.testName, func(t *testing.T) { + cmd, err := GetArgs(context.TODO(), "kubectl", harness.Command{ + Command: test.args, + Namespaced: true, + }, test.namespace) + assert.Nil(t, err) + assert.Equal(t, test.expected, cmd.Args) + }) + } +} diff --git a/pkg/test/utils/logger.go b/pkg/test/utils/logger.go new file mode 100644 index 00000000..f2c99bd7 --- /dev/null +++ b/pkg/test/utils/logger.go @@ -0,0 +1,48 @@ +package utils + +import ( + "fmt" + "testing" + "time" +) + +// Logger is an interface used by the KUDO test operator to provide logging of tests. +type Logger interface { + Log(args ...interface{}) + Logf(format string, args ...interface{}) + WithPrefix(string) Logger +} + +// TestLogger implements the Logger interface to be compatible with the go test operator's +// output buffering (without this, the use of Parallel tests combined with subtests causes test +// output to be mixed). +type TestLogger struct { + prefix string + test *testing.T +} + +// NewTestLogger creates a new test logger. +func NewTestLogger(test *testing.T, prefix string) *TestLogger { + return &TestLogger{ + prefix: prefix, + test: test, + } +} + +// Log logs the provided arguments with the logger's prefix. See testing.Log for more details. +func (t *TestLogger) Log(args ...interface{}) { + args = append([]interface{}{ + fmt.Sprintf("%s | %s |", time.Now().Format("15:04:05"), t.prefix), + }, args...) + t.test.Log(args...) +} + +// Logf logs the provided arguments with the logger's prefix. See testing.Logf for more details. +func (t *TestLogger) Logf(format string, args ...interface{}) { + t.Log(fmt.Sprintf(format, args...)) +} + +// WithPrefix returns a new TestLogger with the provided prefix appended to the current prefix. +func (t *TestLogger) WithPrefix(prefix string) Logger { + return NewTestLogger(t.test, fmt.Sprintf("%s/%s", t.prefix, prefix)) +} diff --git a/pkg/test/utils/subset.go b/pkg/test/utils/subset.go new file mode 100644 index 00000000..9c737f37 --- /dev/null +++ b/pkg/test/utils/subset.go @@ -0,0 +1,91 @@ +package utils + +import ( + "fmt" + "reflect" +) + +// SubsetError is an error type used by IsSubset for tracking the path in the struct. +type SubsetError struct { + path []string + message string +} + +// AppendPath appends key to the existing struct path. For example, in struct member `a.Key1.Key2`, the path would be ["Key1", "Key2"] +func (e *SubsetError) AppendPath(key string) { + if e.path == nil { + e.path = []string{} + } + + e.path = append(e.path, key) +} + +// Error implements the error interface. +func (e *SubsetError) Error() string { + if e.path == nil || len(e.path) == 0 { + return e.message + } + + path := "" + for i := len(e.path) - 1; i >= 0; i-- { + path = fmt.Sprintf("%s.%s", path, e.path[i]) + } + + return fmt.Sprintf("%s: %s", path, e.message) +} + +// IsSubset checks to see if `expected` is a subset of `actual`. A "subset" is an object that is equivalent to +// the other object, but where map keys found in actual that are not defined in expected are ignored. +func IsSubset(expected, actual interface{}) error { + if reflect.TypeOf(expected) != reflect.TypeOf(actual) { + return &SubsetError{ + message: fmt.Sprintf("type mismatch: %v != %v", reflect.TypeOf(expected), reflect.TypeOf(actual)), + } + } + + if reflect.DeepEqual(expected, actual) { + return nil + } + + if reflect.TypeOf(expected).Kind() == reflect.Slice { + if reflect.ValueOf(expected).Len() != reflect.ValueOf(actual).Len() { + return &SubsetError{ + message: fmt.Sprintf("slice length mismatch: %d != %d", reflect.ValueOf(expected).Len(), reflect.ValueOf(actual).Len()), + } + } + + for i := 0; i < reflect.ValueOf(expected).Len(); i++ { + if err := IsSubset(reflect.ValueOf(expected).Index(i).Interface(), reflect.ValueOf(actual).Index(i).Interface()); err != nil { + return err + } + } + } else if reflect.TypeOf(expected).Kind() == reflect.Map { + iter := reflect.ValueOf(expected).MapRange() + + for iter.Next() { + actualValue := reflect.ValueOf(actual).MapIndex(iter.Key()) + + if !actualValue.IsValid() { + return &SubsetError{ + path: []string{iter.Key().String()}, + message: fmt.Sprintf("key is missing from map"), + } + } + + if err := IsSubset(iter.Value().Interface(), actualValue.Interface()); err != nil { + subsetErr, ok := err.(*SubsetError) + if ok { + subsetErr.AppendPath(iter.Key().String()) + return subsetErr + } + return err + } + } + } else { + return &SubsetError{ + message: fmt.Sprintf("value mismatch, expected: %v != actual: %v", expected, actual), + } + } + + return nil +} diff --git a/pkg/test/utils/subset_test.go b/pkg/test/utils/subset_test.go new file mode 100644 index 00000000..51f0185d --- /dev/null +++ b/pkg/test/utils/subset_test.go @@ -0,0 +1,126 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsSubset(t *testing.T) { + assert.Nil(t, IsSubset(map[string]interface{}{ + "hello": "world", + }, map[string]interface{}{ + "hello": "world", + "bye": "moon", + })) + + assert.NotNil(t, IsSubset(map[string]interface{}{ + "hello": "moon", + }, map[string]interface{}{ + "hello": "world", + "bye": "moon", + })) + + assert.Nil(t, IsSubset(map[string]interface{}{ + "hello": map[string]interface{}{ + "hello": "world", + }, + }, map[string]interface{}{ + "hello": map[string]interface{}{ + "hello": "world", + "bye": "moon", + }, + })) + + assert.NotNil(t, IsSubset(map[string]interface{}{ + "hello": map[string]interface{}{ + "hello": "moon", + }, + }, map[string]interface{}{ + "hello": map[string]interface{}{ + "hello": "world", + "bye": "moon", + }, + })) + + assert.NotNil(t, IsSubset(map[string]interface{}{ + "hello": map[string]interface{}{ + "hello": "moon", + }, + }, map[string]interface{}{ + "hello": "world", + })) + + assert.NotNil(t, IsSubset(map[string]interface{}{ + "hello": "world", + }, map[string]interface{}{})) + + assert.Nil(t, IsSubset(map[string]interface{}{ + "hello": []int{ + 1, 2, 3, + }, + }, map[string]interface{}{ + "hello": []int{ + 1, 2, 3, + }, + })) + + assert.Nil(t, IsSubset(map[string]interface{}{ + "hello": map[string]interface{}{ + "hello": []map[string]interface{}{ + { + "image": "hello", + }, + }, + }, + }, map[string]interface{}{ + "hello": map[string]interface{}{ + "hello": []map[string]interface{}{ + { + "image": "hello", + "bye": "moon", + }, + }, + }, + })) + + assert.NotNil(t, IsSubset(map[string]interface{}{ + "hello": map[string]interface{}{ + "hello": []map[string]interface{}{ + { + "image": "hello", + }, + }, + }, + }, map[string]interface{}{ + "hello": map[string]interface{}{ + "hello": []map[string]interface{}{ + { + "image": "hello", + "bye": "moon", + }, + { + "bye": "moon", + }, + }, + }, + })) + + assert.NotNil(t, IsSubset(map[string]interface{}{ + "hello": map[string]interface{}{ + "hello": []map[string]interface{}{ + { + "image": "hello", + }, + }, + }, + }, map[string]interface{}{ + "hello": map[string]interface{}{ + "hello": []map[string]interface{}{ + { + "image": "world", + }, + }, + }, + })) +} diff --git a/pkg/test/utils/testing.go b/pkg/test/utils/testing.go new file mode 100644 index 00000000..fcb9bc62 --- /dev/null +++ b/pkg/test/utils/testing.go @@ -0,0 +1,89 @@ +package utils + +import ( + "flag" + "fmt" + "io" + "os" + "regexp" + "runtime/pprof" + "testing" +) + +// RunTests runs a Go test method without requiring the Go compiler. +// This does not currently support test caching. +// If testToRun is set to a non-empty string, it is passed as a `-run` argument to the go test harness. +// If paralellism is set, it limits the number of concurrently running tests. +func RunTests(testName string, testToRun string, parallelism int, testFunc func(*testing.T)) { + flag.Parse() + testing.Init() + + // Set the verbose test flag to true since we are not using the regular go test CLI. + if err := flag.Set("test.v", "true"); err != nil { + panic(err) + } + + // Set the -run flag on the Go test harness. + // See the go test documentation: https://golang.org/pkg/cmd/go/internal/test/ + if testToRun != "" { + if err := flag.Set("test.run", fmt.Sprintf("//%s", testToRun)); err != nil { + panic(err) + } + } + + parallelismStr := "8" + if parallelism != 0 { + parallelismStr = fmt.Sprintf("%d", parallelism) + } + if err := flag.Set("test.parallel", parallelismStr); err != nil { + panic(err) + } + + os.Exit(testing.MainStart(&testDeps{}, []testing.InternalTest{ + { + Name: testName, + F: testFunc, + }, + }, nil, nil).Run()) +} + +// testDeps implements the testDeps interface for MainStart. +type testDeps struct{} + +var matchPat string +var matchRe *regexp.Regexp + +func (testDeps) MatchString(pat, str string) (result bool, err error) { + if matchRe == nil || matchPat != pat { + matchPat = pat + matchRe, err = regexp.Compile(matchPat) + + if err != nil { + return + } + } + + return matchRe.MatchString(str), nil +} + +func (testDeps) StartCPUProfile(w io.Writer) error { + return pprof.StartCPUProfile(w) +} + +func (testDeps) StopCPUProfile() { + pprof.StopCPUProfile() +} + +func (testDeps) WriteProfileTo(name string, w io.Writer, debug int) error { + return pprof.Lookup(name).WriteTo(w, debug) +} + +func (testDeps) ImportPath() string { + return "" +} + +func (testDeps) StartTestLog(w io.Writer) {} + +func (testDeps) StopTestLog() error { + return nil +} diff --git a/tools.go b/tools.go new file mode 100644 index 00000000..655b4e6b --- /dev/null +++ b/tools.go @@ -0,0 +1,14 @@ +// +build tools + +// Package tools is used to import go modules that we use for tooling as dependencies. +// For more information, please refer to: https://github.com/go-modules-by-example/index/blob/ac9bf72/010_tools/README.md +package tools + +import ( + _ "k8s.io/code-generator/cmd/client-gen" + _ "k8s.io/code-generator/cmd/deepcopy-gen" + _ "k8s.io/code-generator/cmd/defaulter-gen" + _ "k8s.io/code-generator/cmd/informer-gen" + _ "k8s.io/code-generator/cmd/lister-gen" + _ "sigs.k8s.io/controller-tools/cmd/controller-gen" +)