├── .circleci └── config.yml ├── .dockerignore ├── .github └── dependabot.yml ├── .gitignore ├── .goreleaser.yml ├── BUILDING.md ├── CODE_OF_CONDUCT.md ├── CONDUCT.md ├── Dockerfile ├── Dockerfile.acceptance ├── Dockerfile.offline ├── LICENSE ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── acceptance.bats ├── chocolatey └── kubeval │ ├── kubeval.nuspec │ └── tools │ ├── chocolateyinstall.ps1 │ └── chocolateyuninstall.ps1 ├── docs ├── contrib.md ├── go.md ├── index.md └── installation.md ├── fixtures ├── blank.yaml ├── comment.yaml ├── duplicates-non-namespaced.yaml ├── duplicates-skipped-kinds.yaml ├── duplicates-with-namespace-default.yaml ├── duplicates-with-namespace.yaml ├── duplicates.yaml ├── extra_property.yaml ├── full_domain_group.yaml ├── generate_name.yaml ├── int_or_string.yaml ├── invalid.yaml ├── list_empty_valid.yaml ├── list_invalid.yaml ├── list_valid.yaml ├── missing_kind.yaml ├── missing_kind_value.yaml ├── multi_invalid.yaml ├── multi_invalid_resources.yaml ├── multi_valid.yaml ├── multi_valid_source.yaml ├── null_array.yaml ├── null_string.yaml ├── quantity.yaml ├── same-kind-different-api.yaml ├── same-object-different-namespace-default.yaml ├── same-object-different-namespace.yaml ├── test_crd.yaml ├── unconventional_keys.yaml ├── valid.json ├── valid.yaml └── valid_version.yaml ├── go.mod ├── go.sum ├── kubeval ├── config.go ├── kubeval.go ├── kubeval_test.go ├── output.go ├── output_test.go └── utils.go ├── log └── log.go ├── main.go ├── mkdocs.yml └── version └── version.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | snyk: garethr/snyk@0.3.0 4 | jobs: 5 | build: 6 | docker: 7 | - image: circleci/golang:1.15-node 8 | 9 | steps: 10 | - checkout 11 | - restore_cache: 12 | keys: 13 | - go-mod-{{ checksum "go.sum" }} 14 | - run: make test 15 | - run: 16 | name: Build binary 17 | command: make build 18 | - save_cache: 19 | key: go-mod-{{ checksum "go.sum" }} 20 | paths: 21 | - "/go/pkg/mod" 22 | - run: 23 | name: Install Bats 24 | command: | 25 | cd /tmp 26 | curl -L https://github.com/sstephenson/bats/archive/v0.4.0.tar.gz | tar -xz 27 | sudo ./bats-0.4.0/install.sh /usr/local 28 | - snyk/install_snyk 29 | - run: 30 | name: Run acceptance tests 31 | command: | 32 | sudo npm install tap-xunit -g 33 | mkdir -p ~/reports 34 | bats acceptance.bats --tap | tap-xunit > ~/reports/kubeval.xml 35 | - snyk/check_code_with_snyk: 36 | args: --file=go.mod 37 | - store_test_results: 38 | path: ~/reports 39 | - store_artifacts: 40 | path: ~/reports 41 | image: 42 | docker: 43 | - image: circleci/buildpack-deps:stretch 44 | 45 | steps: 46 | - checkout 47 | - setup_remote_docker: 48 | version: 18.09.3 49 | - snyk/install_snyk 50 | - run: 51 | name: Build image 52 | command: docker build --progress plain -t garethr/kubeval . 53 | environment: 54 | DOCKER_BUILDKIT: 1 55 | - snyk/check_docker_with_snyk: 56 | image: garethr/kubeval 57 | project: docker.io/garethr/kubeval 58 | 59 | workflows: 60 | version: 2 61 | build: 62 | jobs: 63 | - build 64 | - image 65 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | README.md 2 | Dockerfile 3 | LICENSE 4 | bin 5 | releases 6 | vendor 7 | .bats 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic dependabot.yml file with 2 | # minimum configuration for two package managers 3 | 4 | version: 2 5 | updates: 6 | # Enable version updates for npm 7 | - package-ecosystem: "gomod" 8 | # Look for `package.json` and `lock` files in the `root` directory 9 | directory: "/" 10 | # Check the npm registry for updates every day (weekdays) 11 | schedule: 12 | interval: "weekly" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | bin 3 | releases 4 | .bats 5 | chocolatey/kubeval/kubeval.*.nupkg 6 | dist 7 | site 8 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | builds: 5 | - env: 6 | - CGO_ENABLED=0 7 | goos: 8 | - windows 9 | - linux 10 | - darwin 11 | goarch: 12 | - amd64 13 | - arm 14 | - arm64 15 | goarm: 16 | - 6 17 | - 7 18 | archives: 19 | - format: tar.gz 20 | format_overrides: 21 | - goos: windows 22 | format: zip 23 | checksum: 24 | name_template: 'checksums.txt' 25 | snapshot: 26 | name_template: "{{ .Tag }}-next" 27 | changelog: 28 | sort: asc 29 | filters: 30 | exclude: 31 | - '^docs:' 32 | - '^test:' 33 | brews: 34 | - github: 35 | owner: instrumenta 36 | name: homebrew-instrumenta 37 | folder: Formula 38 | description: "Validate your Kubernetes configurations" 39 | homepage: "https://github.com/instrumenta/kubeval" 40 | test: | 41 | system "#{bin}/kubeval --version" 42 | scoop: 43 | bucket: 44 | owner: instrumenta 45 | name: scoop-instrumenta 46 | description: "Validate your Kubernetes configurations" 47 | homepage: "https://github.com/instrumenta/kubeval" 48 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Developing Kubeval 2 | 3 | If you are interested in contributing to Kubeval then the following instructions should 4 | be useful in getting started. 5 | 6 | ## Pre-requisites 7 | 8 | For building Kubeval you'll need the following: 9 | 10 | * Make 11 | * Go 1.15 12 | 13 | For releasing Kubeval you'll also need: 14 | 15 | * [Goreleaser](https://goreleaser.com/) 16 | 17 | The acceptance tests use [Bats](https://github.com/sstephenson/bats) and can be run 18 | directly or via Docker. 19 | 20 | 21 | ## Building 22 | 23 | Building a binary for your platform can be done by running: 24 | 25 | ``` 26 | make build 27 | ``` 28 | 29 | This should create `bin/kubeval`. 30 | 31 | ### Release Snapshot 32 | 33 | To build the release snapshots run: 34 | 35 | ``` 36 | make snapshot 37 | ``` 38 | 39 | This creates the directory `dist` with all available release artifacts and the final configuration for `goreleaser`. 40 | 41 | ## Testing 42 | 43 | The unit tests, along with some basic static analysis, can be run with: 44 | 45 | ``` 46 | make test 47 | ``` 48 | 49 | The [Bats](https://github.com/sstephenson/bats) based acceptance tests 50 | are run using the following target. Note that this runs the tests using Docker. 51 | 52 | ``` 53 | make acceptance 54 | ``` 55 | 56 | If you would prefer to run them directly 57 | 58 | ``` 59 | make build && PATH=./bin:$PATH ./acceptance.bats 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at gareth@morethanseven.net. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting one of the project maintainers listed below. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Project Maintainers 69 | 70 | * Gareth Rushgrove <> 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at [http://contributor-covenant.org/version/1/4][version] 76 | 77 | [homepage]: http://contributor-covenant.org 78 | [version]: http://contributor-covenant.org/version/1/4/ 79 | 80 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.15-alpine as builder 2 | RUN apk --no-cache add make git 3 | WORKDIR / 4 | COPY . / 5 | RUN make build 6 | 7 | FROM alpine:latest 8 | RUN apk --no-cache add ca-certificates 9 | COPY --from=builder /bin/kubeval . 10 | RUN ln -s /kubeval /usr/local/bin/kubeval 11 | ENTRYPOINT ["/kubeval"] 12 | CMD ["--help"] 13 | -------------------------------------------------------------------------------- /Dockerfile.acceptance: -------------------------------------------------------------------------------- 1 | FROM golang:1.15-alpine as builder 2 | RUN apk --no-cache add make git 3 | WORKDIR / 4 | COPY . / 5 | RUN make build 6 | 7 | FROM bats/bats:v1.1.0 8 | RUN apk --no-cache add ca-certificates 9 | COPY --from=builder /bin/kubeval /bin/kubeval 10 | COPY acceptance.bats /acceptance.bats 11 | COPY fixtures /fixtures 12 | RUN "/acceptance.bats" 13 | -------------------------------------------------------------------------------- /Dockerfile.offline: -------------------------------------------------------------------------------- 1 | FROM golang:1.15-alpine as builder 2 | RUN apk --no-cache add make git 3 | WORKDIR / 4 | COPY . / 5 | RUN make build 6 | 7 | FROM alpine:latest as schemas 8 | RUN apk --no-cache add git 9 | RUN git clone --depth 1 https://github.com/instrumenta/kubernetes-json-schema.git 10 | RUN git clone --depth 1 https://github.com/garethr/openshift-json-schema.git 11 | 12 | FROM schemas as standalone-schemas 13 | RUN cd kubernetes-json-schema/master && \ 14 | find -maxdepth 1 -type d -not -name "." -not -name "*-standalone*" | xargs rm -rf 15 | 16 | FROM alpine:latest 17 | RUN apk --no-cache add ca-certificates 18 | COPY --from=builder /bin/kubeval . 19 | COPY --from=standalone-schemas /kubernetes-json-schema /schemas/kubernetes-json-schema/master 20 | COPY --from=standalone-schemas /openshift-json-schema /schemas/openshift-json-schema/master 21 | ENV KUBEVAL_SCHEMA_LOCATION=file:///schemas 22 | RUN ln -s /kubeval /usr/local/bin/kubeval 23 | ENTRYPOINT ["/kubeval"] 24 | CMD ["--help"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Kubeval - Validate Kubernetes configuration files 2 | 3 | Copyright (C) 2017 Gareth Rushgrove 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=kubeval 2 | IMAGE_NAME=garethr/$(NAME) 3 | PACKAGE_NAME=github.com/instrumenta/$(NAME) 4 | GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor) 5 | TAG=$(shell git describe --abbrev=0 --tags) 6 | 7 | 8 | all: build 9 | 10 | $(GOPATH)/bin/golint$(suffix): 11 | go get github.com/golang/lint/golint 12 | 13 | $(GOPATH)/bin/goveralls$(suffix): 14 | go get github.com/mattn/goveralls 15 | 16 | vendor: 17 | go mod vendor 18 | 19 | .bats: 20 | git clone --depth 1 https://github.com/sstephenson/bats.git .bats 21 | 22 | bin: 23 | mkdir bin 24 | 25 | release: 26 | goreleaser --rm-dist 27 | 28 | snapshot: 29 | goreleaser --snapshot --skip-publish --rm-dist 30 | 31 | build: bin 32 | go build -o bin/$(NAME) . 33 | 34 | lint: $(GOPATH)/bin/golint$(suffix) 35 | golint 36 | 37 | docker: 38 | docker build -t $(IMAGE_NAME):$(TAG) . 39 | docker tag $(IMAGE_NAME):$(TAG) $(IMAGE_NAME):latest 40 | docker push $(IMAGE_NAME):$(TAG) 41 | docker push $(IMAGE_NAME):latest 42 | 43 | docker-offline: 44 | docker build -f Dockerfile.offline -t $(IMAGE_NAME):$(TAG)-offline . 45 | docker tag $(IMAGE_NAME):$(TAG)-offline $(IMAGE_NAME):offline 46 | 47 | vet: 48 | go vet 49 | 50 | test: vet 51 | go test -race -v -cover ./... 52 | 53 | watch: 54 | ls */*.go | entr make test 55 | 56 | acceptance: 57 | docker build -f Dockerfile.acceptance -t $(IMAGE_NAME):$(TAG)-acceptance . 58 | docker tag $(IMAGE_NAME):$(TAG)-acceptance $(IMAGE_NAME):acceptance 59 | 60 | cover: 61 | go test -v ./$(NAME) -coverprofile=coverage.out 62 | go tool cover -html=coverage.out 63 | rm coverage.out 64 | 65 | clean: 66 | rm -fr dist bin 67 | 68 | fmt: 69 | gofmt -w $(GOFMT_FILES) 70 | 71 | dist/$(NAME)-checksum-%: 72 | cd dist && sha256sum $@.zip 73 | 74 | checksums: dist/$(NAME)-checksum-darwin-amd64 dist/$(NAME)-checksum-windows-386 dist/$(NAME)-checksum-windows-amd64 dist/$(NAME)-checksum-linux-amd64 75 | 76 | chocolatey/$(NAME)/$(NAME).$(TAG).nupkg: chocolatey/$(NAME)/$(NAME).nuspec 77 | cd chocolatey/$(NAME) && choco pack 78 | 79 | choco: 80 | cd chocolatey/$(NAME) && choco push $(NAME).$(TAG).nupkg -s https://chocolatey.org/ 81 | 82 | .PHONY: release snapshot fmt clean cover acceptance lint docker test vet watch build check choco checksums 83 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | mkdocs = "*" 10 | mkdocs-material = "*" 11 | 12 | [requires] 13 | python_version = "3.7" 14 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "e1cdbad22bfb2ca546e5b207d0b2495ad3edd2e3f2b69c8cbf7b94cc98ce8ccb" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "click": { 20 | "hashes": [ 21 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 22 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 23 | ], 24 | "version": "==7.1.2" 25 | }, 26 | "importlib-metadata": { 27 | "hashes": [ 28 | "sha256:1cedf994a9b6885dcbb7ed40b24c332b1de3956319f4b1a0f07c0621d453accc", 29 | "sha256:c9c1b6c7dbc62084f3e6a614a194eb16ded7947736c18e3300125d5c0a7a8b3c" 30 | ], 31 | "markers": "python_version < '3.8'", 32 | "version": "==3.9.1" 33 | }, 34 | "jinja2": { 35 | "hashes": [ 36 | "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", 37 | "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" 38 | ], 39 | "version": "==2.11.3" 40 | }, 41 | "livereload": { 42 | "hashes": [ 43 | "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869" 44 | ], 45 | "version": "==2.6.3" 46 | }, 47 | "markdown": { 48 | "hashes": [ 49 | "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49", 50 | "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c" 51 | ], 52 | "version": "==3.3.4" 53 | }, 54 | "markupsafe": { 55 | "hashes": [ 56 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 57 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 58 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 59 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 60 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 61 | "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", 62 | "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", 63 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 64 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 65 | "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", 66 | "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", 67 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 68 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 69 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 70 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 71 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 72 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 73 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 74 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 75 | "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", 76 | "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", 77 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 78 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 79 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 80 | "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", 81 | "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", 82 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 83 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 84 | "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", 85 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 86 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 87 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 88 | "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", 89 | "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", 90 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 91 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 92 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 93 | "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", 94 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 95 | "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", 96 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 97 | "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", 98 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 99 | "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", 100 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 101 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 102 | "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", 103 | "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", 104 | "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", 105 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 106 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", 107 | "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" 108 | ], 109 | "version": "==1.1.1" 110 | }, 111 | "mkdocs": { 112 | "hashes": [ 113 | "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", 114 | "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a" 115 | ], 116 | "index": "pypi", 117 | "version": "==1.0.4" 118 | }, 119 | "mkdocs-material": { 120 | "hashes": [ 121 | "sha256:8a572f4b3358b9c0e11af8ae319ba4f3747ebb61e2393734d875133b0d2f7891", 122 | "sha256:91210776db541283dd4b7beb5339c190aa69de78ad661aa116a8aa97dd73c803" 123 | ], 124 | "index": "pypi", 125 | "version": "==4.1.2" 126 | }, 127 | "pygments": { 128 | "hashes": [ 129 | "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94", 130 | "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8" 131 | ], 132 | "version": "==2.8.1" 133 | }, 134 | "pymdown-extensions": { 135 | "hashes": [ 136 | "sha256:478b2c04513fbb2db61688d5f6e9030a92fb9be14f1f383535c43f7be9dff95b", 137 | "sha256:632371fa3bf1b21a0e3f4063010da59b41db049f261f4c0b0872069a9b6d1735" 138 | ], 139 | "version": "==8.1.1" 140 | }, 141 | "pyyaml": { 142 | "hashes": [ 143 | "sha256:02c78d77281d8f8d07a255e57abdbf43b02257f59f50cc6b636937d68efa5dd0", 144 | "sha256:0dc9f2eb2e3c97640928dec63fd8dc1dd91e6b6ed236bd5ac00332b99b5c2ff9", 145 | "sha256:124fd7c7bc1e95b1eafc60825f2daf67c73ce7b33f1194731240d24b0d1bf628", 146 | "sha256:26fcb33776857f4072601502d93e1a619f166c9c00befb52826e7b774efaa9db", 147 | "sha256:31ba07c54ef4a897758563e3a0fcc60077698df10180abe4b8165d9895c00ebf", 148 | "sha256:3c49e39ac034fd64fd576d63bb4db53cda89b362768a67f07749d55f128ac18a", 149 | "sha256:52bf0930903818e600ae6c2901f748bc4869c0c406056f679ab9614e5d21a166", 150 | "sha256:5a3f345acff76cad4aa9cb171ee76c590f37394186325d53d1aa25318b0d4a09", 151 | "sha256:5e7ac4e0e79a53451dc2814f6876c2fa6f71452de1498bbe29c0b54b69a986f4", 152 | "sha256:7242790ab6c20316b8e7bb545be48d7ed36e26bbe279fd56f2c4a12510e60b4b", 153 | "sha256:737bd70e454a284d456aa1fa71a0b429dd527bcbf52c5c33f7c8eee81ac16b89", 154 | "sha256:8635d53223b1f561b081ff4adecb828fd484b8efffe542edcfdff471997f7c39", 155 | "sha256:8b818b6c5a920cbe4203b5a6b14256f0e5244338244560da89b7b0f1313ea4b6", 156 | "sha256:8bf38641b4713d77da19e91f8b5296b832e4db87338d6aeffe422d42f1ca896d", 157 | "sha256:a36a48a51e5471513a5aea920cdad84cbd56d70a5057cca3499a637496ea379c", 158 | "sha256:b2243dd033fd02c01212ad5c601dafb44fbb293065f430b0d3dbf03f3254d615", 159 | "sha256:cc547d3ead3754712223abb7b403f0a184e4c3eae18c9bb7fd15adef1597cc4b", 160 | "sha256:cc552b6434b90d9dbed6a4f13339625dc466fd82597119897e9489c953acbc22", 161 | "sha256:f3790156c606299ff499ec44db422f66f05a7363b39eb9d5b064f17bd7d7c47b", 162 | "sha256:f7a21e3d99aa3095ef0553e7ceba36fb693998fbb1226f1392ce33681047465f", 163 | "sha256:fdc6b2cb4b19e431994f25a9160695cc59a4e861710cc6fc97161c5e845fc579" 164 | ], 165 | "index": "pypi", 166 | "version": "==5.4" 167 | }, 168 | "six": { 169 | "hashes": [ 170 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 171 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 172 | ], 173 | "version": "==1.15.0" 174 | }, 175 | "tornado": { 176 | "hashes": [ 177 | "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb", 178 | "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c", 179 | "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288", 180 | "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95", 181 | "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558", 182 | "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe", 183 | "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791", 184 | "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d", 185 | "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326", 186 | "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b", 187 | "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4", 188 | "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c", 189 | "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910", 190 | "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5", 191 | "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c", 192 | "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0", 193 | "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675", 194 | "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd", 195 | "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f", 196 | "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c", 197 | "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea", 198 | "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6", 199 | "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05", 200 | "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd", 201 | "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575", 202 | "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a", 203 | "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37", 204 | "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795", 205 | "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f", 206 | "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32", 207 | "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c", 208 | "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01", 209 | "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4", 210 | "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2", 211 | "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921", 212 | "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085", 213 | "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df", 214 | "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102", 215 | "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5", 216 | "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68", 217 | "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5" 218 | ], 219 | "version": "==6.1" 220 | }, 221 | "typing-extensions": { 222 | "hashes": [ 223 | "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", 224 | "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", 225 | "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" 226 | ], 227 | "markers": "python_version < '3.8'", 228 | "version": "==3.7.4.3" 229 | }, 230 | "zipp": { 231 | "hashes": [ 232 | "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", 233 | "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" 234 | ], 235 | "version": "==3.4.1" 236 | } 237 | }, 238 | "develop": {} 239 | } 240 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubeval 2 | 3 | **NOTE: This project is [no longer maintained](https://github.com/instrumenta/kubeval/issues/268#issuecomment-902128481), a good replacement is [kubeconform](https://github.com/yannh/kubeconform)** 4 | 5 | 6 | `kubeval` is a tool for validating a Kubernetes YAML or JSON configuration file. 7 | It does so using schemas generated from the Kubernetes OpenAPI specification, and 8 | therefore can validate schemas for multiple versions of Kubernetes. 9 | 10 | [![CircleCI](https://circleci.com/gh/instrumenta/kubeval.svg?style=svg)](https://circleci.com/gh/instrumenta/kubeval) 11 | [![Go Report 12 | Card](https://goreportcard.com/badge/github.com/instrumenta/kubeval)](https://goreportcard.com/report/github.com/instrumenta/kubeval) 13 | [![GoDoc](https://godoc.org/github.com/instrumenta/kubeval?status.svg)](https://godoc.org/github.com/instrumenta/kubeval) 14 | 15 | 16 | ``` 17 | $ kubeval my-invalid-rc.yaml 18 | WARN - fixtures/my-invalid-rc.yaml contains an invalid ReplicationController - spec.replicas: Invalid type. Expected: [integer,null], given: string 19 | $ echo $? 20 | 1 21 | ``` 22 | 23 | 24 | For full usage and installation instructions see [kubeval.com](https://kubeval.com/). 25 | -------------------------------------------------------------------------------- /acceptance.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "Pass when parsing a valid Kubernetes config YAML file" { 4 | run bin/kubeval fixtures/valid.yaml 5 | [ "$status" -eq 0 ] 6 | [ "$output" = "PASS - fixtures/valid.yaml contains a valid ReplicationController (bob)" ] 7 | } 8 | 9 | @test "Pass when parsing a valid Kubernetes config YAML file on stdin" { 10 | run bash -c "cat fixtures/valid.yaml | bin/kubeval" 11 | [ "$status" -eq 0 ] 12 | [ "$output" = "PASS - stdin contains a valid ReplicationController (bob)" ] 13 | } 14 | 15 | @test "Pass when parsing a valid Kubernetes config YAML file explicitly on stdin" { 16 | run bash -c "cat fixtures/valid.yaml | bin/kubeval -" 17 | [ "$status" -eq 0 ] 18 | [ "$output" = "PASS - stdin contains a valid ReplicationController (bob)" ] 19 | } 20 | 21 | @test "Pass when parsing a valid Kubernetes config JSON file" { 22 | run bin/kubeval fixtures/valid.json 23 | [ "$status" -eq 0 ] 24 | [ "$output" = "PASS - fixtures/valid.json contains a valid Deployment (default.nginx-deployment)" ] 25 | } 26 | 27 | @test "Pass when parsing a Kubernetes file with string and integer quantities" { 28 | run bin/kubeval fixtures/quantity.yaml 29 | [ "$status" -eq 0 ] 30 | [ "$output" = "PASS - fixtures/quantity.yaml contains a valid LimitRange (mem-limit-range)" ] 31 | } 32 | 33 | @test "Pass when parsing a valid Kubernetes config file with int_to_string vars" { 34 | run bin/kubeval fixtures/int_or_string.yaml 35 | [ "$status" -eq 0 ] 36 | [ "$output" = "PASS - fixtures/int_or_string.yaml contains a valid Service (kube-system.heapster)" ] 37 | } 38 | 39 | @test "Pass when parsing a valid Kubernetes config file with null arrays" { 40 | run bin/kubeval fixtures/null_array.yaml 41 | [ "$status" -eq 0 ] 42 | [ "$output" = "PASS - fixtures/null_array.yaml contains a valid Deployment (kube-system.kubernetes-dashboard)" ] 43 | } 44 | 45 | @test "Pass when parsing a valid Kubernetes config file with null strings" { 46 | run bin/kubeval fixtures/null_string.yaml 47 | [ "$status" -eq 0 ] 48 | [ "$output" = "PASS - fixtures/null_string.yaml contains a valid Service (frontend)" ] 49 | } 50 | 51 | @test "Pass when parsing a valid Kubernetes config YAML file with generate name" { 52 | run bin/kubeval fixtures/generate_name.yaml 53 | [ "$status" -eq 0 ] 54 | [ "$output" = "PASS - fixtures/generate_name.yaml contains a valid Job (pi-{{ generateName }})" ] 55 | } 56 | 57 | @test "Pass when parsing a multi-document config file" { 58 | run bin/kubeval fixtures/multi_valid.yaml 59 | [ "$status" -eq 0 ] 60 | } 61 | 62 | @test "Fail when parsing a multi-document config file with one invalid resource" { 63 | run bin/kubeval fixtures/multi_invalid.yaml 64 | [ "$status" -eq 1 ] 65 | } 66 | 67 | @test "Fail when parsing an invalid Kubernetes config file" { 68 | run bin/kubeval fixtures/invalid.yaml 69 | [ "$status" -eq 1 ] 70 | } 71 | 72 | @test "Fail when parsing an invalid Kubernetes config file on stdin" { 73 | run bash -c "cat fixtures/invalid.yaml | bin/kubeval -" 74 | [ "$status" -eq 1 ] 75 | } 76 | 77 | @test "Return relevant error for non-existent file" { 78 | run bin/kubeval fixtures/not-here 79 | [ "$status" -eq 1 ] 80 | [ $(expr "$output" : "^ERR - Could not open file") -ne 0 ] 81 | } 82 | 83 | @test "Pass when parsing a blank config file" { 84 | run bin/kubeval fixtures/blank.yaml 85 | [ "$status" -eq 0 ] 86 | [ "$output" = "PASS - fixtures/blank.yaml contains an empty YAML document" ] 87 | } 88 | 89 | @test "Pass when parsing a blank config file with a comment" { 90 | run bin/kubeval fixtures/comment.yaml 91 | [ "$status" -eq 0 ] 92 | [ "$output" = "PASS - fixtures/comment.yaml contains an empty YAML document" ] 93 | } 94 | 95 | @test "Return relevant error for YAML missing kind key" { 96 | run bin/kubeval fixtures/missing_kind.yaml 97 | [ "$status" -eq 1 ] 98 | } 99 | 100 | @test "Fail when parsing a config with additional properties and strict set" { 101 | run bin/kubeval --strict fixtures/extra_property.yaml 102 | [ "$status" -eq 1 ] 103 | } 104 | 105 | @test "Fail when parsing a config with a kind key but no value" { 106 | run bin/kubeval fixtures/missing_kind_value.yaml 107 | [ "$status" -eq 1 ] 108 | } 109 | 110 | @test "Pass when parsing a config with additional properties" { 111 | run bin/kubeval fixtures/extra_property.yaml 112 | [ "$status" -eq 0 ] 113 | } 114 | 115 | @test "Fail when parsing a config with CRD" { 116 | run bin/kubeval fixtures/test_crd.yaml 117 | [ "$status" -eq 1 ] 118 | } 119 | 120 | @test "Pass when parsing a config with CRD and ignoring missing schemas" { 121 | run bin/kubeval --ignore-missing-schemas fixtures/test_crd.yaml 122 | [ "$status" -eq 0 ] 123 | } 124 | 125 | @test "Pass when using a valid --schema-location" { 126 | run bin/kubeval fixtures/valid.yaml --schema-location https://kubernetesjsonschema.dev 127 | [ "$status" -eq 0 ] 128 | } 129 | 130 | @test "Fail when using a faulty --schema-location" { 131 | run bin/kubeval fixtures/valid.yaml --schema-location foo 132 | [ "$status" -eq 1 ] 133 | } 134 | 135 | @test "Pass when using a valid KUBEVAL_SCHEMA_LOCATION variable" { 136 | KUBEVAL_SCHEMA_LOCATION=https://kubernetesjsonschema.dev run bin/kubeval fixtures/valid.yaml 137 | [ "$status" -eq 0 ] 138 | } 139 | 140 | @test "Fail when using a faulty KUBEVAL_SCHEMA_LOCATION variable" { 141 | KUBEVAL_SCHEMA_LOCATION=foo run bin/kubeval fixtures/valid.yaml 142 | [ "$status" -eq 1 ] 143 | } 144 | 145 | @test "Pass when using a valid --schema-location, which overrides a faulty KUBEVAL_SCHEMA_LOCATION variable" { 146 | KUBEVAL_SCHEMA_LOCATION=foo run bin/kubeval fixtures/valid.yaml --schema-location https://kubernetesjsonschema.dev 147 | [ "$status" -eq 0 ] 148 | } 149 | 150 | @test "Fail when using a faulty --schema-location, which overrides a valid KUBEVAL_SCHEMA_LOCATION variable" { 151 | KUBEVAL_SCHEMA_LOCATION=https://kubernetesjsonschema.dev run bin/kubeval fixtures/valid.yaml --schema-location foo 152 | [ "$status" -eq 1 ] 153 | } 154 | 155 | @test "Pass when using --openshift with a valid input" { 156 | run bin/kubeval fixtures/valid.yaml --openshift 157 | [ "$status" -eq 0 ] 158 | } 159 | 160 | @test "Fail when using --openshift with an invalid input" { 161 | run bin/kubeval fixtures/invalid.yaml --openshift 162 | [ "$status" -eq 1 ] 163 | } 164 | 165 | @test "Only prints a single warning when --ignore-missing-schemas is supplied" { 166 | run bin/kubeval --ignore-missing-schemas fixtures/valid.yaml fixtures/valid.yaml 167 | [ "$status" -eq 0 ] 168 | [[ "${lines[0]}" == *"WARN - Set to ignore missing schemas"* ]] 169 | [[ "${lines[1]}" == *"PASS - fixtures/valid.yaml contains a valid ReplicationController"* ]] 170 | [[ "${lines[2]}" == *"PASS - fixtures/valid.yaml contains a valid ReplicationController"* ]] 171 | } 172 | 173 | @test "Does not print warnings if --quiet is supplied" { 174 | run bin/kubeval --ignore-missing-schemas --quiet fixtures/valid.yaml 175 | [ "$status" -eq 0 ] 176 | [ "$output" = "PASS - fixtures/valid.yaml contains a valid ReplicationController (bob)" ] 177 | } 178 | 179 | @test "Adjusts help string when invoked as a kubectl plugin" { 180 | ln -sf kubeval bin/kubectl-kubeval 181 | 182 | run bin/kubectl-kubeval --help 183 | [ "$status" -eq 0 ] 184 | [[ ${lines[2]} == " kubectl kubeval "* ]] 185 | } 186 | -------------------------------------------------------------------------------- /chocolatey/kubeval/kubeval.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | kubeval 11 | 0.14.0 12 | https://github.com/instrumenta/kubeval/tree/master/chocolatey/kubeval 13 | Gareth Rushgrove 14 | kubeval 15 | Gareth Rushgrove 16 | https://github.com/instrumenta/kubeval 17 | 2017 Gareth Rushgrove 18 | https://github.com/instrumenta/kubeval/blob/master/LICENSE 19 | true 20 | https://github.com/instrumenta/kubeval.git 21 | https://github.com/instrumenta/kubeval/issues 22 | kubeval kubernetes 23 | Validate your Kubernetes configuration files, supports multiple Kubernetes versions 24 | Validate your Kubernetes configuration files, supports multiple Kubernetes versions 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /chocolatey/kubeval/tools/chocolateyinstall.ps1: -------------------------------------------------------------------------------- 1 |  2 | $ErrorActionPreference = 'Stop' 3 | 4 | $packageName= $env:ChocolateyPackageName 5 | $toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" 6 | $url = "https://github.com/instrumenta/kubeval/releases/download/$($env:ChocolateyPackageVersion)/kubeval-windows-386.zip" 7 | $url64 = "https://github.com/instrumenta/kubeval/releases/download/$($env:ChocolateyPackageVersion)/kubeval-windows-amd64.zip" 8 | 9 | $packageArgs = @{ 10 | packageName = $packageName 11 | unzipLocation = $toolsDir 12 | url = $url 13 | url64bit = $url64 14 | 15 | checksum = '5DED35273DD35993C0FC52A08D9CC268487620736C4782077BC72723CC7224D0' 16 | checksumType = 'sha256' 17 | checksum64 = '2A844518981848A7D77CCED9B51A05174BA9C17FC007A1C48CD2AF0D3FB021D7' 18 | checksumType64= 'sha256' 19 | } 20 | 21 | Install-ChocolateyZipPackage @packageArgs 22 | -------------------------------------------------------------------------------- /chocolatey/kubeval/tools/chocolateyuninstall.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop'; 2 | 3 | $kubevalExe = Get-ChildItem $(Split-Path -Parent $MyInvocation.MyCommand.Definition) | Where-Object -Property Name -Match "kubeval.exe" 4 | 5 | if (-Not($kubevalExe)) 6 | { 7 | Write-Error -Message "kubeval.exe not found, please contact the maintainer of the package" -Category ResourceUnavailable 8 | } 9 | 10 | Write-Host "found kubeval.exe in $($kubevalExe.FullName)" 11 | Write-Host "attempting to remove it" 12 | Remove-Item $kubevalExe.FullName 13 | -------------------------------------------------------------------------------- /docs/contrib.md: -------------------------------------------------------------------------------- 1 | # Contrib 2 | 3 | There are lots of different ways of using Kubeval, this page collects some of those 4 | contributed by users. 5 | 6 | ## Git pre-commit hook 7 | 8 | Add the following to your Kubernetes configs repository in `.git/hooks/pre-commit` to trigger `kubeval` before each commit. 9 | 10 | This will validate all the `yaml` files in the top directory of the repository. 11 | 12 | ```shell 13 | #!/bin/sh -e 14 | 15 | echo "Running kubeval validations..." 16 | 17 | if ! [ -x "$(command -v kubeval)" ]; then 18 | echo 'Error: kubeval is not installed.' >&2 19 | exit 1 20 | fi 21 | 22 | # Inspect code using kubeval 23 | if kubeval --strict -d . ; then 24 | echo "Static analysis found no problems." 25 | exit 0 26 | else 27 | echo 1>&2 "Static analysis found violations that need to be fixed." 28 | exit 1 29 | fi 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/go.md: -------------------------------------------------------------------------------- 1 | # Go Library 2 | 3 | Kubeval is implemented in Go, and can be used as a Go library as well as being 4 | used as a command line tool. 5 | 6 | The module can be imported like so: 7 | 8 | 9 | ```go 10 | import ( 11 | "github.com/instrumenta/kubeval/kubeval" 12 | ) 13 | ``` 14 | 15 | The module provides one public function, `Validate`, which can be used 16 | like so: 17 | 18 | ```go 19 | results, err := kubeval.Validate(fileContents, fileName) 20 | ``` 21 | 22 | The method signature for `Validate` is: 23 | 24 | ```go 25 | Validate(input []byte, config kubeval.Config) ([]ValidationResult, error) 26 | ``` 27 | 28 | The simplest way of seeing it's usage is probably in the `kubeval` 29 | [command line tool source code](https://github.com/instrumenta/kubeval/blob/master/main.go). 30 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Using Kubeval 2 | 3 | Kubeval is used to validate one or more Kubernetes configuration files, and 4 | is often used locally as part of a development workflow as well as in CI pipelines. 5 | 6 | At the most basic level, Kubeval is used like so: 7 | 8 | ```console 9 | $ kubeval my-invalid-rc.yaml 10 | WARN - my-invalid-rc.yaml contains an invalid ReplicationController - spec.replicas: Invalid type. Expected: integer, given: string 11 | $ echo $? 12 | 1 13 | ``` 14 | 15 | ## Strict schemas 16 | 17 | The Kubernetes API allows for specifying properties on objects that are not part of the schemas. 18 | However, `kubectl` will throw an error if you use it with such files. Kubeval can be 19 | used to simulate this behaviour using the `--strict` flag. 20 | 21 | ```console 22 | $ kubeval additional-properties.yaml 23 | PASS - additional-properties.yaml contains a valid ReplicationController 24 | $ echo $? 25 | 0 26 | $ kubeval --strict additional-properties.yaml 27 | WARN - additional-properties.yaml contains an invalid ReplicationController - spec: Additional property replicas is not allowed 28 | $ echo $? 29 | 1 30 | ``` 31 | 32 | If you're using `kubectl` you may find it useful to always set the `--strict` flag. 33 | 34 | ## Stdin 35 | 36 | Alternatively Kubeval can also take input via `stdin` which can make using 37 | it as part of an automated pipeline easier by removing the need to securely 38 | manage temporary files. 39 | 40 | ```console 41 | $ cat my-invalid-rc.yaml | kubeval 42 | WARN - stdin contains an invalid ReplicationController - spec.replicas: Invalid type. Expected: integer, given: string 43 | $ echo $? 44 | 1 45 | ``` 46 | 47 | To make the output of pipelines more readable, a filename can be injected 48 | to replace `stdin` in the output: 49 | 50 | ```console 51 | $ cat my-invalid-rc.yaml | kubeval --filename="my-invalid-rc.yaml" 52 | WARN - my-invalid-rc.yaml contains an invalid ReplicationController - spec.replicas: Invalid type. Expected: integer, given: string 53 | $ echo $? 54 | 1 55 | ``` 56 | 57 | ## CRDs 58 | 59 | Currently kubeval relies on schemas generated from the Kubernetes API. This means it's not 60 | possible to validate resources using CRDs. Currently you need to pass a flag to ignore 61 | missing schemas, though this may change in a future major version. 62 | 63 | ```console 64 | $ kubeval --ignore-missing-schemas fixtures/test_crd.yaml 65 | WARN - Set to ignore missing schemas 66 | WARN - fixtures/test_crd.yaml containing a SealedSecret was not validated against a schema 67 | ``` 68 | 69 | If you would prefer to be more explicit about which custom resources to skip you can instead 70 | provide a list of resources to skip like so. 71 | 72 | ```console 73 | $ kubeval --skip-kinds SealedSecret fixtures/test_crd.yaml 74 | WARN - fixtures/test_crd.yaml containing a SealedSecret was not validated against a schema 75 | ``` 76 | 77 | ## Helm 78 | 79 | Helm chart configurations generally have a reference to the source template in a comment 80 | like so: 81 | 82 | ```console 83 | # Source: chart/templates/frontend.yaml 84 | ``` 85 | 86 | When kubeval detects these comments it will report the relevant chart template files in 87 | the output. 88 | 89 | ```console 90 | $ kubeval fixtures/multi_valid_source.yaml 91 | PASS - chart/templates/primary.yaml contains a valid Service 92 | PASS - chart/templates/primary.yaml contains a valid ReplicationControlle 93 | ``` 94 | 95 | ## Configuring Output 96 | 97 | The output of `kubeval` can be configured using the `--output` flag (`-o`). 98 | 99 | As of today `kubeval` supports the following output types: 100 | 101 | - Plaintext `--output=stdout` 102 | - JSON: `--output=json` 103 | - TAP: `--output=tap` 104 | 105 | ### Example Output 106 | 107 | #### Plaintext 108 | 109 | ```console 110 | $ kubeval my-invalid-rc.yaml 111 | WARN - my-invalid-rc.yaml contains an invalid ReplicationController - spec.replicas: Invalid type. Expected: integer, given: string 112 | ``` 113 | 114 | #### JSON 115 | 116 | ```console 117 | $ kubeval fixtures/invalid.yaml -o json 118 | [ 119 | { 120 | "filename": "fixtures/invalid.yaml", 121 | "kind": "ReplicationController", 122 | "status": "invalid", 123 | "errors": [ 124 | "spec.replicas: Invalid type. Expected: [integer,null], given: string" 125 | ] 126 | } 127 | ] 128 | ``` 129 | 130 | #### TAP 131 | 132 | ```console 133 | $ kubeval fixtures/invalid.yaml -o tap 134 | 1..1 135 | not ok 1 - fixtures/invalid.yaml (ReplicationController) - spec.replicas: Invalid type. Expected: [integer,null], given: string 136 | ``` 137 | 138 | ## Full usage instructions 139 | 140 | ```console 141 | $ kubeval --help 142 | Validate a Kubernetes YAML file against the relevant schema 143 | 144 | Usage: 145 | kubeval [file...] [flags] 146 | 147 | Flags: 148 | -d, --directories strings A comma-separated list of directories to recursively search for YAML documents 149 | --exit-on-error Immediately stop execution when the first error is encountered 150 | -f, --filename string filename to be displayed when testing manifests read from stdin (default "stdin") 151 | --force-color Force colored output even if stdout is not a TTY 152 | -h, --help help for kubeval 153 | --ignore-missing-schemas Skip validation for resource definitions without a schema 154 | -v, --kubernetes-version string Version of Kubernetes to validate against (default "master") 155 | --openshift Use OpenShift schemas instead of upstream Kubernetes 156 | -o, --output string The format of the output of this script. Options are: [stdout json] 157 | --schema-location string Base URL used to download schemas. Can also be specified with the environment variable KUBEVAL_SCHEMA_LOCATION 158 | --skip-kinds strings Comma-separated list of case-sensitive kinds to skip when validating against schemas 159 | --strict Disallow additional properties not in schema 160 | --version version for kubeval 161 | ``` 162 | 163 | The command has three important features: 164 | 165 | - You can pass one or more files as arguments, including using wildcard 166 | expansion. Each file will be validated in turn, and `kubeval` will 167 | exit with a non-zero code if _any_ of the files fail validation. 168 | - You can toggle between the upstream Kubernetes definitions and the 169 | expanded OpenShift ones using the `--openshift` flag. The default is 170 | to use the upstream Kubernetes definitions. 171 | - You can pass a version of Kubernetes or OpenShift and the relevant 172 | type schemas for that version will be used. For instance: 173 | 174 | ```console 175 | $ kubeval -v 1.6.6 my-deployment.yaml 176 | $ kubeval --openshift -v 1.5.1 my-deployment.yaml 177 | ``` 178 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installing Kubeval 2 | 3 | Tagged versions of `kubeval` are built using [GoReleaser](https://goreleaser.com/) and 4 | uploaded to GitHub. This means you should find `tar.gz` and `.zip` files under the 5 | release tab. These should contain a single `kubeval` binary for the platform 6 | in the filename (ie. windows, linux, darwin). Either execute that binary 7 | directly or place it on your path. 8 | 9 | 10 | ## Linux 11 | 12 | ``` 13 | wget https://github.com/instrumenta/kubeval/releases/latest/download/kubeval-linux-amd64.tar.gz 14 | tar xf kubeval-linux-amd64.tar.gz 15 | sudo cp kubeval /usr/local/bin 16 | ``` 17 | 18 | ## macOS 19 | 20 | ``` 21 | wget https://github.com/instrumenta/kubeval/releases/latest/download/kubeval-darwin-amd64.tar.gz 22 | tar xf kubeval-darwin-amd64.tar.gz 23 | sudo cp kubeval /usr/local/bin 24 | ``` 25 | 26 | For those using [Homebrew](https://brew.sh/) you can use the kubeval tap: 27 | 28 | ``` 29 | brew tap instrumenta/instrumenta 30 | brew install kubeval 31 | ``` 32 | 33 | ## Windows 34 | 35 | Windows users can download the `zip` files from the releases page. For [Scoop](https://scoop.sh/) 36 | users you can install with: 37 | 38 | ``` 39 | scoop bucket add instrumenta https://github.com/instrumenta/scoop-instrumenta 40 | scoop install kubeval 41 | ``` 42 | 43 | ## Docker 44 | 45 | 46 | `kubeval` is also published as a Docker image. This can be used as follows: 47 | follows: 48 | 49 | ``` 50 | $ docker run -it -v `pwd`/fixtures:/fixtures garethr/kubeval fixtures/* 51 | Missing a kind key in /fixtures/blank.yaml 52 | The document fixtures/int_or_string.yaml contains a valid Service 53 | The document fixtures/int_or_string_false.yaml contains an invalid Deployment 54 | --> spec.template.spec.containers.0.env.0.value: Invalid type. Expected: string, given: integer 55 | The document fixtures/invalid.yaml contains an invalid ReplicationController 56 | --> spec.replicas: Invalid type. Expected: integer, given: string 57 | Missing a kind key in /fixtures/missing-kind.yaml 58 | The document fixtures/valid.json contains a valid Deployment 59 | The document fixtures/valid.yaml contains a valid ReplicationController 60 | ``` 61 | -------------------------------------------------------------------------------- /fixtures/blank.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instrumenta/kubeval/fe0a7c22b93b92adfdc57d07b92d5231fd0b3e0e/fixtures/blank.yaml -------------------------------------------------------------------------------- /fixtures/comment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Arbitrary comment 3 | 4 | -------------------------------------------------------------------------------- /fixtures/duplicates-non-namespaced.yaml: -------------------------------------------------------------------------------- 1 | # Two objects with same name in same namespace, resource of non-namespaced kind 2 | 3 | apiVersion: v1 4 | kind: PersistentVolume 5 | metadata: 6 | name: pv0003 7 | spec: 8 | capacity: 9 | storage: 5Gi 10 | volumeMode: Filesystem 11 | accessModes: 12 | - ReadWriteOnce 13 | persistentVolumeReclaimPolicy: Recycle 14 | storageClassName: slow 15 | mountOptions: 16 | - hard 17 | - nfsvers=4.1 18 | nfs: 19 | path: /tmp 20 | server: 172.17.0.2 21 | --- 22 | apiVersion: v1 23 | kind: PersistentVolume 24 | metadata: 25 | name: pv0003 26 | spec: 27 | capacity: 28 | storage: 5Gi 29 | volumeMode: Filesystem 30 | accessModes: 31 | - ReadWriteOnce 32 | persistentVolumeReclaimPolicy: Recycle 33 | storageClassName: slow 34 | mountOptions: 35 | - hard 36 | - nfsvers=4.1 37 | nfs: 38 | path: /tmp 39 | server: 172.17.0.2 40 | -------------------------------------------------------------------------------- /fixtures/duplicates-skipped-kinds.yaml: -------------------------------------------------------------------------------- 1 | # Two objects with same name in same namespace, but of a kind configured to be skipped 2 | 3 | apiVersion: v1 4 | kind: SkipThisKind 5 | metadata: 6 | name: "identical" 7 | spec: 8 | replicas: 2 9 | selector: 10 | app: nginx 11 | template: 12 | metadata: 13 | name: nginx 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx 19 | image: nginx 20 | ports: 21 | - containerPort: 80 22 | --- 23 | apiVersion: v1 24 | kind: SkipThisKind 25 | metadata: 26 | name: "identical" 27 | spec: 28 | replicas: 2 29 | selector: 30 | app: nginx 31 | template: 32 | metadata: 33 | name: nginx 34 | labels: 35 | app: nginx 36 | spec: 37 | containers: 38 | - name: nginx 39 | image: nginx 40 | ports: 41 | - containerPort: 80 42 | -------------------------------------------------------------------------------- /fixtures/duplicates-with-namespace-default.yaml: -------------------------------------------------------------------------------- 1 | # Two objects with same name in same namespace (one of them not given, i.e. will use default namespace as passed to kubeval) 2 | 3 | apiVersion: v1 4 | kind: ReplicationController 5 | metadata: 6 | name: "bob" 7 | namespace: the-default-namespace 8 | spec: 9 | replicas: 2 10 | selector: 11 | app: nginx 12 | template: 13 | metadata: 14 | name: nginx 15 | labels: 16 | app: nginx 17 | spec: 18 | containers: 19 | - name: nginx 20 | image: nginx 21 | ports: 22 | - containerPort: 80 23 | --- 24 | apiVersion: v1 25 | kind: ReplicationController 26 | metadata: 27 | name: "bob" 28 | # namespace not given 29 | spec: 30 | replicas: 2 31 | selector: 32 | app: nginx 33 | template: 34 | metadata: 35 | name: nginx 36 | labels: 37 | app: nginx 38 | spec: 39 | containers: 40 | - name: nginx 41 | image: nginx 42 | ports: 43 | - containerPort: 80 44 | -------------------------------------------------------------------------------- /fixtures/duplicates-with-namespace.yaml: -------------------------------------------------------------------------------- 1 | # Two objects with same name in same namespace 2 | 3 | apiVersion: v1 4 | kind: ReplicationController 5 | metadata: 6 | name: "bob" 7 | namespace: x 8 | spec: 9 | replicas: 2 10 | selector: 11 | app: nginx 12 | template: 13 | metadata: 14 | name: nginx 15 | labels: 16 | app: nginx 17 | spec: 18 | containers: 19 | - name: nginx 20 | image: nginx 21 | ports: 22 | - containerPort: 80 23 | --- 24 | apiVersion: v1 25 | kind: ReplicationController 26 | metadata: 27 | name: "bob" 28 | namespace: x 29 | spec: 30 | replicas: 2 31 | selector: 32 | app: nginx 33 | template: 34 | metadata: 35 | name: nginx 36 | labels: 37 | app: nginx 38 | spec: 39 | containers: 40 | - name: nginx 41 | image: nginx 42 | ports: 43 | - containerPort: 80 44 | -------------------------------------------------------------------------------- /fixtures/duplicates.yaml: -------------------------------------------------------------------------------- 1 | # Two objects with same name in same namespace 2 | 3 | apiVersion: v1 4 | kind: ReplicationController 5 | metadata: 6 | name: "bob" 7 | spec: 8 | replicas: 2 9 | selector: 10 | app: nginx 11 | template: 12 | metadata: 13 | name: nginx 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx 19 | image: nginx 20 | ports: 21 | - containerPort: 80 22 | --- 23 | apiVersion: v1 24 | kind: ReplicationController 25 | metadata: 26 | name: "bob" 27 | spec: 28 | replicas: 2 29 | selector: 30 | app: nginx 31 | template: 32 | metadata: 33 | name: nginx 34 | labels: 35 | app: nginx 36 | spec: 37 | containers: 38 | - name: nginx 39 | image: nginx 40 | ports: 41 | - containerPort: 80 42 | -------------------------------------------------------------------------------- /fixtures/extra_property.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: DaemonSet 4 | metadata: 5 | name: nginx-ds 6 | spec: 7 | replicas: 2 8 | template: 9 | spec: 10 | containers: 11 | - image: nginx 12 | name: nginx 13 | -------------------------------------------------------------------------------- /fixtures/full_domain_group.yaml: -------------------------------------------------------------------------------- 1 | kind: RoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: tiller-binding 5 | namespace: dev2 6 | subjects: 7 | - kind: ServiceAccount 8 | name: tiller 9 | namespace: dev2 10 | roleRef: 11 | kind: Role 12 | name: tiller-manager 13 | apiGroup: rbac.authorization.k8s.io 14 | -------------------------------------------------------------------------------- /fixtures/generate_name.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | generateName: pi- 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: pi 10 | image: perl 11 | command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] 12 | restartPolicy: Never 13 | backoffLimit: 4 14 | -------------------------------------------------------------------------------- /fixtures/int_or_string.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | task: monitoring 6 | # For use as a Cluster add-on (https://github.com/kubernetes/kubernetes/tree/master/cluster/addons) 7 | # If you are NOT using this as an addon, you should comment out this line. 8 | kubernetes.io/cluster-service: 'true' 9 | kubernetes.io/name: Heapster 10 | name: heapster 11 | namespace: kube-system 12 | spec: 13 | ports: 14 | - port: 80 15 | targetPort: 8082 16 | selector: 17 | k8s-app: heapster 18 | -------------------------------------------------------------------------------- /fixtures/invalid.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | name: "bob" 5 | spec: 6 | replicas: asd" 7 | selector: 8 | app: nginx 9 | templates: 10 | metadata: 11 | name: nginx 12 | labels: 13 | app: nginx 14 | spec: 15 | containers: 16 | - name: nginx 17 | image: nginx 18 | ports: 19 | - containerPort: 80 20 | -------------------------------------------------------------------------------- /fixtures/list_empty_valid.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: List 4 | items: [] 5 | -------------------------------------------------------------------------------- /fixtures/list_invalid.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: List 3 | items: 4 | - apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: redis-master 8 | labels: 9 | app: redis 10 | tier: backend 11 | role: master 12 | spec: 13 | ports: 14 | # the port that this service should serve on 15 | - port: 6379 16 | targetPort: 6379 17 | selector: 18 | app: redis 19 | tier: backend 20 | role: master 21 | - apiVersion: v1 22 | kind: ReplicationController 23 | metadata: 24 | name: "bob" 25 | spec: 26 | replicas: asd" 27 | selector: 28 | app: nginx 29 | templates: 30 | metadata: 31 | name: nginx 32 | labels: 33 | app: nginx 34 | spec: 35 | containers: 36 | - name: nginx 37 | image: nginx 38 | ports: 39 | - containerPort: 80 -------------------------------------------------------------------------------- /fixtures/list_valid.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: List 3 | items: 4 | - apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: redis-master 8 | labels: 9 | app: redis 10 | tier: backend 11 | role: master 12 | spec: 13 | ports: 14 | # the port that this service should serve on 15 | - port: 6379 16 | targetPort: 6379 17 | selector: 18 | app: redis 19 | tier: backend 20 | role: master 21 | - apiVersion: v1 22 | kind: ReplicationController 23 | metadata: 24 | name: redis-master 25 | # these labels can be applied automatically 26 | # from the labels in the pod template if not set 27 | labels: 28 | app: redis 29 | role: master 30 | tier: backend 31 | spec: 32 | # this replicas value is default 33 | # modify it according to your case 34 | replicas: 1 35 | # selector can be applied automatically 36 | # from the labels in the pod template if not set 37 | # selector: 38 | # app: guestbook 39 | # role: master 40 | # tier: backend 41 | template: 42 | metadata: 43 | labels: 44 | app: redis 45 | role: master 46 | tier: backend 47 | spec: 48 | containers: 49 | - name: master 50 | image: redis 51 | resources: 52 | requests: 53 | cpu: 100m 54 | memory: 100Mi 55 | ports: 56 | - containerPort: 6379 57 | - apiVersion: v1 58 | kind: Service 59 | metadata: 60 | name: redis-slave 61 | labels: 62 | app: redis 63 | tier: backend 64 | role: slave 65 | spec: 66 | ports: 67 | # the port that this service should serve on 68 | - port: 6379 69 | selector: 70 | app: redis 71 | tier: backend 72 | role: slave 73 | - apiVersion: v1 74 | kind: ReplicationController 75 | metadata: 76 | name: redis-slave 77 | # these labels can be applied automatically 78 | # from the labels in the pod template if not set 79 | labels: 80 | app: redis 81 | role: slave 82 | tier: backend 83 | spec: 84 | # this replicas value is default 85 | # modify it according to your case 86 | replicas: 2 87 | # selector can be applied automatically 88 | # from the labels in the pod template if not set 89 | # selector: 90 | # app: guestbook 91 | # role: slave 92 | # tier: backend 93 | template: 94 | metadata: 95 | labels: 96 | app: redis 97 | role: slave 98 | tier: backend 99 | spec: 100 | containers: 101 | - name: slave 102 | image: gcr.io/google_samples/gb-redisslave:v1 103 | resources: 104 | requests: 105 | cpu: 100m 106 | memory: 100Mi 107 | env: 108 | - name: GET_HOSTS_FROM 109 | value: dns 110 | # If your cluster config does not include a dns service, then to 111 | # instead access an environment variable to find the master 112 | # service's host, comment out the 'value: dns' line above, and 113 | # uncomment the line below. 114 | # value: env 115 | ports: 116 | - containerPort: 6379 117 | - apiVersion: v1 118 | kind: Service 119 | metadata: 120 | name: frontend 121 | labels: 122 | app: guestbook 123 | tier: frontend 124 | spec: 125 | # if your cluster supports it, uncomment the following to automatically create 126 | # an external load-balanced IP for the frontend service. 127 | # type: LoadBalancer 128 | ports: 129 | # the port that this service should serve on 130 | - port: 80 131 | selector: 132 | app: guestbook 133 | tier: frontend 134 | - apiVersion: v1 135 | kind: ReplicationController 136 | metadata: 137 | name: frontend 138 | # these labels can be applied automatically 139 | # from the labels in the pod template if not set 140 | labels: 141 | app: guestbook 142 | tier: frontend 143 | spec: 144 | # this replicas value is default 145 | # modify it according to your case 146 | replicas: 3 147 | # selector can be applied automatically 148 | # from the labels in the pod template if not set 149 | # selector: 150 | # app: guestbook 151 | # tier: frontend 152 | template: 153 | metadata: 154 | labels: 155 | app: guestbook 156 | tier: frontend 157 | spec: 158 | containers: 159 | - name: php-redis 160 | image: gcr.io/google_samples/gb-frontend:v3 161 | resources: 162 | requests: 163 | cpu: 100m 164 | memory: 100Mi 165 | env: 166 | - name: GET_HOSTS_FROM 167 | value: dns 168 | # If your cluster config does not include a dns service, then to 169 | # instead access environment variables to find service host 170 | # info, comment out the 'value: dns' line above, and uncomment the 171 | # line below. 172 | # value: env 173 | ports: 174 | - containerPort: 80 -------------------------------------------------------------------------------- /fixtures/missing_kind.yaml: -------------------------------------------------------------------------------- 1 | key: value 2 | -------------------------------------------------------------------------------- /fixtures/missing_kind_value.yaml: -------------------------------------------------------------------------------- 1 | kind: 2 | -------------------------------------------------------------------------------- /fixtures/multi_invalid.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: redis-master 5 | labels: 6 | app: redis 7 | tier: backend 8 | role: master 9 | spec: 10 | ports: 11 | # the port that this service should serve on 12 | - port: sds 13 | targetPort: 6379 14 | selector: 15 | app: redis 16 | tier: backend 17 | role: master 18 | --- 19 | apiVersion: v1 20 | kind: ReplicationController 21 | metadata: 22 | name: redis-master 23 | # these labels can be applied automatically 24 | # from the labels in the pod template if not set 25 | labels: 26 | app: redis 27 | role: master 28 | tier: backend 29 | spec: 30 | # this replicas value is default 31 | # modify it according to your case 32 | replicas: 1 33 | # selector can be applied automatically 34 | # from the labels in the pod template if not set 35 | # selector: 36 | # app: guestbook 37 | # role: master 38 | # tier: backend 39 | template: 40 | metadata: 41 | labels: 42 | app: redis 43 | role: master 44 | tier: backend 45 | spec: 46 | containers: 47 | - name: master 48 | image: redis 49 | resources: 50 | requests: 51 | cpu: 100m 52 | memory: 100Mi 53 | ports: 54 | - containerPort: 6379 55 | --- 56 | apiVersion: v1 57 | kind: Service 58 | metadata: 59 | name: redis-slave 60 | labels: 61 | app: redis 62 | tier: backend 63 | role: slave 64 | spec: 65 | ports: 66 | # the port that this service should serve on 67 | - port: 6379 68 | selector: 69 | app: redis 70 | tier: backend 71 | role: slave 72 | --- 73 | apiVersion: v1 74 | kind: ReplicationController 75 | metadata: 76 | name: redis-slave 77 | # these labels can be applied automatically 78 | # from the labels in the pod template if not set 79 | labels: 80 | app: redis 81 | role: slave 82 | tier: backend 83 | spec: 84 | # this replicas value is default 85 | # modify it according to your case 86 | replicas: 2 87 | # selector can be applied automatically 88 | # from the labels in the pod template if not set 89 | # selector: 90 | # app: guestbook 91 | # role: slave 92 | # tier: backend 93 | template: 94 | metadata: 95 | labels: 96 | app: redis 97 | role: slave 98 | tier: backend 99 | spec: 100 | containers: 101 | - name: slave 102 | image: gcr.io/google_samples/gb-redisslave:v1 103 | resources: 104 | requests: 105 | cpu: 100m 106 | memory: 100Mi 107 | env: 108 | - name: GET_HOSTS_FROM 109 | value: dns 110 | # If your cluster config does not include a dns service, then to 111 | # instead access an environment variable to find the master 112 | # service's host, comment out the 'value: dns' line above, and 113 | # uncomment the line below. 114 | # value: env 115 | ports: 116 | - containerPort: 6379 117 | --- 118 | apiVersion: v1 119 | kind: Service 120 | metadata: 121 | name: frontend 122 | labels: 123 | app: guestbook 124 | tier: frontend 125 | spec: 126 | # if your cluster supports it, uncomment the following to automatically create 127 | # an external load-balanced IP for the frontend service. 128 | # type: LoadBalancer 129 | ports: 130 | # the port that this service should serve on 131 | - port: 80 132 | selector: 133 | app: guestbook 134 | tier: frontend 135 | --- 136 | apiVersion: v1 137 | kind: ReplicationController 138 | metadata: 139 | name: frontend 140 | # these labels can be applied automatically 141 | # from the labels in the pod template if not set 142 | labels: 143 | app: guestbook 144 | tier: frontend 145 | spec: 146 | # this replicas value is default 147 | # modify it according to your case 148 | replicas: 3 149 | # selector can be applied automatically 150 | # from the labels in the pod template if not set 151 | # selector: 152 | # app: guestbook 153 | # tier: frontend 154 | template: 155 | metadata: 156 | labels: 157 | app: guestbook 158 | tier: frontend 159 | spec: 160 | containers: 161 | - name: php-redis 162 | image: gcr.io/google_samples/gb-frontend:v3 163 | resources: 164 | requests: 165 | cpu: 100m 166 | memory: 100Mi 167 | env: 168 | - name: GET_HOSTS_FROM 169 | value: dns 170 | # If your cluster config does not include a dns service, then to 171 | # instead access environment variables to find service host 172 | # info, comment out the 'value: dns' line above, and uncomment the 173 | # line below. 174 | # value: env 175 | ports: 176 | - containerPort: 80 177 | -------------------------------------------------------------------------------- /fixtures/multi_invalid_resources.yaml: -------------------------------------------------------------------------------- 1 | kind: 2 | --- 3 | kind: 4 | --- 5 | kind: 6 | --- 7 | kind: 8 | --- 9 | kind: 10 | -------------------------------------------------------------------------------- /fixtures/multi_valid.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: redis-master 6 | labels: 7 | app: redis 8 | tier: backend 9 | role: master 10 | spec: 11 | ports: 12 | # the port that this service should serve on 13 | - port: 6379 14 | targetPort: 6379 15 | selector: 16 | app: redis 17 | tier: backend 18 | role: master 19 | --- 20 | apiVersion: v1 21 | kind: ReplicationController 22 | metadata: 23 | name: redis-master 24 | # these labels can be applied automatically 25 | # from the labels in the pod template if not set 26 | labels: 27 | app: redis 28 | role: master 29 | tier: backend 30 | spec: 31 | # this replicas value is default 32 | # modify it according to your case 33 | replicas: 1 34 | # selector can be applied automatically 35 | # from the labels in the pod template if not set 36 | # selector: 37 | # app: guestbook 38 | # role: master 39 | # tier: backend 40 | template: 41 | metadata: 42 | labels: 43 | app: redis 44 | role: master 45 | tier: backend 46 | spec: 47 | containers: 48 | - name: master 49 | image: redis 50 | resources: 51 | requests: 52 | cpu: 100m 53 | memory: 100Mi 54 | ports: 55 | - containerPort: 6379 56 | --- 57 | apiVersion: v1 58 | kind: Service 59 | metadata: 60 | name: redis-slave 61 | labels: 62 | app: redis 63 | tier: backend 64 | role: slave 65 | spec: 66 | ports: 67 | # the port that this service should serve on 68 | - port: 6379 69 | selector: 70 | app: redis 71 | tier: backend 72 | role: slave 73 | --- 74 | apiVersion: v1 75 | kind: ReplicationController 76 | metadata: 77 | name: redis-slave 78 | # these labels can be applied automatically 79 | # from the labels in the pod template if not set 80 | labels: 81 | app: redis 82 | role: slave 83 | tier: backend 84 | spec: 85 | # this replicas value is default 86 | # modify it according to your case 87 | replicas: 2 88 | # selector can be applied automatically 89 | # from the labels in the pod template if not set 90 | # selector: 91 | # app: guestbook 92 | # role: slave 93 | # tier: backend 94 | template: 95 | metadata: 96 | labels: 97 | app: redis 98 | role: slave 99 | tier: backend 100 | spec: 101 | containers: 102 | - name: slave 103 | image: gcr.io/google_samples/gb-redisslave:v1 104 | resources: 105 | requests: 106 | cpu: 100m 107 | memory: 100Mi 108 | env: 109 | - name: GET_HOSTS_FROM 110 | value: dns 111 | # If your cluster config does not include a dns service, then to 112 | # instead access an environment variable to find the master 113 | # service's host, comment out the 'value: dns' line above, and 114 | # uncomment the line below. 115 | # value: env 116 | ports: 117 | - containerPort: 6379 118 | --- 119 | apiVersion: v1 120 | kind: Service 121 | metadata: 122 | name: frontend 123 | labels: 124 | app: guestbook 125 | tier: frontend 126 | spec: 127 | # if your cluster supports it, uncomment the following to automatically create 128 | # an external load-balanced IP for the frontend service. 129 | # type: LoadBalancer 130 | ports: 131 | # the port that this service should serve on 132 | - port: 80 133 | selector: 134 | app: guestbook 135 | tier: frontend 136 | --- 137 | apiVersion: v1 138 | kind: ReplicationController 139 | metadata: 140 | name: frontend 141 | # these labels can be applied automatically 142 | # from the labels in the pod template if not set 143 | labels: 144 | app: guestbook 145 | tier: frontend 146 | spec: 147 | # this replicas value is default 148 | # modify it according to your case 149 | replicas: 3 150 | # selector can be applied automatically 151 | # from the labels in the pod template if not set 152 | # selector: 153 | # app: guestbook 154 | # tier: frontend 155 | template: 156 | metadata: 157 | labels: 158 | app: guestbook 159 | tier: frontend 160 | spec: 161 | containers: 162 | - name: php-redis 163 | image: gcr.io/google_samples/gb-frontend:v3 164 | resources: 165 | requests: 166 | cpu: 100m 167 | memory: 100Mi 168 | env: 169 | - name: GET_HOSTS_FROM 170 | value: dns 171 | # If your cluster config does not include a dns service, then to 172 | # instead access environment variables to find service host 173 | # info, comment out the 'value: dns' line above, and uncomment the 174 | # line below. 175 | # value: env 176 | ports: 177 | - containerPort: 80 178 | --- 179 | --- 180 | # an empty resource with comments 181 | --- 182 | apiVersion: v1 183 | kind: List 184 | items: 185 | - apiVersion: v1 186 | kind: Namespace 187 | metadata: 188 | name: b 189 | -------------------------------------------------------------------------------- /fixtures/multi_valid_source.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: chart/templates/primary.yaml 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: redis-primary 7 | labels: 8 | app: redis 9 | tier: backend 10 | role: primary 11 | spec: 12 | ports: 13 | # the port that this service should serve on 14 | - port: 6379 15 | targetPort: 6379 16 | selector: 17 | app: redis 18 | tier: backend 19 | role: primary 20 | --- 21 | apiVersion: v1 22 | kind: ReplicationController 23 | metadata: 24 | name: redis-primary 25 | # these labels can be applied automatically 26 | # from the labels in the pod template if not set 27 | labels: 28 | app: redis 29 | role: primary 30 | tier: backend 31 | spec: 32 | # this replicas value is default 33 | # modify it according to your case 34 | replicas: 1 35 | # selector can be applied automatically 36 | # from the labels in the pod template if not set 37 | # selector: 38 | # app: guestbook 39 | # role: primary 40 | # tier: backend 41 | template: 42 | metadata: 43 | labels: 44 | app: redis 45 | role: primary 46 | tier: backend 47 | spec: 48 | containers: 49 | - name: primary 50 | image: redis 51 | resources: 52 | requests: 53 | cpu: 100m 54 | memory: 100Mi 55 | ports: 56 | - containerPort: 6379 57 | --- 58 | # Source: chart/templates/secondary.yaml 59 | apiVersion: v1 60 | kind: Service 61 | metadata: 62 | name: redis-secondary 63 | labels: 64 | app: redis 65 | tier: backend 66 | role: secondary 67 | spec: 68 | ports: 69 | # the port that this service should serve on 70 | - port: 6379 71 | selector: 72 | app: redis 73 | tier: backend 74 | role: secondary 75 | --- 76 | apiVersion: v1 77 | kind: ReplicationController 78 | metadata: 79 | name: redis-secondary 80 | # these labels can be applied automatically 81 | # from the labels in the pod template if not set 82 | labels: 83 | app: redis 84 | role: secondary 85 | tier: backend 86 | spec: 87 | # this replicas value is default 88 | # modify it according to your case 89 | replicas: 2 90 | # selector can be applied automatically 91 | # from the labels in the pod template if not set 92 | # selector: 93 | # app: guestbook 94 | # role: secondary 95 | # tier: backend 96 | template: 97 | metadata: 98 | labels: 99 | app: redis 100 | role: secondary 101 | tier: backend 102 | spec: 103 | containers: 104 | - name: secondary 105 | image: gcr.io/google_samples/gb-redissecondary:v1 106 | resources: 107 | requests: 108 | cpu: 100m 109 | memory: 100Mi 110 | env: 111 | - name: GET_HOSTS_FROM 112 | value: dns 113 | # If your cluster config does not include a dns service, then to 114 | # instead access an environment variable to find the primary 115 | # service's host, comment out the 'value: dns' line above, and 116 | # uncomment the line below. 117 | # value: env 118 | ports: 119 | - containerPort: 6379 120 | --- 121 | # Source: chart/templates/frontend.yaml 122 | apiVersion: v1 123 | kind: Service 124 | metadata: 125 | name: frontend 126 | labels: 127 | app: guestbook 128 | tier: frontend 129 | spec: 130 | # if your cluster supports it, uncomment the following to automatically create 131 | # an external load-balanced IP for the frontend service. 132 | # type: LoadBalancer 133 | ports: 134 | # the port that this service should serve on 135 | - port: 80 136 | selector: 137 | app: guestbook 138 | tier: frontend 139 | --- 140 | apiVersion: v1 141 | kind: ReplicationController 142 | metadata: 143 | name: frontend 144 | # these labels can be applied automatically 145 | # from the labels in the pod template if not set 146 | labels: 147 | app: guestbook 148 | tier: frontend 149 | spec: 150 | # this replicas value is default 151 | # modify it according to your case 152 | replicas: 3 153 | # selector can be applied automatically 154 | # from the labels in the pod template if not set 155 | # selector: 156 | # app: guestbook 157 | # tier: frontend 158 | template: 159 | metadata: 160 | labels: 161 | app: guestbook 162 | tier: frontend 163 | spec: 164 | containers: 165 | - name: php-redis 166 | image: gcr.io/google_samples/gb-frontend:v3 167 | resources: 168 | requests: 169 | cpu: 100m 170 | memory: 100Mi 171 | env: 172 | - name: GET_HOSTS_FROM 173 | value: dns 174 | # If your cluster config does not include a dns service, then to 175 | # instead access environment variables to find service host 176 | # info, comment out the 'value: dns' line above, and uncomment the 177 | # line below. 178 | # value: env 179 | ports: 180 | - containerPort: 80 181 | --- 182 | --- 183 | # an empty resource with comments 184 | --- 185 | -------------------------------------------------------------------------------- /fixtures/null_array.yaml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | apiVersion: extensions/v1beta1 3 | metadata: 4 | labels: 5 | k8s-app: kubernetes-dashboard 6 | name: kubernetes-dashboard 7 | namespace: kube-system 8 | spec: 9 | replicas: 1 10 | revisionHistoryLimit: 10 11 | selector: 12 | matchLabels: 13 | k8s-app: kubernetes-dashboard 14 | template: 15 | metadata: 16 | labels: 17 | k8s-app: kubernetes-dashboard 18 | spec: 19 | containers: 20 | - name: kubernetes-dashboard 21 | image: gcr.io/google_containers/kubernetes-dashboard-amd64:v1.6.1 22 | ports: 23 | - containerPort: 9090 24 | protocol: TCP 25 | args: 26 | # Uncomment the following line to manually specify Kubernetes API server Host 27 | # If not specified, Dashboard will attempt to auto discover the API server and connect 28 | # to it. Uncomment only if the default does not work. 29 | # - --apiserver-host=http://my-address:port 30 | livenessProbe: 31 | httpGet: 32 | path: / 33 | port: 9090 34 | initialDelaySeconds: 30 35 | timeoutSeconds: 30 36 | serviceAccountName: kubernetes-dashboard 37 | # Comment the following tolerations if Dashboard must not be deployed on master 38 | tolerations: 39 | - key: node-role.kubernetes.io/master 40 | effect: NoSchedule 41 | -------------------------------------------------------------------------------- /fixtures/null_string.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | kompose.service.type: LoadBalancer 6 | creationTimestamp: null 7 | labels: 8 | io.kompose.service: frontend 9 | name: frontend 10 | spec: 11 | ports: 12 | - name: "80" 13 | port: 80 14 | targetPort: 80 15 | selector: 16 | io.kompose.service: frontend 17 | type: LoadBalancer 18 | status: 19 | loadBalancer: {} 20 | -------------------------------------------------------------------------------- /fixtures/quantity.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: LimitRange 3 | metadata: 4 | name: mem-limit-range 5 | spec: 6 | limits: 7 | - default: 8 | memory: 512Mi 9 | defaultRequest: 10 | memory: 256000 11 | type: Container 12 | -------------------------------------------------------------------------------- /fixtures/same-kind-different-api.yaml: -------------------------------------------------------------------------------- 1 | # Two objects with same name in same namespace, and having the same kind, but 2 | # of different API (apps/v1 vs. apps/v1beta1). This is important when CRDs 3 | # introduce overlapping `metadata:name` values, e.g. `Deployment` in 4 | # `my-awesome-cd-tool.io/v1` (contrived scenario). 5 | 6 | 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | metadata: 10 | name: nginx-deployment 11 | labels: 12 | app: nginx 13 | spec: 14 | replicas: 3 15 | selector: 16 | matchLabels: 17 | app: nginx 18 | template: 19 | metadata: 20 | labels: 21 | app: nginx 22 | spec: 23 | containers: 24 | - name: nginx 25 | image: nginx:1.7.9 26 | ports: 27 | - containerPort: 80 28 | --- 29 | apiVersion: apps/v1beta1 30 | kind: Deployment 31 | metadata: 32 | name: nginx-deployment 33 | labels: 34 | app: nginx 35 | spec: 36 | replicas: 3 37 | selector: 38 | matchLabels: 39 | app: nginx 40 | template: 41 | metadata: 42 | labels: 43 | app: nginx 44 | spec: 45 | containers: 46 | - name: nginx 47 | image: nginx:1.7.9 48 | ports: 49 | - containerPort: 80 50 | -------------------------------------------------------------------------------- /fixtures/same-object-different-namespace-default.yaml: -------------------------------------------------------------------------------- 1 | # Two objects with same name in different namespace, one of them being the configured default namespace 2 | 3 | apiVersion: v1 4 | kind: ReplicationController 5 | metadata: 6 | name: "bob" 7 | namespace: a 8 | spec: 9 | replicas: 2 10 | selector: 11 | app: nginx 12 | template: 13 | metadata: 14 | name: nginx 15 | labels: 16 | app: nginx 17 | spec: 18 | containers: 19 | - name: nginx 20 | image: nginx 21 | ports: 22 | - containerPort: 80 23 | --- 24 | apiVersion: v1 25 | kind: ReplicationController 26 | metadata: 27 | name: "bob" 28 | namespace: the-default-namespace 29 | spec: 30 | replicas: 2 31 | selector: 32 | app: nginx 33 | template: 34 | metadata: 35 | name: nginx 36 | labels: 37 | app: nginx 38 | spec: 39 | containers: 40 | - name: nginx 41 | image: nginx 42 | ports: 43 | - containerPort: 80 44 | -------------------------------------------------------------------------------- /fixtures/same-object-different-namespace.yaml: -------------------------------------------------------------------------------- 1 | # Two objects with same name in different namespace 2 | 3 | apiVersion: v1 4 | kind: ReplicationController 5 | metadata: 6 | name: "bob" 7 | namespace: a 8 | spec: 9 | replicas: 2 10 | selector: 11 | app: nginx 12 | template: 13 | metadata: 14 | name: nginx 15 | labels: 16 | app: nginx 17 | spec: 18 | containers: 19 | - name: nginx 20 | image: nginx 21 | ports: 22 | - containerPort: 80 23 | --- 24 | apiVersion: v1 25 | kind: ReplicationController 26 | metadata: 27 | name: "bob" 28 | namespace: b 29 | spec: 30 | replicas: 2 31 | selector: 32 | app: nginx 33 | template: 34 | metadata: 35 | name: nginx 36 | labels: 37 | app: nginx 38 | spec: 39 | containers: 40 | - name: nginx 41 | image: nginx 42 | ports: 43 | - containerPort: 80 44 | -------------------------------------------------------------------------------- /fixtures/test_crd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: bitnami.com/v1alpha1 3 | kind: SealedSecret 4 | metadata: 5 | name: test-secret 6 | namespace: test-namespace 7 | spec: 8 | encryptedData: 9 | SOME_ENCRYPTED_DATA: c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2 10 | --- 11 | apiVersion: bitnami.com/v1alpha1 12 | kind: SealedSecret 13 | metadata: 14 | name: test-secret-clone 15 | namespace: test-namespace 16 | spec: 17 | encryptedData: 18 | SOME_ENCRYPTED_DATA: c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2 19 | -------------------------------------------------------------------------------- /fixtures/unconventional_keys.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: unconventional-keys 5 | data: 6 | 5: "integer" 7 | 3.14: "float" 8 | true: "boolean" 9 | -------------------------------------------------------------------------------- /fixtures/valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "apps/v1beta1", 3 | "kind": "Deployment", 4 | "metadata": { 5 | "name": "nginx-deployment", 6 | "namespace": "default" 7 | }, 8 | "spec": { 9 | "replicas": 2, 10 | "template": { 11 | "spec": { 12 | "affinity": { }, 13 | "containers": [ 14 | { 15 | "args": [ ], 16 | "command": [ ], 17 | "env": [ ], 18 | "envFrom": [ ], 19 | "image": "nginx:1.7.9", 20 | "lifecycle": { }, 21 | "livenessProbe": { }, 22 | "name": "nginx", 23 | "ports": [ 24 | { 25 | "containerPort": 80, 26 | "name": "http" 27 | } 28 | ], 29 | "readinessProbe": { }, 30 | "resources": { }, 31 | "securityContext": { }, 32 | "volumeMounts": [ ] 33 | } 34 | ], 35 | "hostMappings": [ ], 36 | "imagePullSecrets": [ ], 37 | "initContainers": [ ], 38 | "nodeSelector": { }, 39 | "securityContext": { }, 40 | "tolerations": [ ], 41 | "volumes": [ ] 42 | } 43 | } 44 | }, 45 | "status": { } 46 | } 47 | -------------------------------------------------------------------------------- /fixtures/valid.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | name: "bob" 5 | spec: 6 | replicas: 2 7 | selector: 8 | app: nginx 9 | template: 10 | metadata: 11 | name: nginx 12 | labels: 13 | app: nginx 14 | spec: 15 | containers: 16 | - name: nginx 17 | image: nginx 18 | ports: 19 | - containerPort: 80 20 | -------------------------------------------------------------------------------- /fixtures/valid_version.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v1 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: my-app-hpa 5 | spec: 6 | minReplicas: 100 7 | maxReplicas: 300 8 | scaleTargetRef: 9 | apiVersion: extensions/v1beta1 10 | kind: Deployment 11 | name: my-app 12 | targetCPUUtilizationPercentage: 15 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/instrumenta/kubeval 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/fatih/color v1.10.0 7 | github.com/hashicorp/go-multierror v1.1.1 8 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 9 | github.com/spf13/cobra v0.0.0-20180820174524-ff0d02e85550 10 | github.com/spf13/viper v1.7.1 11 | github.com/stretchr/testify v1.7.0 12 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 13 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 14 | github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609 15 | sigs.k8s.io/yaml v1.2.0 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 15 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 16 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 17 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 18 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 19 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 20 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 21 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 22 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 23 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 24 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 25 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 26 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 27 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 28 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 29 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 30 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 31 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 32 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 33 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 34 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 36 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 38 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 39 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 40 | github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= 41 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 42 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 43 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 44 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 45 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 46 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 47 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 48 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 49 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 50 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 51 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 52 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 53 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 54 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 55 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 56 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 57 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 58 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 59 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 60 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 61 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 62 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 63 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 64 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 65 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 66 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 67 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 68 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 69 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 70 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 71 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 72 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 73 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 74 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 75 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 76 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 77 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 78 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 79 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 80 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 81 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 82 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 83 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 84 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 85 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 86 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 87 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 88 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 89 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 90 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 91 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 92 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 93 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 94 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 95 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 96 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 97 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 98 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 99 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 100 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 101 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 102 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 103 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 104 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 105 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 106 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 107 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 108 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 109 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 110 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 111 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 112 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 113 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 114 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 115 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 116 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 117 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 118 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 119 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 120 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 121 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 122 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 123 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 124 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 125 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 126 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 127 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 128 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 129 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 130 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 131 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 132 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 133 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 134 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 135 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 136 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 137 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 138 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 139 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 140 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 141 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 142 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 143 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 144 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 145 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 146 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 147 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 148 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 149 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 150 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 151 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 152 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 153 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 154 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 155 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 156 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 157 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 158 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 159 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 160 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 161 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 162 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 163 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 164 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 165 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 166 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 167 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 168 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 169 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 170 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 171 | github.com/spf13/cobra v0.0.0-20180820174524-ff0d02e85550 h1:LB9SHuuXO8gnsHtexOQSpsJrrAHYA35lvHUaE74kznU= 172 | github.com/spf13/cobra v0.0.0-20180820174524-ff0d02e85550/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 173 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 174 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 175 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 176 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 177 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= 178 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 179 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 180 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 181 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 182 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 183 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 184 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 185 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 186 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 187 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 188 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 189 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 190 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 191 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 192 | github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609 h1:BcMExZAULPkihVZ7UJXK7t8rwGqisXFw75tILnafhBY= 193 | github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= 194 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 195 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 196 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 197 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 198 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 199 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 200 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 201 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 202 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 203 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 204 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 205 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 206 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 207 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 208 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 209 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 210 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 211 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 212 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 213 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 214 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 215 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 216 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 217 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 218 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 219 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 220 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 221 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 222 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 223 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 224 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 225 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 226 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 227 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 228 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 229 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 230 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 231 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 232 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 233 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 234 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 235 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 236 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 237 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 238 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 239 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 240 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 241 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 242 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 243 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 244 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 245 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 246 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 247 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 248 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 249 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 250 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 251 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 252 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 253 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 255 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 257 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 258 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 259 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 260 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= 261 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 262 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 263 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 264 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 265 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 266 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 267 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 268 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 269 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 270 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 271 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 272 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 273 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 274 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 275 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 276 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 277 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 278 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 279 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 280 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 281 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 282 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 283 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 284 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 285 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 286 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 287 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 288 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 289 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 290 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 291 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 292 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 293 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 294 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 295 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 296 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 297 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 298 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 299 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 300 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 301 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 302 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 303 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 304 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 305 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 306 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 307 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 308 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 309 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 310 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 311 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 312 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 313 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 314 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 315 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 316 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 317 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 318 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 319 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 320 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 321 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 322 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 323 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 324 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 325 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 326 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 327 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 328 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 329 | -------------------------------------------------------------------------------- /kubeval/config.go: -------------------------------------------------------------------------------- 1 | package kubeval 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // DefaultSchemaLocation is the default location to search for schemas 10 | const DefaultSchemaLocation = "https://kubernetesjsonschema.dev" 11 | 12 | // OpenShiftSchemaLocation is the alternative location for OpenShift specific schemas 13 | const OpenShiftSchemaLocation = "https://raw.githubusercontent.com/garethr/openshift-json-schema/master" 14 | 15 | // A Config object contains various configuration data for kubeval 16 | type Config struct { 17 | // DefaultNamespace is the namespace to assume in resources 18 | // if no namespace is set in `metadata:namespace` (as used with 19 | // `kubectl apply --namespace ...` or `helm install --namespace ...`, 20 | // for example) 21 | DefaultNamespace string 22 | 23 | // KubernetesVersion represents the version of Kubernetes 24 | // for which we should load the schema 25 | KubernetesVersion string 26 | 27 | // SchemaLocation is the base URL from which to search for schemas. 28 | // It can be either a remote location or a local directory 29 | SchemaLocation string 30 | 31 | // AdditionalSchemaLocations is a list of alternative base URLs from 32 | // which to search for schemas, given that the desired schema was not 33 | // found at SchemaLocation 34 | AdditionalSchemaLocations []string 35 | 36 | // OpenShift represents whether to test against 37 | // upstream Kubernetes or the OpenShift schemas 38 | OpenShift bool 39 | 40 | // Strict tells kubeval whether to prohibit properties not in 41 | // the schema. The API allows them, but kubectl does not 42 | Strict bool 43 | 44 | // IgnoreMissingSchemas tells kubeval whether to skip validation 45 | // for resource definitions without an available schema 46 | IgnoreMissingSchemas bool 47 | 48 | // ExitOnError tells kubeval whether to halt processing upon the 49 | // first error encountered or to continue, aggregating all errors 50 | ExitOnError bool 51 | 52 | // KindsToSkip is a list of kubernetes resources types with which to skip 53 | // schema validation 54 | KindsToSkip []string 55 | 56 | // KindsToReject is a list of case-sensitive prohibited kubernetes resources types 57 | KindsToReject []string 58 | 59 | // FileName is the name to be displayed when testing manifests read from stdin 60 | FileName string 61 | 62 | // OutputFormat is the name of the output formatter which will be used when 63 | // reporting results to the user. 64 | OutputFormat string 65 | 66 | // Quiet indicates whether non-results output should be emitted to the applications 67 | // log. 68 | Quiet bool 69 | 70 | // InsecureSkipTLSVerify controls whether to skip TLS certificate validation 71 | // when retrieving schema content over HTTPS 72 | InsecureSkipTLSVerify bool 73 | } 74 | 75 | // NewDefaultConfig creates a Config with default values 76 | func NewDefaultConfig() *Config { 77 | return &Config{ 78 | DefaultNamespace: "default", 79 | FileName: "stdin", 80 | KubernetesVersion: "master", 81 | } 82 | } 83 | 84 | // AddKubevalFlags adds the default flags for kubeval to cmd 85 | func AddKubevalFlags(cmd *cobra.Command, config *Config) *cobra.Command { 86 | cmd.Flags().StringVarP(&config.DefaultNamespace, "default-namespace", "n", "default", "Namespace to assume in resources if no namespace is set in metadata:namespace") 87 | cmd.Flags().BoolVar(&config.ExitOnError, "exit-on-error", false, "Immediately stop execution when the first error is encountered") 88 | cmd.Flags().BoolVar(&config.IgnoreMissingSchemas, "ignore-missing-schemas", false, "Skip validation for resource definitions without a schema") 89 | cmd.Flags().BoolVar(&config.OpenShift, "openshift", false, "Use OpenShift schemas instead of upstream Kubernetes") 90 | cmd.Flags().BoolVar(&config.Strict, "strict", false, "Disallow additional properties not in schema") 91 | cmd.Flags().StringVarP(&config.FileName, "filename", "f", "stdin", "filename to be displayed when testing manifests read from stdin") 92 | cmd.Flags().StringSliceVar(&config.KindsToSkip, "skip-kinds", []string{}, "Comma-separated list of case-sensitive kinds to skip when validating against schemas") 93 | cmd.Flags().StringSliceVar(&config.KindsToReject, "reject-kinds", []string{}, "Comma-separated list of case-sensitive kinds to prohibit validating against schemas") 94 | cmd.Flags().StringVarP(&config.SchemaLocation, "schema-location", "s", "", "Base URL used to download schemas. Can also be specified with the environment variable KUBEVAL_SCHEMA_LOCATION.") 95 | cmd.Flags().StringSliceVar(&config.AdditionalSchemaLocations, "additional-schema-locations", []string{}, "Comma-seperated list of secondary base URLs used to download schemas") 96 | cmd.Flags().StringVarP(&config.KubernetesVersion, "kubernetes-version", "v", "master", "Version of Kubernetes to validate against") 97 | cmd.Flags().StringVarP(&config.OutputFormat, "output", "o", "", fmt.Sprintf("The format of the output of this script. Options are: %v", validOutputs())) 98 | cmd.Flags().BoolVar(&config.Quiet, "quiet", false, "Silences any output aside from the direct results") 99 | cmd.Flags().BoolVar(&config.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure") 100 | 101 | return cmd 102 | } 103 | -------------------------------------------------------------------------------- /kubeval/kubeval.go: -------------------------------------------------------------------------------- 1 | package kubeval 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/hashicorp/go-multierror" 11 | "github.com/xeipuuv/gojsonschema" 12 | "sigs.k8s.io/yaml" 13 | ) 14 | 15 | // ValidFormat is a type for quickly forcing 16 | // new formats on the gojsonschema loader 17 | type ValidFormat struct{} 18 | 19 | // IsFormat always returns true and meets the 20 | // gojsonschema.FormatChecker interface 21 | func (f ValidFormat) IsFormat(input interface{}) bool { 22 | return true 23 | } 24 | 25 | // ValidationResult contains the details from 26 | // validating a given Kubernetes resource 27 | type ValidationResult struct { 28 | FileName string 29 | Kind string 30 | APIVersion string 31 | ValidatedAgainstSchema bool 32 | Errors []gojsonschema.ResultError 33 | ResourceName string 34 | ResourceNamespace string 35 | } 36 | 37 | // VersionKind returns a string representation of this result's apiVersion and kind 38 | func (v *ValidationResult) VersionKind() string { 39 | return v.APIVersion + "/" + v.Kind 40 | } 41 | 42 | // QualifiedName returns a string of the [namespace.]name of the k8s resource 43 | func (v *ValidationResult) QualifiedName() string { 44 | if v.ResourceName == "" { 45 | return "unknown" 46 | } else if v.ResourceNamespace == "" { 47 | return v.ResourceName 48 | } else { 49 | return fmt.Sprintf("%s.%s", v.ResourceNamespace, v.ResourceName) 50 | } 51 | } 52 | 53 | func determineSchemaURL(baseURL, kind, apiVersion string, config *Config) string { 54 | // We have both the upstream Kubernetes schemas and the OpenShift schemas available 55 | // the tool can toggle between then using the config.OpenShift boolean flag and here we 56 | // use that to format the URL to match the required specification. 57 | 58 | // Most of the directories which store the schemas are prefixed with a v so as to 59 | // match the tagging in the Kubernetes repository, apart from master. 60 | normalisedVersion := config.KubernetesVersion 61 | if normalisedVersion != "master" { 62 | normalisedVersion = "v" + normalisedVersion 63 | } 64 | 65 | strictSuffix := "" 66 | if config.Strict { 67 | strictSuffix = "-strict" 68 | } 69 | 70 | if config.OpenShift { 71 | // If we're using the openshift schemas, there's no further processing required 72 | return fmt.Sprintf("%s/%s-standalone%s/%s.json", baseURL, normalisedVersion, strictSuffix, strings.ToLower(kind)) 73 | } 74 | 75 | groupParts := strings.Split(apiVersion, "/") 76 | versionParts := strings.Split(groupParts[0], ".") 77 | 78 | kindSuffix := "-" + strings.ToLower(versionParts[0]) 79 | if len(groupParts) > 1 { 80 | kindSuffix += "-" + strings.ToLower(groupParts[1]) 81 | } 82 | 83 | return fmt.Sprintf("%s/%s-standalone%s/%s%s.json", baseURL, normalisedVersion, strictSuffix, strings.ToLower(kind), kindSuffix) 84 | } 85 | 86 | func determineSchemaBaseURL(config *Config) string { 87 | // Order of precendence: 88 | // 1. If --openshift is passed, return the openshift schema location 89 | // 2. If a --schema-location is passed, use it 90 | // 3. If the KUBEVAL_SCHEMA_LOCATION is set, use it 91 | // 4. Otherwise, use the DefaultSchemaLocation 92 | 93 | if config.OpenShift { 94 | return OpenShiftSchemaLocation 95 | } 96 | 97 | if config.SchemaLocation != "" { 98 | return config.SchemaLocation 99 | } 100 | 101 | // We only care that baseURL has a value after this call, so we can 102 | // ignore LookupEnv's second return value 103 | baseURL, _ := os.LookupEnv("KUBEVAL_SCHEMA_LOCATION") 104 | if baseURL != "" { 105 | return baseURL 106 | } 107 | 108 | return DefaultSchemaLocation 109 | } 110 | 111 | // validateResource validates a single Kubernetes resource against 112 | // the relevant schema, detecting the type of resource automatically. 113 | // Returns the result and raw YAML body as map. 114 | func validateResource(data []byte, schemaCache map[string]*gojsonschema.Schema, config *Config) (ValidationResult, map[string]interface{}, error) { 115 | result := ValidationResult{} 116 | result.FileName = config.FileName 117 | var body map[string]interface{} 118 | err := yaml.Unmarshal(data, &body) 119 | if err != nil { 120 | return result, body, fmt.Errorf("Failed to decode YAML from %s: %s", result.FileName, err.Error()) 121 | } else if body == nil { 122 | return result, body, nil 123 | } 124 | 125 | metadata, _ := getObject(body, "metadata") 126 | if metadata != nil { 127 | namespace, _ := getString(metadata, "namespace") 128 | name, _ := getString(metadata, "name") 129 | generateName, _ := getString(metadata, "generateName") 130 | 131 | if len(name) == 0 && len(generateName) > 0 { 132 | result.ResourceName = fmt.Sprintf("%s{{ generateName }}", generateName) 133 | } else { 134 | result.ResourceName = name 135 | } 136 | result.ResourceNamespace = namespace 137 | } 138 | 139 | kind, err := getString(body, "kind") 140 | if err != nil { 141 | return result, body, fmt.Errorf("%s: %s", result.FileName, err.Error()) 142 | } 143 | result.Kind = kind 144 | 145 | apiVersion, err := getString(body, "apiVersion") 146 | if err != nil { 147 | return result, body, fmt.Errorf("%s: %s", result.FileName, err.Error()) 148 | } 149 | result.APIVersion = apiVersion 150 | 151 | if in(config.KindsToSkip, kind) { 152 | return result, body, nil 153 | } 154 | 155 | if in(config.KindsToReject, kind) { 156 | return result, body, fmt.Errorf("Prohibited resource kind '%s' in %s", kind, result.FileName) 157 | } 158 | 159 | schemaErrors, err := validateAgainstSchema(body, &result, schemaCache, config) 160 | if err != nil { 161 | return result, body, fmt.Errorf("%s: %s", result.FileName, err.Error()) 162 | } 163 | result.Errors = schemaErrors 164 | return result, body, nil 165 | } 166 | 167 | func validateAgainstSchema(body interface{}, resource *ValidationResult, schemaCache map[string]*gojsonschema.Schema, config *Config) ([]gojsonschema.ResultError, error) { 168 | 169 | schema, err := downloadSchema(resource, schemaCache, config) 170 | if err != nil || schema == nil { 171 | return handleMissingSchema(err, config) 172 | } 173 | 174 | // Without forcing these types the schema fails to load 175 | // Need to Work out proper handling for these types 176 | gojsonschema.FormatCheckers.Add("int64", ValidFormat{}) 177 | gojsonschema.FormatCheckers.Add("byte", ValidFormat{}) 178 | gojsonschema.FormatCheckers.Add("int32", ValidFormat{}) 179 | gojsonschema.FormatCheckers.Add("int-or-string", ValidFormat{}) 180 | 181 | documentLoader := gojsonschema.NewGoLoader(body) 182 | results, err := schema.Validate(documentLoader) 183 | if err != nil { 184 | // This error can only happen if the Object to validate is poorly formed. There's no hope of saving this one 185 | wrappedErr := fmt.Errorf("Problem validating schema. Check JSON formatting: %s", err) 186 | return []gojsonschema.ResultError{}, wrappedErr 187 | } 188 | resource.ValidatedAgainstSchema = true 189 | if !results.Valid() { 190 | return results.Errors(), nil 191 | } 192 | 193 | return []gojsonschema.ResultError{}, nil 194 | } 195 | 196 | // returned schema may be nil scehma is missing and missing schemas are allowed 197 | func downloadSchema(resource *ValidationResult, schemaCache map[string]*gojsonschema.Schema, config *Config) (*gojsonschema.Schema, error) { 198 | if schema, ok := schemaCache[resource.VersionKind()]; ok { 199 | // If the schema was previously cached, there's no work to be done 200 | return schema, nil 201 | } 202 | 203 | // We haven't cached this schema yet; look for one that works 204 | primarySchemaBaseURL := determineSchemaBaseURL(config) 205 | primarySchemaRef := determineSchemaURL(primarySchemaBaseURL, resource.Kind, resource.APIVersion, config) 206 | schemaRefs := []string{primarySchemaRef} 207 | 208 | for _, additionalSchemaURLs := range config.AdditionalSchemaLocations { 209 | additionalSchemaRef := determineSchemaURL(additionalSchemaURLs, resource.Kind, resource.APIVersion, config) 210 | schemaRefs = append(schemaRefs, additionalSchemaRef) 211 | } 212 | 213 | var errors *multierror.Error 214 | 215 | for _, schemaRef := range schemaRefs { 216 | schemaLoader := gojsonschema.NewReferenceLoader(schemaRef) 217 | schema, err := gojsonschema.NewSchema(schemaLoader) 218 | if err == nil { 219 | // success! cache this and stop looking 220 | schemaCache[resource.VersionKind()] = schema 221 | return schema, nil 222 | } 223 | // We couldn't find a schema for this URL, so take a note, then try the next URL 224 | wrappedErr := fmt.Errorf("Failed initializing schema %s: %s", schemaRef, err) 225 | errors = multierror.Append(errors, wrappedErr) 226 | } 227 | 228 | if errors != nil { 229 | errors.ErrorFormat = singleLineErrorFormat 230 | } 231 | 232 | // We couldn't find a schema for this resource. Cache its lack of existence 233 | schemaCache[resource.VersionKind()] = nil 234 | return nil, errors.ErrorOrNil() 235 | } 236 | 237 | func handleMissingSchema(err error, config *Config) ([]gojsonschema.ResultError, error) { 238 | if config.IgnoreMissingSchemas { 239 | return []gojsonschema.ResultError{}, nil 240 | } 241 | return []gojsonschema.ResultError{}, err 242 | } 243 | 244 | // NewSchemaCache returns a new schema cache to be used with 245 | // ValidateWithCache 246 | func NewSchemaCache() map[string]*gojsonschema.Schema { 247 | return make(map[string]*gojsonschema.Schema, 0) 248 | } 249 | 250 | // Validate a Kubernetes YAML file, parsing out individual resources 251 | // and validating them all according to the relevant schemas 252 | func Validate(input []byte, conf ...*Config) ([]ValidationResult, error) { 253 | schemaCache := NewSchemaCache() 254 | return ValidateWithCache(input, schemaCache, conf...) 255 | } 256 | 257 | // ValidateWithCache validates a Kubernetes YAML file, parsing out individual resources 258 | // and validating them all according to the relevant schemas 259 | // Allows passing a kubeval.NewSchemaCache() to cache schemas in-memory 260 | // between validations 261 | func ValidateWithCache(input []byte, schemaCache map[string]*gojsonschema.Schema, conf ...*Config) ([]ValidationResult, error) { 262 | config := NewDefaultConfig() 263 | if len(conf) == 1 { 264 | config = conf[0] 265 | } 266 | 267 | results := make([]ValidationResult, 0) 268 | 269 | if len(config.DefaultNamespace) == 0 { 270 | return results, fmt.Errorf("Default namespace ('-n/--default-namespace' flag) must not be empty") 271 | } 272 | 273 | if len(input) == 0 { 274 | result := ValidationResult{} 275 | result.FileName = config.FileName 276 | results = append(results, result) 277 | return results, nil 278 | } 279 | 280 | splitBits := bytes.Split(input, []byte(detectLineBreak(input)+"---"+detectLineBreak(input))) 281 | bits := make([][]byte, len(splitBits)) 282 | j := 0 283 | 284 | // split any list into its elements and add them to "bits" 285 | for _, element := range splitBits { 286 | 287 | list := struct { 288 | Version string 289 | Kind string 290 | Items []interface{} 291 | }{} 292 | 293 | unmarshalErr := yaml.Unmarshal(element, &list) 294 | isYamlList := unmarshalErr == nil && list.Items != nil 295 | 296 | if isYamlList { 297 | listBits := make([][]byte, len(list.Items)) 298 | for i, item := range list.Items { 299 | b, _ := yaml.Marshal(item) 300 | listBits[i] = b 301 | } 302 | bits = append(bits, listBits...) 303 | j += len(list.Items) 304 | } else { 305 | bits[j] = element 306 | j++ 307 | } 308 | } 309 | 310 | var errors *multierror.Error 311 | 312 | // special case regexp for helm 313 | helmSourcePattern := regexp.MustCompile(`^(?:---` + detectLineBreak(input) + `)?# Source: (.*)`) 314 | 315 | // Save the fileName we were provided; if we detect a new fileName 316 | // we'll use that, but we'll need to revert to the default afterward 317 | originalFileName := config.FileName 318 | defer func() { 319 | // revert the filename back to the original 320 | config.FileName = originalFileName 321 | }() 322 | 323 | seenResourcesSet := make(map[[4]string]bool) // set of [API version, kind, namespace, name] 324 | 325 | for _, element := range bits { 326 | if len(element) > 0 { 327 | if found := helmSourcePattern.FindStringSubmatch(string(element)); found != nil { 328 | config.FileName = found[1] 329 | } 330 | 331 | result, body, err := validateResource(element, schemaCache, config) 332 | if err != nil { 333 | errors = multierror.Append(errors, err) 334 | if config.ExitOnError { 335 | return results, errors 336 | } 337 | } else { 338 | if !in(config.KindsToSkip, result.Kind) { 339 | 340 | metadata, _ := getObject(body, "metadata") 341 | if metadata != nil { 342 | namespace, _ := getString(metadata, "namespace") 343 | name, _ := getString(metadata, "name") 344 | 345 | var resolvedNamespace string 346 | if len(namespace) > 0 { 347 | resolvedNamespace = namespace 348 | } else { 349 | resolvedNamespace = config.DefaultNamespace 350 | } 351 | 352 | // If resource has `metadata:name` attribute 353 | if len(resolvedNamespace) > 0 && len(name) > 0 { 354 | key := [4]string{result.APIVersion, result.Kind, resolvedNamespace, name} 355 | if _, hasDuplicate := seenResourcesSet[key]; hasDuplicate { 356 | errors = multierror.Append(errors, fmt.Errorf("%s: Duplicate '%s' resource '%s' in namespace '%s'", result.FileName, result.Kind, name, namespace)) 357 | } 358 | 359 | seenResourcesSet[key] = true 360 | } 361 | } 362 | } 363 | } 364 | results = append(results, result) 365 | } else { 366 | result := ValidationResult{} 367 | result.FileName = config.FileName 368 | results = append(results, result) 369 | } 370 | } 371 | 372 | if errors != nil { 373 | errors.ErrorFormat = singleLineErrorFormat 374 | } 375 | return results, errors.ErrorOrNil() 376 | } 377 | 378 | func singleLineErrorFormat(es []error) string { 379 | messages := make([]string, len(es)) 380 | for i, e := range es { 381 | messages[i] = e.Error() 382 | } 383 | return strings.Join(messages, "\n") 384 | } 385 | -------------------------------------------------------------------------------- /kubeval/kubeval_test.go: -------------------------------------------------------------------------------- 1 | package kubeval 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | multierror "github.com/hashicorp/go-multierror" 10 | "github.com/spf13/cobra" 11 | "github.com/xeipuuv/gojsonschema" 12 | ) 13 | 14 | var validTestFiles = []string{ 15 | "blank.yaml", 16 | "comment.yaml", 17 | "valid.yaml", 18 | "valid.json", 19 | "generate_name.yaml", 20 | "multi_valid.yaml", 21 | "int_or_string.yaml", 22 | "null_array.yaml", 23 | "quantity.yaml", 24 | "extra_property.yaml", 25 | "full_domain_group.yaml", 26 | "unconventional_keys.yaml", 27 | "list_valid.yaml", 28 | "list_empty_valid.yaml", 29 | "same-object-different-namespace.yaml", 30 | "same-object-different-namespace-default.yaml", 31 | "duplicates-skipped-kinds.yaml", 32 | "same-kind-different-api.yaml", 33 | } 34 | 35 | func TestValidateBlankInput(t *testing.T) { 36 | blank := []byte("") 37 | config := NewDefaultConfig() 38 | config.FileName = "blank" 39 | _, err := Validate(blank, config) 40 | if err != nil { 41 | t.Errorf("Validate should pass when passed a blank string") 42 | } 43 | } 44 | 45 | func TestValidateValidInputs(t *testing.T) { 46 | var tests = validTestFiles 47 | for _, test := range tests { 48 | filePath, _ := filepath.Abs("../fixtures/" + test) 49 | fileContents, _ := ioutil.ReadFile(filePath) 50 | config := NewDefaultConfig() 51 | config.DefaultNamespace = "the-default-namespace" 52 | config.FileName = test 53 | config.KindsToSkip = []string{"SkipThisKind"} 54 | _, err := Validate(fileContents, config) 55 | if err != nil { 56 | t.Errorf("Validate should pass when testing valid configuration in %s, got errors: %v", test, err) 57 | } 58 | } 59 | } 60 | 61 | func TestValidateValidInputsWithCache(t *testing.T) { 62 | var tests = validTestFiles 63 | schemaCache := make(map[string]*gojsonschema.Schema, 0) 64 | 65 | for _, test := range tests { 66 | filePath, _ := filepath.Abs("../fixtures/" + test) 67 | fileContents, _ := ioutil.ReadFile(filePath) 68 | config := NewDefaultConfig() 69 | config.DefaultNamespace = "the-default-namespace" 70 | config.FileName = test 71 | config.KindsToSkip = []string{"SkipThisKind"} 72 | _, err := ValidateWithCache(fileContents, schemaCache, config) 73 | if err != nil { 74 | t.Errorf("Validate should pass when testing valid configuration in " + test) 75 | } 76 | } 77 | } 78 | 79 | func TestValidateInvalidInputs(t *testing.T) { 80 | var tests = []string{ 81 | "missing_kind.yaml", 82 | "missing_kind_value.yaml", 83 | "duplicates.yaml", 84 | "duplicates-non-namespaced.yaml", 85 | "duplicates-with-namespace.yaml", 86 | } 87 | for _, test := range tests { 88 | filePath, _ := filepath.Abs("../fixtures/" + test) 89 | fileContents, _ := ioutil.ReadFile(filePath) 90 | config := NewDefaultConfig() 91 | config.DefaultNamespace = "the-default-namespace" 92 | config.FileName = test 93 | _, err := Validate(fileContents, config) 94 | if err == nil { 95 | t.Errorf("Validate should not pass when testing invalid configuration in " + test) 96 | } 97 | } 98 | } 99 | 100 | func TestValidateSourceExtraction(t *testing.T) { 101 | expectedFileNames := []string{ 102 | "chart/templates/primary.yaml", // first from primary template 103 | "chart/templates/primary.yaml", // second resource from primary template 104 | "chart/templates/secondary.yaml", // first resource from secondary template 105 | "chart/templates/secondary.yaml", // second resource from secondary template 106 | "chart/templates/frontend.yaml", // first resource from frontend template 107 | "chart/templates/frontend.yaml", // second resource from frontend template 108 | "chart/templates/frontend.yaml", // empty resource no comment 109 | "chart/templates/frontend.yaml", // empty resource with comment 110 | } 111 | filePath, _ := filepath.Abs("../fixtures/multi_valid_source.yaml") 112 | fileContents, _ := ioutil.ReadFile(filePath) 113 | config := NewDefaultConfig() 114 | config.FileName = "multi_valid_source.yaml" 115 | results, err := Validate(fileContents, config) 116 | if err != nil { 117 | t.Fatalf("Unexpected error while validating source: %v", err) 118 | } 119 | for i, r := range results { 120 | if r.FileName != expectedFileNames[i] { 121 | t.Errorf("%v: expected filename [%v], got [%v]", i, expectedFileNames[i], r.FileName) 122 | } 123 | } 124 | } 125 | 126 | func TestStrictCatchesAdditionalErrors(t *testing.T) { 127 | config := NewDefaultConfig() 128 | config.Strict = true 129 | config.FileName = "extra_property.yaml" 130 | filePath, _ := filepath.Abs("../fixtures/extra_property.yaml") 131 | fileContents, _ := ioutil.ReadFile(filePath) 132 | results, _ := Validate(fileContents, config) 133 | if len(results[0].Errors) == 0 { 134 | t.Errorf("Validate should not pass when testing for additional properties not in schema") 135 | } 136 | } 137 | 138 | func TestValidateMultipleVersions(t *testing.T) { 139 | config := NewDefaultConfig() 140 | config.Strict = true 141 | config.FileName = "valid_version.yaml" 142 | config.KubernetesVersion = "1.14.0" 143 | filePath, _ := filepath.Abs("../fixtures/valid_version.yaml") 144 | fileContents, _ := ioutil.ReadFile(filePath) 145 | results, err := Validate(fileContents, config) 146 | if err != nil || len(results[0].Errors) > 0 { 147 | t.Errorf("Validate should pass when testing valid configuration with multiple versions: %v", err) 148 | } 149 | } 150 | 151 | func TestValidateInputsWithErrors(t *testing.T) { 152 | var tests = []string{ 153 | "invalid.yaml", 154 | "multi_invalid.yaml", 155 | "list_invalid.yaml", 156 | } 157 | for _, test := range tests { 158 | filePath, _ := filepath.Abs("../fixtures/" + test) 159 | fileContents, _ := ioutil.ReadFile(filePath) 160 | config := NewDefaultConfig() 161 | config.FileName = test 162 | results, _ := Validate(fileContents, config) 163 | errorCount := 0 164 | for _, result := range results { 165 | errorCount += len(result.Errors) 166 | } 167 | if errorCount == 0 { 168 | t.Errorf("Validate should not pass when testing invalid configuration in " + test) 169 | } 170 | } 171 | } 172 | 173 | func TestValidateMultipleResourcesWithErrors(t *testing.T) { 174 | var tests = []string{ 175 | "multi_invalid_resources.yaml", 176 | } 177 | for _, test := range tests { 178 | config := NewDefaultConfig() 179 | filePath, _ := filepath.Abs("../fixtures/" + test) 180 | fileContents, _ := ioutil.ReadFile(filePath) 181 | config.ExitOnError = true 182 | config.FileName = test 183 | _, err := Validate(fileContents, config) 184 | if err == nil { 185 | t.Errorf("Validate should not pass when testing invalid configuration in " + test) 186 | } else if merr, ok := err.(*multierror.Error); ok { 187 | if len(merr.Errors) != 1 { 188 | t.Errorf("Validate should encounter exactly 1 error when testing invalid configuration in " + test + " with ExitOnError=true") 189 | } 190 | } 191 | config.ExitOnError = false 192 | _, err = Validate(fileContents, config) 193 | if err == nil { 194 | t.Errorf("Validate should not pass when testing invalid configuration in " + test) 195 | } else if merr, ok := err.(*multierror.Error); ok { 196 | if len(merr.Errors) != 5 { 197 | t.Errorf("Validate should encounter exactly 5 errors when testing invalid configuration in " + test) 198 | } 199 | } else if !ok { 200 | t.Errorf("Validate should encounter exactly 5 errors when testing invalid configuration in " + test) 201 | } 202 | } 203 | } 204 | 205 | func TestValidateKindsToReject(t *testing.T) { 206 | var tests = []struct { 207 | Name string 208 | KindsToReject []string 209 | Fixture string 210 | Pass bool 211 | }{ 212 | { 213 | Name: "allow_all", 214 | KindsToReject: []string{}, 215 | Fixture: "valid.yaml", 216 | Pass: true, 217 | }, 218 | { 219 | Name: "reject_one", 220 | KindsToReject: []string{"ReplicationController"}, 221 | Fixture: "valid.yaml", 222 | Pass: false, 223 | }, 224 | } 225 | schemaCache := make(map[string]*gojsonschema.Schema, 0) 226 | 227 | for _, test := range tests { 228 | filePath, _ := filepath.Abs("../fixtures/" + test.Fixture) 229 | fileContents, _ := ioutil.ReadFile(filePath) 230 | config := NewDefaultConfig() 231 | config.FileName = test.Fixture 232 | config.KindsToReject = test.KindsToReject 233 | _, err := ValidateWithCache(fileContents, schemaCache, config) 234 | if err != nil && test.Pass == true { 235 | t.Errorf("Validate should pass when testing valid configuration in " + test.Name) 236 | } 237 | } 238 | } 239 | 240 | func TestDetermineSchemaURL(t *testing.T) { 241 | var tests = []struct { 242 | config *Config 243 | baseURL string 244 | kind string 245 | version string 246 | expected string 247 | }{ 248 | { 249 | config: NewDefaultConfig(), 250 | baseURL: "https://base", 251 | kind: "sample", 252 | version: "v1", 253 | expected: "https://base/master-standalone/sample-v1.json", 254 | }, 255 | { 256 | config: &Config{KubernetesVersion: "2"}, 257 | baseURL: "https://base", 258 | kind: "sample", 259 | version: "v1", 260 | expected: "https://base/v2-standalone/sample-v1.json", 261 | }, 262 | { 263 | config: &Config{KubernetesVersion: "master", Strict: true}, 264 | baseURL: "https://base", 265 | kind: "sample", 266 | version: "v1", 267 | expected: "https://base/master-standalone-strict/sample-v1.json", 268 | }, 269 | { 270 | config: NewDefaultConfig(), 271 | baseURL: "https://base", 272 | kind: "sample", 273 | version: "extensions/v1beta1", 274 | expected: "https://base/master-standalone/sample-extensions-v1beta1.json", 275 | }, 276 | { 277 | config: &Config{KubernetesVersion: "master", OpenShift: true}, 278 | baseURL: "https://base", 279 | kind: "sample", 280 | version: "v1", 281 | expected: "https://base/master-standalone/sample.json", 282 | }, 283 | } 284 | for _, test := range tests { 285 | schemaURL := determineSchemaURL(test.baseURL, test.kind, test.version, test.config) 286 | if schemaURL != test.expected { 287 | t.Errorf("Schema URL should be %s, got %s", test.expected, schemaURL) 288 | } 289 | } 290 | } 291 | 292 | func TestDetermineSchemaForSchemaLocation(t *testing.T) { 293 | oldVal, found := os.LookupEnv("KUBEVAL_SCHEMA_LOCATION") 294 | defer func() { 295 | if found { 296 | os.Setenv("KUBEVAL_SCHEMA_LOCATION", oldVal) 297 | } else { 298 | os.Unsetenv("KUBEVAL_SCHEMA_LOCATION") 299 | } 300 | }() 301 | 302 | var tests = []struct { 303 | config *Config 304 | envVar string 305 | expected string 306 | }{ 307 | { 308 | config: &Config{OpenShift: true}, 309 | envVar: "", 310 | expected: OpenShiftSchemaLocation, 311 | }, 312 | { 313 | config: &Config{SchemaLocation: "https://base"}, 314 | envVar: "", 315 | expected: "https://base", 316 | }, 317 | { 318 | config: &Config{}, 319 | envVar: "https://base", 320 | expected: "https://base", 321 | }, 322 | { 323 | config: &Config{}, 324 | envVar: "", 325 | expected: DefaultSchemaLocation, 326 | }, 327 | } 328 | for i, test := range tests { 329 | os.Setenv("KUBEVAL_SCHEMA_LOCATION", test.envVar) 330 | schemaBaseURL := determineSchemaBaseURL(test.config) 331 | if schemaBaseURL != test.expected { 332 | t.Errorf("test #%d: Schema Base URL should be %s, got %s", i, test.expected, schemaBaseURL) 333 | } 334 | } 335 | } 336 | 337 | func TestGetString(t *testing.T) { 338 | var tests = []struct { 339 | body map[string]interface{} 340 | key string 341 | expectedVal string 342 | expectError bool 343 | }{ 344 | { 345 | body: map[string]interface{}{"goodKey": "goodVal"}, 346 | key: "goodKey", 347 | expectedVal: "goodVal", 348 | expectError: false, 349 | }, 350 | { 351 | body: map[string]interface{}{}, 352 | key: "missingKey", 353 | expectedVal: "", 354 | expectError: true, 355 | }, 356 | { 357 | body: map[string]interface{}{"nilKey": nil}, 358 | key: "nilKey", 359 | expectedVal: "", 360 | expectError: true, 361 | }, 362 | { 363 | body: map[string]interface{}{"badKey": 5}, 364 | key: "badKey", 365 | expectedVal: "", 366 | expectError: true, 367 | }, 368 | } 369 | 370 | for _, test := range tests { 371 | actualVal, err := getString(test.body, test.key) 372 | if err != nil { 373 | if !test.expectError { 374 | t.Errorf("Unexpected error: %s", err.Error()) 375 | } 376 | // We expected this error, so move to the next test 377 | continue 378 | } 379 | if test.expectError { 380 | t.Errorf("Expected an error, but didn't receive one") 381 | continue 382 | } 383 | if actualVal != test.expectedVal { 384 | t.Errorf("Expected %s, got %s", test.expectedVal, actualVal) 385 | } 386 | } 387 | } 388 | 389 | func TestSkipCrdSchemaMiss(t *testing.T) { 390 | config := NewDefaultConfig() 391 | config.FileName = "test_crd.yaml" 392 | filePath, _ := filepath.Abs("../fixtures/test_crd.yaml") 393 | fileContents, _ := ioutil.ReadFile(filePath) 394 | _, err := Validate(fileContents) 395 | if err == nil { 396 | t.Errorf("For custom CRD's with schema missing we should error without IgnoreMissingSchemas flag") 397 | } 398 | 399 | config.IgnoreMissingSchemas = true 400 | results, _ := Validate(fileContents, config) 401 | if len(results[0].Errors) != 0 { 402 | t.Errorf("For custom CRD's with schema missing we should skip with IgnoreMissingSchemas flag") 403 | } 404 | 405 | config.IgnoreMissingSchemas = false 406 | config.KindsToSkip = []string{"SealedSecret"} 407 | results, _ = Validate(fileContents, config) 408 | if len(results[0].Errors) != 0 { 409 | t.Errorf("We should skip resources listed in KindsToSkip") 410 | } 411 | } 412 | 413 | func TestAdditionalSchemas(t *testing.T) { 414 | // This test uses a hack - first tell kubeval to use a bogus URL as its 415 | // primary search location, then give the DefaultSchemaLocation as an 416 | // additional schema. 417 | // This should cause kubeval to fail when looking for the schema in the 418 | // primary location, then succeed when it finds the schema at the 419 | // "additional location" 420 | config := NewDefaultConfig() 421 | config.SchemaLocation = "testLocation" 422 | config.AdditionalSchemaLocations = []string{DefaultSchemaLocation} 423 | 424 | config.FileName = "valid.yaml" 425 | filePath, _ := filepath.Abs("../fixtures/valid.yaml") 426 | fileContents, _ := ioutil.ReadFile(filePath) 427 | results, err := Validate(fileContents, config) 428 | if err != nil { 429 | t.Errorf("Unexpected error: %s", err.Error()) 430 | } else if len(results[0].Errors) != 0 { 431 | t.Errorf("Validate should pass when testing a valid configuration using additional schema") 432 | } 433 | } 434 | 435 | func TestFlagAdding(t *testing.T) { 436 | cmd := &cobra.Command{} 437 | config := &Config{} 438 | 439 | AddKubevalFlags(cmd, config) 440 | 441 | expectedFlags := []string{ 442 | "exit-on-error", 443 | "ignore-missing-schemas", 444 | "openshift", 445 | "strict", 446 | "filename", 447 | "skip-kinds", 448 | "schema-location", 449 | "additional-schema-locations", 450 | "kubernetes-version", 451 | } 452 | 453 | for _, expected := range expectedFlags { 454 | flag := cmd.Flags().Lookup(expected) 455 | if flag == nil { 456 | t.Errorf("Could not find flag '%s'", expected) 457 | } 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /kubeval/output.go: -------------------------------------------------------------------------------- 1 | package kubeval 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "os" 9 | 10 | kLog "github.com/instrumenta/kubeval/log" 11 | ) 12 | 13 | // TODO (brendanryan) move these structs to `/log` once we have removed the potential 14 | // circular dependancy between this package and `/log` 15 | 16 | // outputManager controls how results of the `kubeval` evaluation will be recorded 17 | // and reported to the end user. 18 | // This interface is kept private to ensure all implementations are closed within 19 | // this package. 20 | type outputManager interface { 21 | Put(r ValidationResult) error 22 | Flush() error 23 | } 24 | 25 | const ( 26 | outputSTD = "stdout" 27 | outputJSON = "json" 28 | outputTAP = "tap" 29 | ) 30 | 31 | func validOutputs() []string { 32 | return []string{ 33 | outputSTD, 34 | outputJSON, 35 | outputTAP, 36 | } 37 | } 38 | 39 | func GetOutputManager(outFmt string) outputManager { 40 | switch outFmt { 41 | case outputSTD: 42 | return newSTDOutputManager() 43 | case outputJSON: 44 | return newDefaultJSONOutputManager() 45 | case outputTAP: 46 | return newDefaultTAPOutputManager() 47 | default: 48 | return newSTDOutputManager() 49 | } 50 | } 51 | 52 | // STDOutputManager reports `kubeval` results to stdout. 53 | type STDOutputManager struct { 54 | } 55 | 56 | // newSTDOutputManager instantiates a new instance of STDOutputManager. 57 | func newSTDOutputManager() *STDOutputManager { 58 | return &STDOutputManager{} 59 | } 60 | 61 | func (s *STDOutputManager) Put(result ValidationResult) error { 62 | if len(result.Errors) > 0 { 63 | for _, desc := range result.Errors { 64 | kLog.Warn(result.FileName, "contains an invalid", result.Kind, fmt.Sprintf("(%s)", result.QualifiedName()), "-", desc.String()) 65 | } 66 | } else if result.Kind == "" { 67 | kLog.Success(result.FileName, "contains an empty YAML document") 68 | } else if !result.ValidatedAgainstSchema { 69 | kLog.Warn(result.FileName, "containing a", result.Kind, fmt.Sprintf("(%s)", result.QualifiedName()), "was not validated against a schema") 70 | } else { 71 | kLog.Success(result.FileName, "contains a valid", result.Kind, fmt.Sprintf("(%s)", result.QualifiedName())) 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func (s *STDOutputManager) Flush() error { 78 | // no op 79 | return nil 80 | } 81 | 82 | type status string 83 | 84 | const ( 85 | statusInvalid = "invalid" 86 | statusValid = "valid" 87 | statusSkipped = "skipped" 88 | ) 89 | 90 | type dataEvalResult struct { 91 | Filename string `json:"filename"` 92 | Kind string `json:"kind"` 93 | Status status `json:"status"` 94 | Errors []string `json:"errors"` 95 | } 96 | 97 | // jsonOutputManager reports `ccheck` results to `stdout` as a json array.. 98 | type jsonOutputManager struct { 99 | logger *log.Logger 100 | 101 | data []dataEvalResult 102 | } 103 | 104 | func newDefaultJSONOutputManager() *jsonOutputManager { 105 | return newJSONOutputManager(log.New(os.Stdout, "", 0)) 106 | } 107 | 108 | func newJSONOutputManager(l *log.Logger) *jsonOutputManager { 109 | return &jsonOutputManager{ 110 | logger: l, 111 | } 112 | } 113 | 114 | func getStatus(r ValidationResult) status { 115 | if r.Kind == "" { 116 | return statusSkipped 117 | } 118 | 119 | if !r.ValidatedAgainstSchema { 120 | return statusSkipped 121 | } 122 | 123 | if len(r.Errors) > 0 { 124 | return statusInvalid 125 | } 126 | 127 | return statusValid 128 | } 129 | 130 | func (j *jsonOutputManager) Put(r ValidationResult) error { 131 | // stringify gojsonschema errors 132 | // use a pre-allocated slice to ensure the json will have an 133 | // empty array in the "zero" case 134 | errs := make([]string, 0, len(r.Errors)) 135 | for _, e := range r.Errors { 136 | errs = append(errs, e.String()) 137 | } 138 | 139 | j.data = append(j.data, dataEvalResult{ 140 | Filename: r.FileName, 141 | Kind: r.Kind, 142 | Status: getStatus(r), 143 | Errors: errs, 144 | }) 145 | 146 | return nil 147 | } 148 | 149 | func (j *jsonOutputManager) Flush() error { 150 | b, err := json.Marshal(j.data) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | var out bytes.Buffer 156 | err = json.Indent(&out, b, "", "\t") 157 | if err != nil { 158 | return err 159 | } 160 | 161 | j.logger.Print(out.String()) 162 | return nil 163 | } 164 | 165 | // tapOutputManager reports `conftest` results to stdout. 166 | type tapOutputManager struct { 167 | logger *log.Logger 168 | 169 | data []dataEvalResult 170 | } 171 | 172 | // newDefaultTapOutManager instantiates a new instance of tapOutputManager 173 | // using the default logger. 174 | func newDefaultTAPOutputManager() *tapOutputManager { 175 | return newTAPOutputManager(log.New(os.Stdout, "", 0)) 176 | } 177 | 178 | // newTapOutputManager constructs an instance of tapOutputManager given a 179 | // logger instance. 180 | func newTAPOutputManager(l *log.Logger) *tapOutputManager { 181 | return &tapOutputManager{ 182 | logger: l, 183 | } 184 | } 185 | 186 | func (j *tapOutputManager) Put(r ValidationResult) error { 187 | errs := make([]string, 0, len(r.Errors)) 188 | for _, e := range r.Errors { 189 | errs = append(errs, e.String()) 190 | } 191 | 192 | j.data = append(j.data, dataEvalResult{ 193 | Filename: r.FileName, 194 | Kind: r.Kind, 195 | Status: getStatus(r), 196 | Errors: errs, 197 | }) 198 | 199 | return nil 200 | } 201 | 202 | func (j *tapOutputManager) Flush() error { 203 | issues := len(j.data) 204 | if issues > 0 { 205 | total := 0 206 | for _, r := range j.data { 207 | if len(r.Errors) > 0 { 208 | total = total + len(r.Errors) 209 | } else { 210 | total = total + 1 211 | } 212 | } 213 | j.logger.Print(fmt.Sprintf("1..%d", total)) 214 | count := 0 215 | for _, r := range j.data { 216 | count = count + 1 217 | var kindMarker string 218 | if r.Kind == "" { 219 | kindMarker = "" 220 | } else { 221 | kindMarker = fmt.Sprintf(" (%s)", r.Kind) 222 | } 223 | if r.Status == "valid" { 224 | j.logger.Print("ok ", count, " - ", r.Filename, kindMarker) 225 | } else if r.Status == "skipped" { 226 | j.logger.Print("ok ", count, " - ", r.Filename, kindMarker, " # SKIP") 227 | } else if r.Status == "invalid" { 228 | for i, e := range r.Errors { 229 | j.logger.Print("not ok ", count, " - ", r.Filename, kindMarker, " - ", e) 230 | 231 | // We have to skip adding 1 if it's the last error 232 | if len(r.Errors) != i+1 { 233 | count = count + 1 234 | } 235 | } 236 | } 237 | } 238 | } 239 | return nil 240 | } 241 | -------------------------------------------------------------------------------- /kubeval/output_test.go: -------------------------------------------------------------------------------- 1 | package kubeval 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "testing" 7 | 8 | "github.com/xeipuuv/gojsonschema" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func newResultError(msg string) gojsonschema.ResultError { 14 | r := &gojsonschema.ResultErrorFields{} 15 | 16 | r.SetContext(gojsonschema.NewJsonContext("error", nil)) 17 | r.SetDescription(msg) 18 | 19 | return r 20 | } 21 | 22 | func newResultErrors(msgs []string) []gojsonschema.ResultError { 23 | var res []gojsonschema.ResultError 24 | for _, m := range msgs { 25 | res = append(res, newResultError(m)) 26 | } 27 | return res 28 | } 29 | 30 | func Test_jsonOutputManager_put(t *testing.T) { 31 | type args struct { 32 | vr ValidationResult 33 | } 34 | 35 | tests := []struct { 36 | msg string 37 | args args 38 | exp string 39 | expErr error 40 | }{ 41 | { 42 | msg: "empty input", 43 | args: args{ 44 | vr: ValidationResult{}, 45 | }, 46 | exp: `[ 47 | { 48 | "filename": "", 49 | "kind": "", 50 | "status": "skipped", 51 | "errors": [] 52 | } 53 | ] 54 | `, 55 | }, 56 | { 57 | msg: "file with no errors", 58 | args: args{ 59 | vr: ValidationResult{ 60 | FileName: "deployment.yaml", 61 | Kind: "deployment", 62 | ValidatedAgainstSchema: true, 63 | Errors: nil, 64 | }, 65 | }, 66 | exp: `[ 67 | { 68 | "filename": "deployment.yaml", 69 | "kind": "deployment", 70 | "status": "valid", 71 | "errors": [] 72 | } 73 | ] 74 | `, 75 | }, 76 | { 77 | msg: "file with errors", 78 | args: args{ 79 | vr: ValidationResult{ 80 | FileName: "service.yaml", 81 | Kind: "service", 82 | ValidatedAgainstSchema: true, 83 | Errors: newResultErrors([]string{ 84 | "i am a error", 85 | "i am another error", 86 | }), 87 | }, 88 | }, 89 | exp: `[ 90 | { 91 | "filename": "service.yaml", 92 | "kind": "service", 93 | "status": "invalid", 94 | "errors": [ 95 | "error: i am a error", 96 | "error: i am another error" 97 | ] 98 | } 99 | ] 100 | `, 101 | }, 102 | } 103 | for _, tt := range tests { 104 | t.Run(tt.msg, func(t *testing.T) { 105 | buf := new(bytes.Buffer) 106 | s := newJSONOutputManager(log.New(buf, "", 0)) 107 | 108 | // record results 109 | err := s.Put(tt.args.vr) 110 | if err != nil { 111 | assert.Equal(t, tt.expErr, err) 112 | } 113 | 114 | // flush final buffer 115 | err = s.Flush() 116 | if err != nil { 117 | assert.Equal(t, tt.expErr, err) 118 | } 119 | 120 | assert.Equal(t, tt.exp, buf.String()) 121 | }) 122 | } 123 | } 124 | 125 | func Test_tapOutputManager_put(t *testing.T) { 126 | type args struct { 127 | vr ValidationResult 128 | } 129 | 130 | tests := []struct { 131 | msg string 132 | args args 133 | exp string 134 | expErr error 135 | }{ 136 | { 137 | msg: "file with no errors", 138 | args: args{ 139 | vr: ValidationResult{ 140 | FileName: "deployment.yaml", 141 | Kind: "Deployment", 142 | ValidatedAgainstSchema: true, 143 | Errors: nil, 144 | }, 145 | }, 146 | exp: `1..1 147 | ok 1 - deployment.yaml (Deployment) 148 | `, 149 | }, 150 | { 151 | msg: "file with errors", 152 | args: args{ 153 | vr: ValidationResult{ 154 | FileName: "service.yaml", 155 | Kind: "Service", 156 | ValidatedAgainstSchema: true, 157 | Errors: newResultErrors([]string{ 158 | "i am a error", 159 | "i am another error", 160 | }), 161 | }, 162 | }, 163 | exp: `1..2 164 | not ok 1 - service.yaml (Service) - error: i am a error 165 | not ok 2 - service.yaml (Service) - error: i am another error 166 | `, 167 | }, 168 | { 169 | msg: "file with no errors because of a skip", 170 | args: args{ 171 | vr: ValidationResult{ 172 | FileName: "deployment.yaml", 173 | Kind: "Deployment", 174 | ValidatedAgainstSchema: false, 175 | Errors: nil, 176 | }, 177 | }, 178 | exp: `1..1 179 | ok 1 - deployment.yaml (Deployment) # SKIP 180 | `, 181 | }, 182 | } 183 | for _, tt := range tests { 184 | t.Run(tt.msg, func(t *testing.T) { 185 | buf := new(bytes.Buffer) 186 | s := newTAPOutputManager(log.New(buf, "", 0)) 187 | 188 | // record results 189 | err := s.Put(tt.args.vr) 190 | if err != nil { 191 | assert.Equal(t, tt.expErr, err) 192 | } 193 | 194 | // flush final buffer 195 | err = s.Flush() 196 | if err != nil { 197 | assert.Equal(t, tt.expErr, err) 198 | } 199 | 200 | assert.Equal(t, tt.exp, buf.String()) 201 | }) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /kubeval/utils.go: -------------------------------------------------------------------------------- 1 | package kubeval 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | func getObject(body map[string]interface{}, key string) (map[string]interface{}, error) { 11 | value, found := body[key] 12 | if !found { 13 | return nil, fmt.Errorf("Missing '%s' key", key) 14 | } 15 | if value == nil { 16 | return nil, fmt.Errorf("Missing '%s' value", key) 17 | } 18 | typedValue, ok := value.(map[string]interface{}) 19 | if !ok { 20 | return nil, fmt.Errorf("Expected object value for key '%s'", key) 21 | } 22 | return typedValue, nil 23 | } 24 | 25 | func getStringAt(body map[string]interface{}, path []string) (string, error) { 26 | obj := body 27 | visited := []string{} 28 | var last interface{} = body 29 | for _, key := range path { 30 | visited = append(visited, key) 31 | 32 | typed, ok := last.(map[string]interface{}) 33 | if !ok { 34 | return "", fmt.Errorf("Expected object at key '%s'", strings.Join(visited, ".")) 35 | } 36 | obj = typed 37 | 38 | value, found := obj[key] 39 | if !found { 40 | return "", fmt.Errorf("Missing '%s' key", strings.Join(visited, ".")) 41 | } 42 | last = value 43 | } 44 | typed, ok := last.(string) 45 | if !ok { 46 | return "", fmt.Errorf("Expected string value for key '%s'", strings.Join(visited, ".")) 47 | } 48 | return typed, nil 49 | } 50 | 51 | func getString(body map[string]interface{}, key string) (string, error) { 52 | value, found := body[key] 53 | if !found { 54 | return "", fmt.Errorf("Missing '%s' key", key) 55 | } 56 | if value == nil { 57 | return "", fmt.Errorf("Missing '%s' value", key) 58 | } 59 | typedValue, ok := value.(string) 60 | if !ok { 61 | return "", fmt.Errorf("Expected string value for key '%s'", key) 62 | } 63 | return typedValue, nil 64 | } 65 | 66 | // detectLineBreak returns the relevant platform specific line ending 67 | func detectLineBreak(haystack []byte) string { 68 | windowsLineEnding := bytes.Contains(haystack, []byte("\r\n")) 69 | if windowsLineEnding && runtime.GOOS == "windows" { 70 | return "\r\n" 71 | } 72 | return "\n" 73 | } 74 | 75 | // in is a method which tests whether the `key` is in the set 76 | func in(set []string, key string) bool { 77 | for _, k := range set { 78 | if k == key { 79 | return true 80 | } 81 | } 82 | return false 83 | } 84 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/fatih/color" 8 | multierror "github.com/hashicorp/go-multierror" 9 | ) 10 | 11 | func Success(message ...string) { 12 | green := color.New(color.FgGreen).SprintFunc() 13 | fmt.Printf("%s - %v\n", green("PASS"), strings.Join(message, " ")) 14 | } 15 | 16 | func Warn(message ...string) { 17 | yellow := color.New(color.FgYellow).SprintFunc() 18 | fmt.Printf("%s - %v\n", yellow("WARN"), strings.Join(message, " ")) 19 | } 20 | 21 | func Error(message error) { 22 | if merr, ok := message.(*multierror.Error); ok { 23 | for _, serr := range merr.Errors { 24 | Error(serr) 25 | } 26 | } else { 27 | red := color.New(color.FgRed).SprintFunc() 28 | fmt.Printf("%s - %v\n", red("ERR "), message) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "regexp" 14 | "runtime" 15 | "strings" 16 | 17 | "github.com/fatih/color" 18 | multierror "github.com/hashicorp/go-multierror" 19 | "github.com/spf13/cobra" 20 | "github.com/spf13/viper" 21 | 22 | "github.com/instrumenta/kubeval/kubeval" 23 | "github.com/instrumenta/kubeval/log" 24 | ) 25 | 26 | var ( 27 | version = "dev" 28 | commit = "none" 29 | date = "unknown" 30 | directories = []string{} 31 | ignoredPathPatterns = []string{} 32 | 33 | // forceColor tells kubeval to use colored output even if 34 | // stdout is not a TTY 35 | forceColor bool 36 | 37 | config = kubeval.NewDefaultConfig() 38 | ) 39 | 40 | // RootCmd represents the the command to run when kubeval is run 41 | var RootCmd = &cobra.Command{ 42 | Short: "Validate a Kubernetes YAML file against the relevant schema", 43 | Long: `Validate a Kubernetes YAML file against the relevant schema`, 44 | Version: fmt.Sprintf("Version: %s\nCommit: %s\nDate: %s\n", version, commit, date), 45 | Run: func(cmd *cobra.Command, args []string) { 46 | if config.IgnoreMissingSchemas && !config.Quiet { 47 | log.Warn("Set to ignore missing schemas") 48 | } 49 | 50 | // This is not particularly secure but we highlight that with the name of 51 | // the config item. It would be good to also support a configurable set of 52 | // trusted certificate authorities as in the `--certificate-authority` 53 | // kubectl option. 54 | if config.InsecureSkipTLSVerify { 55 | http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{ 56 | InsecureSkipVerify: true, 57 | } 58 | } 59 | 60 | success := true 61 | windowsStdinIssue := false 62 | outputManager := kubeval.GetOutputManager(config.OutputFormat) 63 | 64 | stat, err := os.Stdin.Stat() 65 | if err != nil { 66 | // Stat() will return an error on Windows in both Powershell and 67 | // console until go1.9 when nothing is passed on stdin. 68 | // See https://github.com/golang/go/issues/14853. 69 | if runtime.GOOS != "windows" { 70 | log.Error(err) 71 | os.Exit(1) 72 | } else { 73 | windowsStdinIssue = true 74 | } 75 | } 76 | // Assert that colors will definitely be used if requested 77 | if forceColor { 78 | color.NoColor = false 79 | } 80 | // We detect whether we have anything on stdin to process if we have no arguments 81 | // or if the argument is a - 82 | notty := (stat.Mode() & os.ModeCharDevice) == 0 83 | noFileOrDirArgs := (len(args) < 1 || args[0] == "-") && len(directories) < 1 84 | if noFileOrDirArgs && !windowsStdinIssue && notty { 85 | buffer := new(bytes.Buffer) 86 | _, err := io.Copy(buffer, os.Stdin) 87 | if err != nil { 88 | log.Error(err) 89 | os.Exit(1) 90 | } 91 | schemaCache := kubeval.NewSchemaCache() 92 | config.FileName = viper.GetString("filename") 93 | results, err := kubeval.ValidateWithCache(buffer.Bytes(), schemaCache, config) 94 | if err != nil { 95 | log.Error(err) 96 | os.Exit(1) 97 | } 98 | success = !hasErrors(results) 99 | 100 | for _, r := range results { 101 | err = outputManager.Put(r) 102 | if err != nil { 103 | log.Error(err) 104 | os.Exit(1) 105 | } 106 | } 107 | } else { 108 | if len(args) < 1 && len(directories) < 1 { 109 | log.Error(errors.New("You must pass at least one file as an argument, or at least one directory to the directories flag")) 110 | os.Exit(1) 111 | } 112 | schemaCache := kubeval.NewSchemaCache() 113 | files, err := aggregateFiles(args) 114 | if err != nil { 115 | log.Error(err) 116 | success = false 117 | } 118 | 119 | var aggResults []kubeval.ValidationResult 120 | for _, fileName := range files { 121 | filePath, _ := filepath.Abs(fileName) 122 | fileContents, err := ioutil.ReadFile(filePath) 123 | if err != nil { 124 | log.Error(fmt.Errorf("Could not open file %v", fileName)) 125 | earlyExit() 126 | success = false 127 | continue 128 | } 129 | config.FileName = fileName 130 | results, err := kubeval.ValidateWithCache(fileContents, schemaCache, config) 131 | if err != nil { 132 | log.Error(err) 133 | earlyExit() 134 | success = false 135 | continue 136 | } 137 | 138 | for _, r := range results { 139 | err := outputManager.Put(r) 140 | if err != nil { 141 | log.Error(err) 142 | os.Exit(1) 143 | } 144 | } 145 | 146 | aggResults = append(aggResults, results...) 147 | } 148 | 149 | // only use result of hasErrors check if `success` is currently truthy 150 | success = success && !hasErrors(aggResults) 151 | } 152 | 153 | // flush any final logs which may be sitting in the buffer 154 | err = outputManager.Flush() 155 | if err != nil { 156 | log.Error(err) 157 | os.Exit(1) 158 | } 159 | 160 | if !success { 161 | os.Exit(1) 162 | } 163 | }, 164 | } 165 | 166 | // hasErrors returns truthy if any of the provided results 167 | // contain errors. 168 | func hasErrors(res []kubeval.ValidationResult) bool { 169 | for _, r := range res { 170 | if len(r.Errors) > 0 { 171 | return true 172 | } 173 | } 174 | return false 175 | } 176 | 177 | // isIgnored returns whether the specified filename should be ignored. 178 | func isIgnored(path string) (bool, error) { 179 | for _, p := range ignoredPathPatterns { 180 | m, err := regexp.MatchString(p, path) 181 | if err != nil { 182 | return false, err 183 | } 184 | if m { 185 | return true, nil 186 | } 187 | } 188 | return false, nil 189 | } 190 | 191 | func aggregateFiles(args []string) ([]string, error) { 192 | files := make([]string, len(args)) 193 | copy(files, args) 194 | 195 | var allErrors *multierror.Error 196 | for _, directory := range directories { 197 | err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { 198 | if err != nil { 199 | return err 200 | } 201 | ignored, err := isIgnored(path) 202 | if err != nil { 203 | return err 204 | } 205 | if !info.IsDir() && (strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml")) && !ignored { 206 | files = append(files, path) 207 | } 208 | return nil 209 | }) 210 | if err != nil { 211 | allErrors = multierror.Append(allErrors, err) 212 | } 213 | } 214 | 215 | return files, allErrors.ErrorOrNil() 216 | } 217 | 218 | func earlyExit() { 219 | if config.ExitOnError { 220 | os.Exit(1) 221 | } 222 | } 223 | 224 | // Execute adds all child commands to the root command sets flags appropriately. 225 | // This is called by main.main(). It only needs to happen once to the rootCmd. 226 | func Execute() { 227 | if err := RootCmd.Execute(); err != nil { 228 | log.Error(err) 229 | os.Exit(-1) 230 | } 231 | } 232 | 233 | func init() { 234 | rootCmdName := filepath.Base(os.Args[0]) 235 | if strings.HasPrefix(rootCmdName, "kubectl-") { 236 | rootCmdName = strings.Replace(rootCmdName, "-", " ", 1) 237 | } 238 | RootCmd.Use = fmt.Sprintf("%s [file...]", rootCmdName) 239 | kubeval.AddKubevalFlags(RootCmd, config) 240 | RootCmd.Flags().BoolVarP(&forceColor, "force-color", "", false, "Force colored output even if stdout is not a TTY") 241 | RootCmd.SetVersionTemplate(`{{.Version}}`) 242 | RootCmd.Flags().StringSliceVarP(&directories, "directories", "d", []string{}, "A comma-separated list of directories to recursively search for YAML documents") 243 | RootCmd.Flags().StringSliceVarP(&ignoredPathPatterns, "ignored-path-patterns", "i", []string{}, "A comma-separated list of regular expressions specifying paths to ignore") 244 | RootCmd.Flags().StringSliceVarP(&ignoredPathPatterns, "ignored-filename-patterns", "", []string{}, "An alias for ignored-path-patterns") 245 | 246 | viper.SetEnvPrefix("KUBEVAL") 247 | viper.AutomaticEnv() 248 | viper.BindPFlag("schema_location", RootCmd.Flags().Lookup("schema-location")) 249 | viper.BindPFlag("filename", RootCmd.Flags().Lookup("filename")) 250 | } 251 | 252 | func main() { 253 | Execute() 254 | } 255 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Kubeval 2 | theme: 3 | name: "material" 4 | repo_url: https://github.com/instrumenta/kubeval 5 | nav: 6 | - "Usage": "index.md" 7 | - "Installation": "installation.md" 8 | - "Go Library": "go.md" 9 | - "Contrib": "contrib.md" 10 | markdown_extensions: 11 | - codehilite 12 | google_analytics: ["UA-138875752-1", "kubeval.instrumenta.dev"] 13 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "runtime" 5 | ) 6 | 7 | var ( 8 | // BuildVersion set at build time 9 | BuildVersion string 10 | // BuildTime set at build time 11 | BuildTime string 12 | // BuildSHA set at build time 13 | BuildSHA string 14 | ) 15 | 16 | // ClientVersion contains information about the current client 17 | type ClientVersion struct { 18 | BuildVersion string 19 | BuildTime string 20 | BuildSHA string 21 | GoVersion string 22 | Os string 23 | Arch string 24 | } 25 | 26 | // Version constructed at build time 27 | var Version = ClientVersion{BuildVersion, 28 | BuildTime, 29 | BuildSHA, 30 | runtime.Version(), 31 | runtime.GOOS, 32 | runtime.GOARCH} 33 | --------------------------------------------------------------------------------