mirror of
https://github.com/yannh/kubeconform.git
synced 2026-02-12 14:39:21 +00:00
Compare commits
148 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e60892483e | ||
|
|
c7f8490e52 | ||
|
|
e65429b1e5 | ||
|
|
a23275d5ca | ||
|
|
3134f4477e | ||
|
|
9f04fec268 | ||
|
|
31e9679c96 | ||
|
|
df26febc54 | ||
|
|
1bd44986dd | ||
|
|
43a2445cb4 | ||
|
|
706cd56e87 | ||
|
|
50ce5f8ecb | ||
|
|
347cd5e4c9 | ||
|
|
142517fc45 | ||
|
|
7062384492 | ||
|
|
20805f652c | ||
|
|
9627dd185b | ||
|
|
14053aaa54 | ||
|
|
71a59d74f2 | ||
|
|
ad166c7f0d | ||
|
|
a8000fd445 | ||
|
|
b6728f181c | ||
|
|
a4d74ce7d2 | ||
|
|
808e6d4aa5 | ||
|
|
d8f00a3a30 | ||
|
|
6ae8c45bc1 | ||
|
|
b7d7b4d0dc | ||
|
|
2e50b79b16 | ||
|
|
13a78ebad8 | ||
|
|
ae67bb4709 | ||
|
|
278385f4c9 | ||
|
|
452f1fe1db | ||
|
|
f0a7d5203d | ||
|
|
71fd5f8386 | ||
|
|
c8bce62898 | ||
|
|
065fad003f | ||
|
|
c1a2c159de | ||
|
|
65cfe7e16e | ||
|
|
ce2f6de185 | ||
|
|
ad935b7e32 | ||
|
|
d038bf8840 | ||
|
|
8bc9f42f39 | ||
|
|
9294e94a8d | ||
|
|
16d52804d4 | ||
|
|
e3bb34851d | ||
|
|
aaecabe0b7 | ||
|
|
563e1db94c | ||
|
|
9860cde144 | ||
|
|
ee7c498580 | ||
|
|
84afe70659 | ||
|
|
752a33eaeb | ||
|
|
33cdbf16a4 | ||
|
|
f94844183f | ||
|
|
9d34445328 | ||
|
|
a31707ca58 | ||
|
|
46b7622a08 | ||
|
|
d8e348a597 | ||
|
|
466ec73ed7 | ||
|
|
dbcd787256 | ||
|
|
f5338b07f9 | ||
|
|
f68d6ec6ea | ||
|
|
3cb76bc5e6 | ||
|
|
5cbbd1a898 | ||
|
|
321cc0ea1d | ||
|
|
5e63bc5ad7 | ||
|
|
9a6fff13cb | ||
|
|
7bf1e01dec | ||
|
|
014cbf754f | ||
|
|
f8ab9ae49e | ||
|
|
b5f34caa70 | ||
|
|
932b35d71f | ||
|
|
c5f7348af8 | ||
|
|
85b30f8c2a | ||
|
|
607c90a0a9 | ||
|
|
d10c9bde67 | ||
|
|
2b3139b1db | ||
|
|
3a3d05b27c | ||
|
|
6c1fa513e9 | ||
|
|
048ccf125e | ||
|
|
c32562e742 | ||
|
|
ff2ab3d770 | ||
|
|
e38ff8efd4 | ||
|
|
836b6e5b14 | ||
|
|
fe79a7cfff | ||
|
|
c1b3e93a75 | ||
|
|
30528c5671 | ||
|
|
67a73a9315 | ||
|
|
c7894baed6 | ||
|
|
f2e47c3596 | ||
|
|
c3e6205f15 | ||
|
|
9410471142 | ||
|
|
d63de52458 | ||
|
|
d536a659bd | ||
|
|
ea3c592d63 | ||
|
|
aebc298047 | ||
|
|
a30381c6aa | ||
|
|
ab4745ddf0 | ||
|
|
b571b18f8d | ||
|
|
563106ede1 | ||
|
|
b5c823d6b5 | ||
|
|
4a8aace275 | ||
|
|
8bb87b9a63 | ||
|
|
ff7da0f73b | ||
|
|
19be42b9a6 | ||
|
|
dcc77ac3a3 | ||
|
|
f8dcb19789 | ||
|
|
72d648a5d1 | ||
|
|
468d42f556 | ||
|
|
dfd7a5a102 | ||
|
|
0bf6b97269 | ||
|
|
54e0b8f5bb | ||
|
|
73f65d7530 | ||
|
|
4e83800979 | ||
|
|
9228dba915 | ||
|
|
dee75355d0 | ||
|
|
4544f45fa1 | ||
|
|
2eefa7fc22 | ||
|
|
1b01a42488 | ||
|
|
80d2203a5a | ||
|
|
aadb07a585 | ||
|
|
5bce1d4180 | ||
|
|
ec52b39db3 | ||
|
|
ab7fd7f97d | ||
|
|
ae74fa6e53 | ||
|
|
cf50a6c9da | ||
|
|
727f5fe57e | ||
|
|
98c80939c1 | ||
|
|
44b7ba9aef | ||
|
|
c489a69a4c | ||
|
|
f8ffb2f9e3 | ||
|
|
8721f4c32a | ||
|
|
7be447f44f | ||
|
|
0e22547520 | ||
|
|
53a1689bac | ||
|
|
3a697f3ce2 | ||
|
|
0a14aae014 | ||
|
|
892b3807fe | ||
|
|
affc63bb18 | ||
|
|
c4b044f3be | ||
|
|
df386fc1fd | ||
|
|
a053bbabe1 | ||
|
|
4eb75860d9 | ||
|
|
5ed3ddac8a | ||
|
|
49d434dc4a | ||
|
|
803bf58797 | ||
|
|
f1db99db6a | ||
|
|
2e7c2bfe33 | ||
|
|
d94454920b |
291 changed files with 676107 additions and 10534 deletions
89
.github/workflows/main.yml
vendored
89
.github/workflows/main.yml
vendored
|
|
@ -1,95 +1,50 @@
|
|||
name: ci
|
||||
on: push
|
||||
jobs:
|
||||
test:
|
||||
kubeconform-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: test
|
||||
run: make docker-test
|
||||
|
||||
- name: build
|
||||
run: make docker-build-static
|
||||
run: make goreleaser-build-static
|
||||
|
||||
- name: acceptance-test
|
||||
run: make docker-acceptance
|
||||
|
||||
- name: build-image
|
||||
run: make docker-image
|
||||
|
||||
- name: save image
|
||||
run: make save-image
|
||||
|
||||
- name: archive image
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: kubeconform-image
|
||||
path: kubeconform-image.tar
|
||||
|
||||
publish-image-master:
|
||||
openapi2jsonschema-test:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master'
|
||||
needs: test
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download kubeconform image
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: kubeconform-image
|
||||
|
||||
- name: load image
|
||||
run: docker load < kubeconform-image.tar
|
||||
|
||||
- name: push
|
||||
run: |
|
||||
echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin
|
||||
make push-image
|
||||
env:
|
||||
RELEASE_VERSION: master
|
||||
|
||||
publish-image-release:
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: test
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download kubeconform image
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: kubeconform-image
|
||||
|
||||
- name: load image
|
||||
run: docker load < kubeconform-image.tar
|
||||
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: push-tag
|
||||
run: |
|
||||
echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin
|
||||
make push-image
|
||||
|
||||
- name: push-latest
|
||||
run: |
|
||||
make push-image
|
||||
env:
|
||||
RELEASE_VERSION: latest
|
||||
- name: test
|
||||
working-directory: ./scripts
|
||||
run: make docker-test docker-acceptance
|
||||
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
needs:
|
||||
- kubeconform-test
|
||||
- openapi2jsonschema-test
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # https://github.com/goreleaser/goreleaser-action/issues/56
|
||||
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: goreleaser
|
||||
run: make release
|
||||
run: |
|
||||
echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin
|
||||
GITHUB_ACTOR=$(echo ${GITHUB_ACTOR} | tr '[:upper:]' '[:lower:]')
|
||||
GIT_OWNER=${GITHUB_ACTOR} make release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
30
.github/workflows/site.yml
vendored
Normal file
30
.github/workflows/site.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'site/**'
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v2
|
||||
with:
|
||||
hugo-version: '0.83.1'
|
||||
|
||||
- name: Build
|
||||
run: hugo --minify
|
||||
working-directory: site
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
if: github.ref == 'refs/heads/master'
|
||||
with:
|
||||
publish_dir: ./site/public
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cname: kubeconform.mandragor.org
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +1,4 @@
|
|||
dist/
|
||||
bin/
|
||||
.idea/
|
||||
**/*.pyc
|
||||
|
|
|
|||
|
|
@ -1,31 +1,103 @@
|
|||
project_name: kubeconform
|
||||
builds:
|
||||
- main: ./cmd/kubeconform
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOFLAGS = -mod=vendor
|
||||
- GO111MODULE = on
|
||||
- GIT_OWNER = yannh
|
||||
goos:
|
||||
- windows
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
flags:
|
||||
- -trimpath
|
||||
- -tags=netgo
|
||||
- -a
|
||||
ldflags:
|
||||
- -extldflags "-static"
|
||||
- -X main.version={{.Tag}}
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
|
||||
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
|
||||
dockers:
|
||||
- image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-amd64'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-amd64'
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--platform=linux/amd64"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-arm64'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-arm64'
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--platform=linux/arm64"
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
- image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-amd64-alpine'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-amd64-alpine'
|
||||
dockerfile: Dockerfile-alpine
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--platform=linux/amd64"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-arm64-alpine'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-arm64-alpine'
|
||||
dockerfile: Dockerfile-alpine
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--platform=linux/arm64"
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
|
||||
docker_manifests:
|
||||
- name_template: 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}'
|
||||
image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-amd64'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-arm64'
|
||||
- name_template: 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest'
|
||||
image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-amd64'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-arm64'
|
||||
- name_template: 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-alpine'
|
||||
image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-amd64-alpine'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-arm64-alpine'
|
||||
- name_template: 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-alpine'
|
||||
image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-amd64-alpine'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-arm64-alpine'
|
||||
|
||||
checksum:
|
||||
name_template: 'CHECKSUMS'
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^test:'
|
||||
- '^test:'
|
||||
|
|
|
|||
14
Dockerfile
14
Dockerfile
|
|
@ -1,8 +1,14 @@
|
|||
FROM alpine:latest as certs
|
||||
FROM alpine:3.21.3 as certs
|
||||
RUN apk add ca-certificates
|
||||
|
||||
FROM scratch AS kubeconform
|
||||
MAINTAINER Yann HAMON <yann@mandragor.org>
|
||||
LABEL org.opencontainers.image.authors="Yann Hamon <yann@mandragor.org>" \
|
||||
org.opencontainers.image.source="https://github.com/yannh/kubeconform/" \
|
||||
org.opencontainers.image.description="A Kubernetes manifests validation tool" \
|
||||
org.opencontainers.image.documentation="https://github.com/yannh/kubeconform/" \
|
||||
org.opencontainers.image.licenses="Apache License 2.0" \
|
||||
org.opencontainers.image.title="kubeconform" \
|
||||
org.opencontainers.image.url="https://github.com/yannh/kubeconform/"
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY bin/kubeconform /
|
||||
ENTRYPOINT ["/kubeconform"]
|
||||
COPY kubeconform /
|
||||
ENTRYPOINT ["/kubeconform"]
|
||||
|
|
|
|||
12
Dockerfile-alpine
Normal file
12
Dockerfile-alpine
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
FROM alpine:3.20.2
|
||||
|
||||
LABEL org.opencontainers.image.authors="Yann Hamon <yann@mandragor.org>" \
|
||||
org.opencontainers.image.source="https://github.com/yannh/kubeconform/" \
|
||||
org.opencontainers.image.description="A Kubernetes manifests validation tool" \
|
||||
org.opencontainers.image.documentation="https://github.com/yannh/kubeconform/" \
|
||||
org.opencontainers.image.licenses="Apache License 2.0" \
|
||||
org.opencontainers.image.title="kubeconform" \
|
||||
org.opencontainers.image.url="https://github.com/yannh/kubeconform/"
|
||||
RUN apk add ca-certificates
|
||||
COPY kubeconform /
|
||||
ENTRYPOINT ["/kubeconform"]
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
FROM bats/bats:v1.2.1
|
||||
RUN apk --no-cache add ca-certificates parallel
|
||||
FROM bats/bats:1.11.0
|
||||
RUN apk --no-cache add ca-certificates parallel libxml2-utils
|
||||
COPY bin/kubeconform /code/bin/
|
||||
COPY acceptance.bats acceptance-nonetwork.bats /code/
|
||||
COPY fixtures /code/fixtures
|
||||
|
|
|
|||
193
LICENSE
193
LICENSE
|
|
@ -1,12 +1,199 @@
|
|||
Kubeconform - Validate Kubernetes configuration files
|
||||
|
||||
Copyright (C) 2020 Yann Hamon
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017-2022 Yann Hamon
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
|
|
|||
44
Makefile
44
Makefile
|
|
@ -2,34 +2,27 @@
|
|||
|
||||
RELEASE_VERSION ?= latest
|
||||
|
||||
.PHONY: test-build test build build-static docker-test docker-build-static build-bats docker-acceptance docker-image release
|
||||
.PHONY: local-test local-build local-build-static docker-test docker-build docker-build-static build-bats docker-acceptance release update-deps build-single-target
|
||||
|
||||
test-build: test build
|
||||
local-test:
|
||||
go test -race ./... -count=1
|
||||
|
||||
test:
|
||||
go test -race ./...
|
||||
|
||||
build:
|
||||
local-build:
|
||||
git config --global --add safe.directory $$PWD
|
||||
go build -o bin/ ./...
|
||||
|
||||
docker-image:
|
||||
docker build -t kubeconform:${RELEASE_VERSION} .
|
||||
|
||||
save-image:
|
||||
docker save --output kubeconform-image.tar kubeconform:${RELEASE_VERSION}
|
||||
|
||||
push-image:
|
||||
docker tag kubeconform:latest ghcr.io/yannh/kubeconform:${RELEASE_VERSION}
|
||||
docker push ghcr.io/yannh/kubeconform:${RELEASE_VERSION}
|
||||
|
||||
build-static:
|
||||
local-build-static:
|
||||
CGO_ENABLED=0 GOFLAGS=-mod=vendor GOOS=linux GOARCH=amd64 GO111MODULE=on go build -trimpath -tags=netgo -ldflags "-extldflags=\"-static\"" -a -o bin/ ./...
|
||||
|
||||
# These only used for development. Release artifacts and docker images are produced by goreleaser.
|
||||
docker-test:
|
||||
docker run -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform golang:1.14 make test
|
||||
docker run -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform golang:1.24.3 make local-test
|
||||
|
||||
docker-build:
|
||||
docker run -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform golang:1.24.3 make local-build
|
||||
|
||||
docker-build-static:
|
||||
docker run -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform golang:1.14 make build-static
|
||||
docker run -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform golang:1.24.3 make local-build-static
|
||||
|
||||
build-bats:
|
||||
docker build -t bats -f Dockerfile.bats .
|
||||
|
|
@ -38,5 +31,16 @@ docker-acceptance: build-bats
|
|||
docker run -t bats -p acceptance.bats
|
||||
docker run --network none -t bats -p acceptance-nonetwork.bats
|
||||
|
||||
goreleaser-build-static:
|
||||
docker run -t -e GOOS=linux -e GOARCH=amd64 -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform goreleaser/goreleaser:v2.9.0 build --clean --single-target --snapshot
|
||||
cp dist/kubeconform_linux_amd64_v1/kubeconform bin/
|
||||
|
||||
release:
|
||||
docker run -e GITHUB_TOKEN -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform goreleaser/goreleaser:v0.138 goreleaser release --rm-dist
|
||||
docker run -e GITHUB_TOKEN -e GIT_OWNER -t -v /var/run/docker.sock:/var/run/docker.sock -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform goreleaser/goreleaser:v2.9.0 release --clean
|
||||
|
||||
update-deps:
|
||||
go get -u ./...
|
||||
go mod tidy
|
||||
|
||||
update-junit-xsd:
|
||||
curl https://raw.githubusercontent.com/junit-team/junit5/main/platform-tests/src/test/resources/jenkins-junit.xsd > fixtures/junit.xsd
|
||||
|
|
|
|||
309
Readme.md
309
Readme.md
|
|
@ -1,11 +1,12 @@
|
|||
# Kubeconform
|
||||
<img width="50%" alt="Kubeconform-GitHub-Hero" src="https://user-images.githubusercontent.com/19731161/142411871-f695e40c-bfa8-43ca-97c0-94c256749732.png">
|
||||
<hr>
|
||||
|
||||
[](https://github.com/yannh/kubeconform/actions?query=branch%3Amaster)
|
||||
[](https://github.com/yannh/kubeconform/actions?query=branch%3Amaster)
|
||||
[](https://formulae.brew.sh/formula/kubeconform)
|
||||
[](https://goreportcard.com/report/github.com/yannh/kubeconform)
|
||||
[](https://pkg.go.dev/github.com/yannh/kubeconform/pkg/validator)
|
||||
|
||||
Kubeconform is a Kubernetes manifests validation tool. Build it into your CI to validate your Kubernetes
|
||||
configuration!
|
||||
`Kubeconform` is a Kubernetes manifest validation tool. Incorporate it into your CI, or use it locally to validate your Kubernetes configuration!
|
||||
|
||||
It is inspired by, contains code from and is designed to stay close to
|
||||
[Kubeval](https://github.com/instrumenta/kubeval), but with the following improvements:
|
||||
|
|
@ -14,88 +15,141 @@ It is inspired by, contains code from and is designed to stay close to
|
|||
* configurable list of **remote, or local schemas locations**, enabling validating Kubernetes
|
||||
custom resources (CRDs) and offline validation capabilities
|
||||
* uses by default a [self-updating fork](https://github.com/yannh/kubernetes-json-schema) of the schemas registry maintained
|
||||
by the [kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) project - which guarantees
|
||||
by the kubernetes-json-schema project - which guarantees
|
||||
up-to-date **schemas for all recent versions of Kubernetes**.
|
||||
|
||||
<details><summary><h4>Speed comparison with Kubeval</h4></summary><p>
|
||||
Running on a pretty large kubeconfigs setup, on a laptop with 4 cores:
|
||||
|
||||
```bash
|
||||
$ time kubeconform -ignore-missing-schemas -n 8 -summary preview staging production
|
||||
Summary: 50714 resources found in 35139 files - Valid: 27334, Invalid: 0, Errors: 0 Skipped: 23380
|
||||
real 0m6,710s
|
||||
user 0m38,701s
|
||||
sys 0m1,161s
|
||||
$ time kubeval -d preview,staging,production --ignore-missing-schemas --quiet
|
||||
[... Skipping output]
|
||||
real 0m35,336s
|
||||
user 0m0,717s
|
||||
sys 0m1,069s
|
||||
```
|
||||
</p></details>
|
||||
|
||||
### A small overview of Kubernetes manifest validation
|
||||
## Table of contents
|
||||
|
||||
* [A small overview of Kubernetes manifest validation](#a-small-overview-of-kubernetes-manifest-validation)
|
||||
* [Limits of Kubeconform validation](#Limits-of-Kubeconform-validation)
|
||||
* [Installation](#Installation)
|
||||
* [Usage](#Usage)
|
||||
* [Usage examples](#Usage-examples)
|
||||
* [Proxy support](#Proxy-support)
|
||||
* [Overriding schemas location](#Overriding-schemas-location)
|
||||
* [CustomResourceDefinition (CRD) Support](#CustomResourceDefinition-CRD-Support)
|
||||
* [OpenShift schema Support](#OpenShift-schema-Support)
|
||||
* [Integrating Kubeconform in the CI](#Integrating-Kubeconform-in-the-CI)
|
||||
* [Github Workflow](#Github-Workflow)
|
||||
* [Gitlab-CI](#Gitlab-CI)
|
||||
* [Helm charts](#helm-charts)
|
||||
* [Using kubeconform as a Go Module](#Using-kubeconform-as-a-Go-Module)
|
||||
* [Credits](#Credits)
|
||||
|
||||
## A small overview of Kubernetes manifest validation
|
||||
|
||||
Kubernetes's API is described using the [OpenAPI (formerly swagger) specification](https://www.openapis.org),
|
||||
in a [file](https://github.com/kubernetes/kubernetes/blob/master/api/openapi-spec/swagger.json) checked into
|
||||
the main Kubernetes repository.
|
||||
|
||||
Because of the state of the tooling to perform validation against OpenAPI schemas, projects usually convert
|
||||
the OpenAPI schemas to [JSON schemas](https://json-schema.org/) first. Kubeval relies on
|
||||
the OpenAPI schemas to [JSON schemas](https://json-schema.org/) first. Kubeval relies on
|
||||
[instrumenta/OpenApi2JsonSchema](https://github.com/instrumenta/openapi2jsonschema) to convert Kubernetes' Swagger file
|
||||
and break it down into multiple JSON schemas, stored in github at
|
||||
[instrumenta/kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) and published on
|
||||
[kubernetesjsonschema.dev](https://kubernetesjsonschema.dev/).
|
||||
|
||||
Kubeconform relies on [a fork of kubernetes-json-schema](https://github.com/yannh/kubernetes-json-schema/)
|
||||
that is more aggressively kept up-to-date, and contains schemas for all recent versions of Kubernetes.
|
||||
`Kubeconform` relies on [a fork of kubernetes-json-schema](https://github.com/yannh/kubernetes-json-schema/)
|
||||
that is more meticulously kept up-to-date, and contains schemas for all recent versions of Kubernetes.
|
||||
|
||||
### Limits of Kubeconform validation
|
||||
|
||||
Kubeconform, similarly to kubeval, only validates manifests using the OpenAPI specifications. In some
|
||||
cases, the Kubernetes controllers might perform additional validation - so that manifests passing kubeval
|
||||
validation would still error when being deployed. See for example these bugs against kubeval:
|
||||
[#253](https://github.com/instrumenta/kubeval/issues/253)
|
||||
[#256](https://github.com/instrumenta/kubeval/issues/256)
|
||||
[#257](https://github.com/instrumenta/kubeval/issues/257)
|
||||
[#259](https://github.com/instrumenta/kubeval/issues/259). The validation logic mentioned in these
|
||||
bug reports is not part of Kubernetes' OpenAPI spec, and therefore kubeconform/kubeval will not detect the
|
||||
configuration errors.
|
||||
`Kubeconform`, similar to `kubeval`, only validates manifests using the official Kubernetes OpenAPI specifications. The Kubernetes controllers still perform additional server-side validations that are not part of the OpenAPI specifications. Those server-side validations are not covered by `Kubeconform` (examples: [#65](https://github.com/yannh/kubeconform/issues/65), [#122](https://github.com/yannh/kubeconform/issues/122), [#142](https://github.com/yannh/kubeconform/issues/142)). You can use a 3rd-party tool or the `kubectl --dry-run=server` command to fill the missing (validation) gap.
|
||||
|
||||
## Installation
|
||||
|
||||
### Usage
|
||||
If you are a [Homebrew](https://brew.sh/) user, you can install by running:
|
||||
|
||||
```bash
|
||||
$ brew install kubeconform
|
||||
```
|
||||
|
||||
If you are a Windows user, you can install with [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/) by running:
|
||||
|
||||
```cmd
|
||||
winget install YannHamon.kubeconform
|
||||
```
|
||||
|
||||
You can also download the latest version from the [release page](https://github.com/yannh/kubeconform/releases).
|
||||
|
||||
Another way of installation is via Golang's package manager:
|
||||
|
||||
```bash
|
||||
# With a specific version tag
|
||||
$ go install github.com/yannh/kubeconform/cmd/kubeconform@v0.4.13
|
||||
|
||||
# Latest version
|
||||
$ go install github.com/yannh/kubeconform/cmd/kubeconform@latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
$ ./bin/kubeconform -h
|
||||
Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
||||
$ kubeconform -h
|
||||
Usage: kubeconform [OPTION]... [FILE OR FOLDER]...
|
||||
-cache string
|
||||
cache schemas downloaded via HTTP to this folder
|
||||
-cpu-prof string
|
||||
debug - log CPU profiling to file
|
||||
cache schemas downloaded via HTTP to this folder
|
||||
-debug
|
||||
print debug information
|
||||
-exit-on-error
|
||||
immediately stop execution when the first error is encountered
|
||||
-h show help information
|
||||
immediately stop execution when the first error is encountered
|
||||
-h show help information
|
||||
-ignore-filename-pattern value
|
||||
regular expression specifying paths to ignore (can be specified multiple times)
|
||||
regular expression specifying paths to ignore (can be specified multiple times)
|
||||
-ignore-missing-schemas
|
||||
skip files with missing schemas instead of failing
|
||||
skip files with missing schemas instead of failing
|
||||
-insecure-skip-tls-verify
|
||||
disable verification of the server's SSL certificate. This will make your HTTPS connections insecure
|
||||
disable verification of the server's SSL certificate. This will make your HTTPS connections insecure
|
||||
-kubernetes-version string
|
||||
version of Kubernetes to validate against (default "1.18.0")
|
||||
version of Kubernetes to validate against, e.g.: 1.18.0 (default "master")
|
||||
-n int
|
||||
number of goroutines to run concurrently (default 4)
|
||||
number of goroutines to run concurrently (default 4)
|
||||
-output string
|
||||
output format - json, junit, tap, text (default "text")
|
||||
output format - json, junit, pretty, tap, text (default "text")
|
||||
-reject string
|
||||
comma-separated list of kinds to reject
|
||||
comma-separated list of kinds or GVKs to reject
|
||||
-schema-location value
|
||||
override schemas location search path (can be specified multiple times)
|
||||
override schemas location search path (can be specified multiple times)
|
||||
-skip string
|
||||
comma-separated list of kinds to ignore
|
||||
comma-separated list of kinds or GVKs to ignore
|
||||
-strict
|
||||
disallow additional properties not in schema
|
||||
disallow additional properties not in schema or duplicated keys
|
||||
-summary
|
||||
print a summary at the end (ignored for junit output)
|
||||
print a summary at the end (ignored for junit output)
|
||||
-v show version information
|
||||
-verbose
|
||||
print results for all resources (ignored for tap and junit output)
|
||||
print results for all resources (ignored for tap and junit output)
|
||||
```
|
||||
|
||||
### Usage examples
|
||||
|
||||
* Validating a single, valid file
|
||||
```
|
||||
$ ./bin/kubeconform fixtures/valid.yaml
|
||||
```bash
|
||||
$ kubeconform fixtures/valid.yaml
|
||||
$ echo $?
|
||||
0
|
||||
```
|
||||
|
||||
* Validating a single invalid file, setting output to json, and printing a summary
|
||||
```
|
||||
$ ./bin/kubeconform -summary -output json fixtures/invalid.yaml
|
||||
```bash
|
||||
$ kubeconform -summary -output json fixtures/invalid.yaml
|
||||
{
|
||||
"resources": [
|
||||
{
|
||||
|
|
@ -118,88 +172,133 @@ $ echo $?
|
|||
```
|
||||
|
||||
* Passing manifests via Stdin
|
||||
```
|
||||
```bash
|
||||
cat fixtures/valid.yaml | ./bin/kubeconform -summary
|
||||
Summary: 1 resource found parsing stdin - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
|
||||
```
|
||||
|
||||
* Validating a file, ignoring its resource using both Kind, and GVK (Group, Version, Kind) notations
|
||||
```
|
||||
# This will ignore ReplicationController for all apiVersions
|
||||
$ kubeconform -summary -skip ReplicationController fixtures/valid.yaml
|
||||
Summary: 1 resource found in 1 file - Valid: 0, Invalid: 0, Errors: 0, Skipped: 1
|
||||
|
||||
# This will ignore ReplicationController only for apiVersion v1
|
||||
$ kubeconform -summary -skip v1/ReplicationController fixtures/valid.yaml
|
||||
Summary: 1 resource found in 1 file - Valid: 0, Invalid: 0, Errors: 0, Skipped: 1
|
||||
```
|
||||
|
||||
* Validating a folder, increasing the number of parallel workers
|
||||
```
|
||||
$ ./bin/kubeconform -summary -n 16 fixtures
|
||||
$ kubeconform -summary -n 16 fixtures
|
||||
fixtures/crd_schema.yaml - CustomResourceDefinition trainingjobs.sagemaker.aws.amazon.com failed validation: could not find schema for CustomResourceDefinition
|
||||
fixtures/invalid.yaml - ReplicationController bob is invalid: Invalid type. Expected: [integer,null], given: string
|
||||
[...]
|
||||
Summary: 65 resources found in 34 files - Valid: 55, Invalid: 2, Errors: 8 Skipped: 0
|
||||
```
|
||||
|
||||
### Overriding schemas location - CRD and Openshift support
|
||||
### Proxy support
|
||||
|
||||
When the `-schema-location` parameter is not used, or set to "default", kubeconform will default to downloading
|
||||
schemas from `https://github.com/yannh/kubernetes-json-schema`. Kubeconform however supports passing one, or multiple,
|
||||
`Kubeconform` will respect the **HTTPS_PROXY** variable when downloading schema files.
|
||||
|
||||
```bash
|
||||
$ HTTPS_PROXY=proxy.local bin/kubeconform fixtures/valid.yaml
|
||||
```
|
||||
|
||||
## Overriding schemas location
|
||||
|
||||
When the `-schema-location` parameter is not used, or set to `default`, kubeconform will default to downloading
|
||||
schemas from https://github.com/yannh/kubernetes-json-schema. Kubeconform however supports passing one, or multiple,
|
||||
schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions
|
||||
in each of them, in order, stopping as soon as a matching file is found.
|
||||
|
||||
* If the -schema-location value does not end with '.json', Kubeconform will assume filenames / a file
|
||||
structure identical to that of kubernetesjsonschema.dev or github.com/yannh/kubernetes-json-schema.
|
||||
* if the -schema-location value ends with '.json' - Kubeconform assumes the value is a Go templated
|
||||
string that indicates how to search for JSON schemas.
|
||||
* the -schema-location value of "default" is an alias for https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json.
|
||||
Both following command lines are equivalent:
|
||||
```
|
||||
$ ./bin/kubeconform fixtures/valid.yaml
|
||||
$ ./bin/kubeconform -schema-location default fixtures/valid.yaml
|
||||
$ ./bin/kubeconform -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
```
|
||||
|
||||
To support validating CRDs, we need to convert OpenAPI files to JSON schema, storing the JSON schemas
|
||||
in a local folder - for example schemas. Then we specify this folder as an additional registry to lookup:
|
||||
* If the `-schema-location` value does not end with `.json`, Kubeconform will assume filenames / a file
|
||||
structure identical to that of [kubernetesjsonschema.dev](https://kubernetesjsonschema.dev/) or [yannh/kubernetes-json-schema](https://github.com/yannh/kubernetes-json-schema).
|
||||
* if the `-schema-location` value ends with `.json` - Kubeconform assumes the value is a **Go templated
|
||||
string** that indicates how to search for JSON schemas.
|
||||
* the `-schema-location` value of `default` is an alias for `https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{.NormalizedKubernetesVersion}}-standalone{{.StrictSuffix}}/{{.ResourceKind}}{{.KindSuffix}}.json`.
|
||||
|
||||
**The following command lines are equivalent:**
|
||||
```bash
|
||||
$ kubeconform fixtures/valid.yaml
|
||||
$ kubeconform -schema-location default fixtures/valid.yaml
|
||||
$ kubeconform -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{.NormalizedKubernetesVersion}}-standalone{{.StrictSuffix}}/{{.ResourceKind}}{{.KindSuffix}}.json' fixtures/valid.yaml
|
||||
```
|
||||
# If the resource Kind is not found in kubernetesjsonschema.dev, also lookup in the schemas/ folder for a matching file
|
||||
$ ./bin/kubeconform -schema-location default -schema-location 'schemas/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/custom-resource.yaml
|
||||
```
|
||||
|
||||
You can validate Openshift manifests using a custom schema location. Set the OpenShift version to validate
|
||||
against using -kubernetes-version.
|
||||
|
||||
```
|
||||
bin/kubeconform -kubernetes-version 3.8.0 -schema-location 'https://raw.githubusercontent.com/garethr/openshift-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}.json' -summary fixtures/valid.yaml
|
||||
Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
|
||||
```
|
||||
|
||||
Here are the variables you can use in -schema-location:
|
||||
* *NormalizedKubernetesVersion* - Kubernetes Version, prefixed by v
|
||||
* *StrictSuffix* - "-strict" or "" depending on whether validation is running in strict mode or not
|
||||
* *ResourceKind* - Kind of the Kubernetes Resource
|
||||
* *ResourceAPIVersion* - Version of API used for the resource - "v1" in "apiVersion: monitoring.coreos.com/v1"
|
||||
* *KindSuffix* - suffix computed from apiVersion - for compatibility with Kubeval schema registries
|
||||
* *Group* - the group name as stated in this resource's definition - "monitoring.coreos.com" in "apiVersion: monitoring.coreos.com/v1"
|
||||
* *KindSuffix* - suffix computed from apiVersion - for compatibility with `Kubeval` schema registries
|
||||
|
||||
### Converting an OpenAPI file to a JSON Schema
|
||||
### CustomResourceDefinition (CRD) Support
|
||||
|
||||
Kubeconform uses JSON schemas to validate Kubernetes resources. For Custom Resource, the CustomResourceDefinition
|
||||
first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions
|
||||
Because Custom Resources (CR) are not native Kubernetes objects, they are not included in the default schema.
|
||||
If your CRs are present in [Datree's CRDs-catalog](https://github.com/datreeio/CRDs-catalog), you can specify this project as an additional registry to lookup:
|
||||
|
||||
```bash
|
||||
# Look in the CRDs-catalog for the desired schema/s
|
||||
$ kubeconform -schema-location default -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' [MANIFEST]
|
||||
```
|
||||
|
||||
If your CRs are not present in the CRDs-catalog, you will need to manually pull the CRDs manifests from your cluster and convert the `OpenAPI.spec` to JSON schema format.
|
||||
|
||||
<details><summary>Converting an OpenAPI file to a JSON Schema</summary>
|
||||
<p>
|
||||
|
||||
`Kubeconform` uses JSON schemas to validate Kubernetes resources. For Custom Resource, the CustomResourceDefinition
|
||||
first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions
|
||||
to JSON schema. Here is an example how to use it:
|
||||
|
||||
```
|
||||
$ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
```bash
|
||||
$ python ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
JSON schema written to trainingjob_v1.json
|
||||
```
|
||||
|
||||
The `FILENAME_FORMAT` environment variable can be used to change the output file name (Available variables: `kind`, `group`, `version`) (Default: `{kind}_{version}`).
|
||||
By default, the file name output format is `{kind}_{version}`. The `FILENAME_FORMAT` environment variable can be used to change the output file name (Available variables: `kind`, `group`, `fullgroup`, `version`):
|
||||
|
||||
```
|
||||
$ export FILENAME_FORMAT='{kind}-{group}-{version}'
|
||||
$ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
JSON schema written to trainingjob-sagemaker-v1.json
|
||||
|
||||
$ export FILENAME_FORMAT='{kind}-{fullgroup}-{version}'
|
||||
$ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
JSON schema written to trainingjob-sagemaker.aws.amazon.com-v1.json
|
||||
```
|
||||
|
||||
### Usage as a Github Action
|
||||
After converting your CRDs to JSON schema files, you can use `kubeconform` to validate your CRs against them:
|
||||
|
||||
Kubeconform is publishes Docker Images to Github's new Container Registry, ghcr.io. These images
|
||||
```
|
||||
# If the resource Kind is not found in default, also lookup in the schemas/ folder for a matching file
|
||||
$ kubeconform -schema-location default -schema-location 'schemas/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/custom-resource.yaml
|
||||
```
|
||||
|
||||
ℹ️ Datree's [CRD Extractor](https://github.com/datreeio/CRDs-catalog#crd-extractor) is a utility that can be used instead of this manual process.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
### OpenShift schema Support
|
||||
|
||||
You can validate Openshift manifests using a custom schema location. Set the OpenShift version (v3.10.0-4.1.0) to validate
|
||||
against using `-kubernetes-version`.
|
||||
|
||||
```
|
||||
kubeconform -kubernetes-version 3.8.0 -schema-location 'https://raw.githubusercontent.com/garethr/openshift-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}.json' -summary fixtures/valid.yaml
|
||||
Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
|
||||
```
|
||||
|
||||
## Integrating Kubeconform in the CI
|
||||
|
||||
`Kubeconform` publishes Docker Images to Github's new Container Registry (ghcr.io). These images
|
||||
can be used directly in a Github Action, once logged in using a [_Github Token_](https://github.blog/changelog/2021-03-24-packages-container-registry-now-supports-github_token/).
|
||||
|
||||
### Github Workflow
|
||||
|
||||
Example:
|
||||
```
|
||||
```yaml
|
||||
name: kubeconform
|
||||
on: push
|
||||
jobs:
|
||||
|
|
@ -209,7 +308,7 @@ jobs:
|
|||
- name: login to Github Packages
|
||||
run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker://ghcr.io/yannh/kubeconform:master
|
||||
- uses: docker://ghcr.io/yannh/kubeconform:latest
|
||||
with:
|
||||
entrypoint: '/kubeconform'
|
||||
args: "-summary -output json kubeconfigs/"
|
||||
|
|
@ -221,37 +320,39 @@ bandwidth costs might be applicable. Since bandwidth from Github Packages within
|
|||
Github Container Registry to also be usable for free within Github Actions in the future. If that were not to be the
|
||||
case, I might publish the Docker image to a different platform.
|
||||
|
||||
### Speed comparison with Kubeval
|
||||
### Gitlab-CI
|
||||
|
||||
Running on a pretty large kubeconfigs setup, on a laptop with 4 cores:
|
||||
|
||||
```
|
||||
$ time kubeconform -ignore-missing-schemas -n 8 -summary preview staging production
|
||||
Summary: 50714 resources found in 35139 files - Valid: 27334, Invalid: 0, Errors: 0 Skipped: 23380
|
||||
|
||||
real 0m6,710s
|
||||
user 0m38,701s
|
||||
sys 0m1,161s
|
||||
|
||||
$ time kubeval -d preview,staging,production --ignore-missing-schemas --quiet
|
||||
[... Skipping output]
|
||||
|
||||
real 0m35,336s
|
||||
user 0m0,717s
|
||||
sys 0m1,069s
|
||||
The Kubeconform Docker image can be used in Gitlab-CI. Here is an example of a Gitlab-CI job:
|
||||
|
||||
```yaml
|
||||
lint-kubeconform:
|
||||
stage: validate
|
||||
image:
|
||||
name: ghcr.io/yannh/kubeconform:latest-alpine
|
||||
entrypoint: [""]
|
||||
script:
|
||||
- /kubeconform -summary -output json kubeconfigs/
|
||||
```
|
||||
|
||||
### Using kubeconform as a Go Module
|
||||
See [issue 106](https://github.com/yannh/kubeconform/issues/106) for more details.
|
||||
|
||||
## Helm charts
|
||||
|
||||
There is a 3rd party [repository](https://github.com/jtyr/kubeconform-helm) that
|
||||
allows to use `kubeconform` to test [Helm charts](https://helm.sh) in the form of
|
||||
a [Helm plugin](https://helm.sh/docs/topics/plugins/) and [`pre-commit`
|
||||
hook](https://pre-commit.com/).
|
||||
|
||||
## Using kubeconform as a Go Module
|
||||
|
||||
**Warning**: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
|
||||
Kubeconform contains a package that can be used as a library.
|
||||
`Kubeconform` contains a package that can be used as a library.
|
||||
An example of usage can be found in [examples/main.go](examples/main.go)
|
||||
|
||||
Additional documentation on [pkg.go.dev](https://pkg.go.dev/github.com/yannh/kubeconform/pkg/validator)
|
||||
|
||||
### Credits
|
||||
## Credits
|
||||
|
||||
* @garethr for the [Kubeval](https://github.com/instrumenta/kubeval) and
|
||||
[kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) projects ❤️
|
||||
|
|
|
|||
|
|
@ -19,3 +19,8 @@
|
|||
run bin/kubeconform -schema-location 'fixtures/{{ .ResourceKind }}.json' -schema-location './fixtures/registry/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/test_crd.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Pass when using a cached schema with external references" {
|
||||
run bin/kubeconform -cache fixtures/cache -summary -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
|
|
|||
132
acceptance.bats
132
acceptance.bats
|
|
@ -11,6 +11,12 @@ resetCacheFolder() {
|
|||
[ "${lines[0]}" == 'Usage: bin/kubeconform [OPTION]... [FILE OR FOLDER]...' ]
|
||||
}
|
||||
|
||||
@test "Fail and display help when using an incorrect flag" {
|
||||
run bin/kubeconform -xyz
|
||||
[ "$status" -eq 1 ]
|
||||
[ "${lines[0]}" == 'flag provided but not defined: -xyz' ]
|
||||
}
|
||||
|
||||
@test "Pass when parsing a valid Kubernetes config YAML file" {
|
||||
run bin/kubeconform -summary fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
|
|
@ -30,7 +36,7 @@ resetCacheFolder() {
|
|||
}
|
||||
|
||||
@test "Pass when parsing a valid Kubernetes config JSON file" {
|
||||
run bin/kubeconform -kubernetes-version 1.17.1 -summary fixtures/valid.json
|
||||
run bin/kubeconform -kubernetes-version 1.20.0 -summary fixtures/valid.json
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0" ]
|
||||
}
|
||||
|
|
@ -128,17 +134,32 @@ resetCacheFolder() {
|
|||
}
|
||||
|
||||
@test "Fail when parsing a config with additional properties and strict set" {
|
||||
run bin/kubeconform -strict -kubernetes-version 1.16.0 fixtures/extra_property.yaml
|
||||
run bin/kubeconform -strict -kubernetes-version 1.20.0 fixtures/extra_property.yaml
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "Fail when parsing a config with duplicate properties and strict set" {
|
||||
run bin/kubeconform -strict -kubernetes-version 1.20.0 fixtures/duplicate_property.yaml
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "Pass when parsing a config with duplicate properties and strict NOT set" {
|
||||
run bin/kubeconform -kubernetes-version 1.20.0 fixtures/duplicate_property.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Pass when using a valid, preset -schema-location" {
|
||||
run bin/kubeconform -schema-location default fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Pass when using a valid HTTP -schema-location" {
|
||||
run bin/kubeconform -schema-location 'https://kubernetesjsonschema.dev/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
run bin/kubeconform -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Pass when using schemas with HTTP references" {
|
||||
run bin/kubeconform -summary -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +180,13 @@ resetCacheFolder() {
|
|||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "Fail early when passing a non valid -kubernetes-version" {
|
||||
run bin/kubeconform -kubernetes-version 1.25 fixtures/valid.yaml
|
||||
[ "${lines[0]}" == 'invalid value "1.25" for flag -kubernetes-version: 1.25 is not a valid version. Valid values are "master" (default) or full version x.y.z (e.g. "1.27.2")' ]
|
||||
[[ "${lines[1]}" == "Usage:"* ]]
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "Pass with a valid input when validating against openshift manifests" {
|
||||
run bin/kubeconform -kubernetes-version 3.8.0 -schema-location 'https://raw.githubusercontent.com/garethr/openshift-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}.json' -summary fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
|
|
@ -202,6 +230,18 @@ resetCacheFolder() {
|
|||
[ "$output" = "fixtures/valid.yaml - bob ReplicationController skipped" ]
|
||||
}
|
||||
|
||||
@test "Skip when parsing a resource with a GVK to skip" {
|
||||
run bin/kubeconform -verbose -skip v1/ReplicationController fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "fixtures/valid.yaml - bob ReplicationController skipped" ]
|
||||
}
|
||||
|
||||
@test "Do not skip when parsing a resource with a GVK to skip, where the Kind matches but not the version" {
|
||||
run bin/kubeconform -verbose -skip v2/ReplicationController fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "fixtures/valid.yaml - ReplicationController bob is valid" ]
|
||||
}
|
||||
|
||||
@test "Fail when parsing a resource from a kind to reject" {
|
||||
run bin/kubeconform -verbose -reject ReplicationController fixtures/valid.yaml
|
||||
[ "$status" -eq 1 ]
|
||||
|
|
@ -224,7 +264,7 @@ resetCacheFolder() {
|
|||
|
||||
@test "Fail when no schema found, ensure 404 is not cached on disk" {
|
||||
resetCacheFolder
|
||||
run bin/kubeconform -cache cache -schema-location 'https://raw.githubusercontent.com/garethr/openshift-json-schema/master/doesnotexist.json' fixtures/valid.yaml
|
||||
run bin/kubeconform -cache cache -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/doesnotexist.json' fixtures/valid.yaml
|
||||
[ "$status" -eq 1 ]
|
||||
[ "$output" == 'fixtures/valid.yaml - ReplicationController bob failed validation: could not find schema for ReplicationController' ]
|
||||
[ "`ls cache/ | wc -l`" -eq 0 ]
|
||||
|
|
@ -236,10 +276,92 @@ resetCacheFolder() {
|
|||
[ "$output" = "failed opening cache folder cache_does_not_exist: stat cache_does_not_exist: no such file or directory" ]
|
||||
}
|
||||
|
||||
@test "HTTP references should be cached" {
|
||||
resetCacheFolder
|
||||
run bin/kubeconform -cache cache -summary -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "`ls cache/ | wc -l`" -eq 2 ]
|
||||
}
|
||||
|
||||
@test "Produces correct TAP output" {
|
||||
run bin/kubeconform -output tap fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "${lines[0]}" == 'TAP version 13' ]
|
||||
[ "${lines[1]}" == 'ok 1 - fixtures/valid.yaml (ReplicationController)' ]
|
||||
[ "${lines[1]}" == 'ok 1 - fixtures/valid.yaml (v1/ReplicationController//bob)' ]
|
||||
[ "${lines[2]}" == '1..1' ]
|
||||
}
|
||||
|
||||
@test "Pass when parsing a file containing a List" {
|
||||
run bin/kubeconform -summary fixtures/list_valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "Summary: 6 resources found in 1 file - Valid: 6, Invalid: 0, Errors: 0, Skipped: 0" ]
|
||||
}
|
||||
|
||||
@test "Pass when parsing a List resource from stdin" {
|
||||
run bash -c "cat fixtures/list_valid.yaml | bin/kubeconform -summary"
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = 'Summary: 6 resources found parsing stdin - Valid: 6, Invalid: 0, Errors: 0, Skipped: 0' ]
|
||||
}
|
||||
|
||||
@test "Fail when parsing a List that contains an invalid resource" {
|
||||
run bin/kubeconform -summary fixtures/list_invalid.yaml
|
||||
[ "$status" -eq 1 ]
|
||||
[ "${lines[0]}" == 'fixtures/list_invalid.yaml - ReplicationController bob is invalid: problem validating schema. Check JSON formatting: jsonschema validation failed with '\''https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master-standalone/replicationcontroller-v1.json#'\'' - at '\''/spec/replicas'\'': got string, want null or integer' ]
|
||||
[ "${lines[1]}" == 'Summary: 2 resources found in 1 file - Valid: 1, Invalid: 1, Errors: 0, Skipped: 0' ]
|
||||
}
|
||||
|
||||
@test "Fail when parsing a List that contains an invalid resource from stdin" {
|
||||
run bash -c "cat fixtures/list_invalid.yaml | bin/kubeconform -summary -"
|
||||
[ "$status" -eq 1 ]
|
||||
[ "${lines[0]}" == 'stdin - ReplicationController bob is invalid: problem validating schema. Check JSON formatting: jsonschema validation failed with '\''https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master-standalone/replicationcontroller-v1.json#'\'' - at '\''/spec/replicas'\'': got string, want null or integer' ]
|
||||
[ "${lines[1]}" == 'Summary: 2 resources found parsing stdin - Valid: 1, Invalid: 1, Errors: 0, Skipped: 0' ]
|
||||
}
|
||||
|
||||
@test "Pass on valid, empty list" {
|
||||
run bin/kubeconform -summary fixtures/list_empty_valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = 'Summary: 0 resource found in 1 file - Valid: 0, Invalid: 0, Errors: 0, Skipped: 0' ]
|
||||
}
|
||||
|
||||
@test "Pass on multi-yaml containing one resource, one list" {
|
||||
run bin/kubeconform -summary fixtures/multi_with_list.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = 'Summary: 2 resources found in 1 file - Valid: 2, Invalid: 0, Errors: 0, Skipped: 0' ]
|
||||
}
|
||||
|
||||
@test "Fail when using HTTPS_PROXY with a failing proxy" {
|
||||
# This only tests that the HTTPS_PROXY variable is picked up and that it tries to use it
|
||||
run bash -c "HTTPS_PROXY=127.0.0.1:1234 bin/kubeconform fixtures/valid.yaml"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"proxyconnect tcp: dial tcp 127.0.0.1:1234: connect: connection refused"* ]]
|
||||
}
|
||||
|
||||
@test "Pass when parsing a very large file" {
|
||||
run bin/kubeconform -summary fixtures/valid_large.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = 'Summary: 100000 resources found in 1 file - Valid: 100000, Invalid: 0, Errors: 0, Skipped: 0' ]
|
||||
}
|
||||
|
||||
@test "Pass when parsing a very long stream from stdin" {
|
||||
run bash -c "cat fixtures/valid_large.yaml | bin/kubeconform -summary"
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = 'Summary: 100000 resources found parsing stdin - Valid: 100000, Invalid: 0, Errors: 0, Skipped: 0' ]
|
||||
}
|
||||
|
||||
@test "JUnit output can be validated against the Junit schema definition" {
|
||||
run bash -c "bin/kubeconform -output junit -summary fixtures/valid.yaml > output.xml"
|
||||
[ "$status" -eq 0 ]
|
||||
run xmllint --noout --schema fixtures/junit.xsd output.xml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "passes when trying to use a CRD that does not have the JSONSchema set" {
|
||||
run bash -c "bin/kubeconform -schema-location default -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' fixtures/httpproxy.yaml"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
# https://github.com/yannh/kubeconform/pull/309
|
||||
@test "passes when validating duration not in ISO8601" {
|
||||
run bash -c "./bin/kubeconform -schema-location ./fixtures/grafanaalertrulegroup_v1beta1.json ./fixtures/grafana-alert-rule-group-sample.yaml"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import (
|
|||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
)
|
||||
|
||||
var version = "development"
|
||||
|
||||
func processResults(cancel context.CancelFunc, o output.Output, validationResults <-chan validator.Result, exitOnError bool) <-chan bool {
|
||||
success := true
|
||||
result := make(chan bool)
|
||||
|
|
@ -44,20 +46,11 @@ func processResults(cancel context.CancelFunc, o output.Output, validationResult
|
|||
return result
|
||||
}
|
||||
|
||||
func realMain() int {
|
||||
cfg, out, err := config.FromFlags(os.Args[0], os.Args[1:])
|
||||
if cfg.Help {
|
||||
return 0
|
||||
} else if out != "" {
|
||||
fmt.Println(out)
|
||||
return 1
|
||||
} else if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed parsing command line: %s\n", err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if cfg.CPUProfileFile != "" {
|
||||
f, err := os.Create(cfg.CPUProfileFile)
|
||||
func kubeconform(cfg config.Config) int {
|
||||
var err error
|
||||
cpuProfileFile := os.Getenv("KUBECONFORM_CPUPROFILE_FILE")
|
||||
if cpuProfileFile != "" {
|
||||
f, err := os.Create(cpuProfileFile)
|
||||
if err != nil {
|
||||
log.Fatal("could not create CPU profile: ", err)
|
||||
}
|
||||
|
|
@ -80,17 +73,18 @@ func realMain() int {
|
|||
}
|
||||
|
||||
var o output.Output
|
||||
if o, err = output.New(cfg.OutputFormat, cfg.Summary, useStdin, cfg.Verbose); err != nil {
|
||||
if o, err = output.New(os.Stdout, cfg.OutputFormat, cfg.Summary, useStdin, cfg.Verbose); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
|
||||
v, err := validator.New(cfg.SchemaLocations, validator.Opts{
|
||||
var v validator.Validator
|
||||
v, err = validator.New(cfg.SchemaLocations, validator.Opts{
|
||||
Cache: cfg.Cache,
|
||||
Debug: cfg.Debug,
|
||||
SkipTLS: cfg.SkipTLS,
|
||||
SkipKinds: cfg.SkipKinds,
|
||||
RejectKinds: cfg.RejectKinds,
|
||||
KubernetesVersion: cfg.KubernetesVersion,
|
||||
KubernetesVersion: cfg.KubernetesVersion.String(),
|
||||
Strict: cfg.Strict,
|
||||
IgnoreMissingSchemas: cfg.IgnoreMissingSchemas,
|
||||
})
|
||||
|
|
@ -163,5 +157,27 @@ func realMain() int {
|
|||
}
|
||||
|
||||
func main() {
|
||||
os.Exit(realMain())
|
||||
cfg, out, err := config.FromFlags(os.Args[0], os.Args[1:])
|
||||
if out != "" {
|
||||
o := os.Stderr
|
||||
errCode := 1
|
||||
if cfg.Help {
|
||||
o = os.Stdout
|
||||
errCode = 0
|
||||
}
|
||||
fmt.Fprintln(o, out)
|
||||
os.Exit(errCode)
|
||||
}
|
||||
|
||||
if cfg.Version {
|
||||
fmt.Println(version)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed parsing command line: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(kubeconform(cfg))
|
||||
}
|
||||
|
|
|
|||
46
fixtures/cache/603105c17f981119fec20ae25cfb97ff6dd99114a875ae841ef965d9345667e3
vendored
Normal file
46
fixtures/cache/603105c17f981119fec20ae25cfb97ff6dd99114a875ae841ef965d9345667e3
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"description": "ReplicationController represents the configuration of a replication controller.",
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"enum": [
|
||||
"v1"
|
||||
]
|
||||
},
|
||||
"kind": {
|
||||
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"enum": [
|
||||
"ReplicationController"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master/_definitions.json#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
|
||||
"description": "If the Labels of a ReplicationController are empty, they are defaulted to be the same as the Pod(s) that the replication controller manages. Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata"
|
||||
},
|
||||
"spec": {
|
||||
"$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master/_definitions.json#/definitions/io.k8s.api.core.v1.ReplicationControllerSpec",
|
||||
"description": "Spec defines the specification of the desired behavior of the replication controller. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master/_definitions.json#/definitions/io.k8s.api.core.v1.ReplicationControllerStatus",
|
||||
"description": "Status is the most recently observed status of the replication controller. This data may be out of date by some window of time. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "",
|
||||
"kind": "ReplicationController",
|
||||
"version": "v1"
|
||||
}
|
||||
],
|
||||
"$schema": "http://json-schema.org/schema#"
|
||||
}
|
||||
22067
fixtures/cache/6dc6142c64b944d783a3e783526114da8e747a14a11d8b32dd1f12e2d89a8330
vendored
Normal file
22067
fixtures/cache/6dc6142c64b944d783a3e783526114da8e747a14a11d8b32dd1f12e2d89a8330
vendored
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
18
fixtures/duplicate_property.yaml
Normal file
18
fixtures/duplicate_property.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: nginx-ds
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
k8s-app: nginx-ds
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- image: envoy
|
||||
name: envoy
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
62
fixtures/grafana-alert-rule-group-sample.yaml
Normal file
62
fixtures/grafana-alert-rule-group-sample.yaml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
apiVersion: grafana.integreatly.org/v1beta1
|
||||
kind: GrafanaAlertRuleGroup
|
||||
metadata:
|
||||
name: grafanaalertrulegroup-sample
|
||||
spec:
|
||||
folderRef: test-folder
|
||||
instanceSelector:
|
||||
matchLabels:
|
||||
dashboards: "grafana"
|
||||
interval: 5m
|
||||
rules:
|
||||
- condition: B
|
||||
data:
|
||||
- datasourceUid: grafanacloud-demoinfra-prom
|
||||
model:
|
||||
datasource:
|
||||
type: prometheus
|
||||
uid: grafanacloud-demoinfra-prom
|
||||
editorMode: code
|
||||
expr: weather_temp_c{}
|
||||
instant: true
|
||||
intervalMs: 1000
|
||||
legendFormat: __auto
|
||||
maxDataPoints: 43200
|
||||
range: false
|
||||
refId: A
|
||||
refId: A
|
||||
relativeTimeRange:
|
||||
from: 600
|
||||
- datasourceUid: __expr__
|
||||
model:
|
||||
conditions:
|
||||
- evaluator:
|
||||
params:
|
||||
- 0
|
||||
type: lt
|
||||
operator:
|
||||
type: and
|
||||
query:
|
||||
params:
|
||||
- C
|
||||
reducer:
|
||||
params: []
|
||||
type: last
|
||||
type: query
|
||||
datasource:
|
||||
type: __expr__
|
||||
uid: __expr__
|
||||
expression: A
|
||||
intervalMs: 1000
|
||||
maxDataPoints: 43200
|
||||
refId: B
|
||||
type: threshold
|
||||
refId: B
|
||||
relativeTimeRange:
|
||||
from: 600
|
||||
execErrState: Error
|
||||
for: 5m0s
|
||||
noDataState: NoData
|
||||
title: Temperature below zero
|
||||
uid: 4843de5c-4f8a-4af0-9509-23526a04faf8
|
||||
334
fixtures/grafanaalertrulegroup_v1beta1.json
Normal file
334
fixtures/grafanaalertrulegroup_v1beta1.json
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
{
|
||||
"description": "GrafanaAlertRuleGroup is the Schema for the grafanaalertrulegroups API",
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"description": "APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"description": "Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"spec": {
|
||||
"description": "GrafanaAlertRuleGroupSpec defines the desired state of GrafanaAlertRuleGroup",
|
||||
"properties": {
|
||||
"allowCrossNamespaceImport": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"editable": {
|
||||
"description": "Whether to enable or disable editing of the alert rule group in Grafana UI",
|
||||
"type": "boolean",
|
||||
"x-kubernetes-validations": [
|
||||
{
|
||||
"message": "Value is immutable",
|
||||
"rule": "self == oldSelf"
|
||||
}
|
||||
]
|
||||
},
|
||||
"folderRef": {
|
||||
"description": "Match GrafanaFolders CRs to infer the uid",
|
||||
"type": "string"
|
||||
},
|
||||
"folderUID": {
|
||||
"description": "UID of the folder containing this rule group\nOverrides the FolderSelector",
|
||||
"type": "string"
|
||||
},
|
||||
"instanceSelector": {
|
||||
"description": "selects Grafanas for import",
|
||||
"properties": {
|
||||
"matchExpressions": {
|
||||
"description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.",
|
||||
"items": {
|
||||
"description": "A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.",
|
||||
"properties": {
|
||||
"key": {
|
||||
"description": "key is the label key that the selector applies to.",
|
||||
"type": "string"
|
||||
},
|
||||
"operator": {
|
||||
"description": "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.",
|
||||
"type": "string"
|
||||
},
|
||||
"values": {
|
||||
"description": "values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"operator"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": "array",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"matchLabels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is \"key\", the\noperator is \"In\", and the values array contains only \"value\". The requirements are ANDed.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"x-kubernetes-map-type": "atomic",
|
||||
"x-kubernetes-validations": [
|
||||
{
|
||||
"message": "Value is immutable",
|
||||
"rule": "self == oldSelf"
|
||||
}
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"interval": {
|
||||
"format": "duration",
|
||||
"pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|\u00b5s|ms|s|m|h))+$",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the alert rule group. If not specified, the resource name will be used.",
|
||||
"type": "string"
|
||||
},
|
||||
"resyncPeriod": {
|
||||
"default": "10m",
|
||||
"format": "duration",
|
||||
"pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|\u00b5s|ms|s|m|h))+$",
|
||||
"type": "string"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"description": "AlertRule defines a specific rule to be evaluated. It is based on the upstream model with some k8s specific type mappings",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"datasourceUid": {
|
||||
"description": "Grafana data source unique identifier; it should be '__expr__' for a Server Side Expression operation.",
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"description": "JSON is the raw JSON query and includes the above properties as well as custom properties.",
|
||||
"x-kubernetes-preserve-unknown-fields": true
|
||||
},
|
||||
"queryType": {
|
||||
"description": "QueryType is an optional identifier for the type of query.\nIt can be used to distinguish different types of queries.",
|
||||
"type": "string"
|
||||
},
|
||||
"refId": {
|
||||
"description": "RefID is the unique identifier of the query, set by the frontend call.",
|
||||
"type": "string"
|
||||
},
|
||||
"relativeTimeRange": {
|
||||
"description": "relative time range",
|
||||
"properties": {
|
||||
"from": {
|
||||
"description": "from",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"to": {
|
||||
"description": "to",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"execErrState": {
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error",
|
||||
"KeepLast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"for": {
|
||||
"format": "duration",
|
||||
"pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|\u00b5s|ms|s|m|h))+$",
|
||||
"type": "string"
|
||||
},
|
||||
"isPaused": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"noDataState": {
|
||||
"enum": [
|
||||
"Alerting",
|
||||
"NoData",
|
||||
"OK",
|
||||
"KeepLast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"properties": {
|
||||
"group_by": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"group_interval": {
|
||||
"type": "string"
|
||||
},
|
||||
"group_wait": {
|
||||
"type": "string"
|
||||
},
|
||||
"mute_time_intervals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"receiver": {
|
||||
"type": "string"
|
||||
},
|
||||
"repeat_interval": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"receiver"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"title": {
|
||||
"example": "Always firing",
|
||||
"maxLength": 190,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"uid": {
|
||||
"pattern": "^[a-zA-Z0-9-_]+$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"condition",
|
||||
"data",
|
||||
"execErrState",
|
||||
"for",
|
||||
"noDataState",
|
||||
"title",
|
||||
"uid"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"instanceSelector",
|
||||
"interval",
|
||||
"rules"
|
||||
],
|
||||
"type": "object",
|
||||
"x-kubernetes-validations": [
|
||||
{
|
||||
"message": "Only one of FolderUID or FolderRef can be set",
|
||||
"rule": "(has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) && !(has(self.folderUID)))"
|
||||
}
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"status": {
|
||||
"description": "GrafanaAlertRuleGroupStatus defines the observed state of GrafanaAlertRuleGroup",
|
||||
"properties": {
|
||||
"conditions": {
|
||||
"items": {
|
||||
"description": "Condition contains details for one aspect of the current state of this API Resource.",
|
||||
"properties": {
|
||||
"lastTransitionTime": {
|
||||
"description": "lastTransitionTime is the last time the condition transitioned from one status to another.\nThis should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"description": "message is a human readable message indicating details about the transition.\nThis may be an empty string.",
|
||||
"maxLength": 32768,
|
||||
"type": "string"
|
||||
},
|
||||
"observedGeneration": {
|
||||
"description": "observedGeneration represents the .metadata.generation that the condition was set based upon.\nFor instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date\nwith respect to the current state of the instance.",
|
||||
"format": "int64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"reason": {
|
||||
"description": "reason contains a programmatic identifier indicating the reason for the condition's last transition.\nProducers of specific condition types may define expected values and meanings for this field,\nand whether the values are considered a guaranteed API.\nThe value should be a CamelCase string.\nThis field may not be empty.",
|
||||
"maxLength": 1024,
|
||||
"minLength": 1,
|
||||
"pattern": "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$",
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"description": "status of the condition, one of True, False, Unknown.",
|
||||
"enum": [
|
||||
"True",
|
||||
"False",
|
||||
"Unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "type of condition in CamelCase or in foo.example.com/CamelCase.",
|
||||
"maxLength": 316,
|
||||
"pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"lastTransitionTime",
|
||||
"message",
|
||||
"reason",
|
||||
"status",
|
||||
"type"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"conditions"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
13
fixtures/httpproxy.yaml
Normal file
13
fixtures/httpproxy.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: projectcontour.io/v1
|
||||
kind: HTTPProxy
|
||||
metadata:
|
||||
name: basic
|
||||
spec:
|
||||
virtualhost:
|
||||
fqdn: foo-basic.example.com
|
||||
routes:
|
||||
- conditions:
|
||||
- prefix: /
|
||||
services:
|
||||
- name: s1
|
||||
port: 80
|
||||
118
fixtures/junit.xsd
Normal file
118
fixtures/junit.xsd
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
Source: https://svn.jenkins-ci.org/trunk/hudson/dtkit/dtkit-format/dtkit-junit-model/src/main/resources/com/thalesgroup/dtkit/junit/model/xsd/junit-4.xsd
|
||||
|
||||
This file available under the terms of the MIT License as follows:
|
||||
|
||||
*******************************************************************************
|
||||
* Copyright (c) 2010 Thales Corporate Services SAS *
|
||||
* *
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy *
|
||||
* of this software and associated documentation files (the "Software"), to deal*
|
||||
* in the Software without restriction, including without limitation the rights *
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell *
|
||||
* copies of the Software, and to permit persons to whom the Software is *
|
||||
* furnished to do so, subject to the following conditions: *
|
||||
* *
|
||||
* The above copyright notice and this permission notice shall be included in *
|
||||
* all copies or substantial portions of the Software. *
|
||||
* *
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,*
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN *
|
||||
* THE SOFTWARE. *
|
||||
********************************************************************************
|
||||
-->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
|
||||
<xs:element name="failure">
|
||||
<xs:complexType mixed="true">
|
||||
<xs:attribute name="type" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="message" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="error">
|
||||
<xs:complexType mixed="true">
|
||||
<xs:attribute name="type" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="message" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="properties">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element ref="property" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="property">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="value" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="skipped" type="xs:string"/>
|
||||
<xs:element name="system-err" type="xs:string"/>
|
||||
<xs:element name="system-out" type="xs:string"/>
|
||||
|
||||
<xs:element name="testcase">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="assertions" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="time" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="classname" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="status" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="testsuite">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element ref="properties" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element ref="testcase" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="system-out" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element ref="system-err" minOccurs="0" maxOccurs="1"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="tests" type="xs:string" use="required"/>
|
||||
<xs:attribute name="failures" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="errors" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="time" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="disabled" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="skipped" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="timestamp" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="hostname" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="id" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="package" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="testsuites">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element ref="testsuite" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="time" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="tests" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="failures" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="disabled" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="errors" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
|
||||
</xs:schema>
|
||||
4
fixtures/list_empty_valid.yaml
Normal file
4
fixtures/list_empty_valid.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
items: []
|
||||
26
fixtures/multi_with_list.yaml
Normal file
26
fixtures/multi_with_list.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis-master
|
||||
labels:
|
||||
app: redis
|
||||
tier: backend
|
||||
role: master
|
||||
spec:
|
||||
ports:
|
||||
# the port that this service should serve on
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
selector:
|
||||
app: redis
|
||||
tier: backend
|
||||
role: master
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
items:
|
||||
- apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: b
|
||||
|
|
@ -1,46 +1,34 @@
|
|||
{
|
||||
"apiVersion": "apps/v1beta1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "nginx-deployment",
|
||||
"namespace": "default"
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 2,
|
||||
"template": {
|
||||
"spec": {
|
||||
"affinity": { },
|
||||
"containers": [
|
||||
{
|
||||
"args": [ ],
|
||||
"command": [ ],
|
||||
"env": [ ],
|
||||
"envFrom": [ ],
|
||||
"image": "nginx:1.7.9",
|
||||
"lifecycle": { },
|
||||
"livenessProbe": { },
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80,
|
||||
"name": "http"
|
||||
}
|
||||
],
|
||||
"readinessProbe": { },
|
||||
"resources": { },
|
||||
"securityContext": { },
|
||||
"volumeMounts": [ ]
|
||||
}
|
||||
],
|
||||
"hostMappings": [ ],
|
||||
"imagePullSecrets": [ ],
|
||||
"initContainers": [ ],
|
||||
"nodeSelector": { },
|
||||
"securityContext": { },
|
||||
"tolerations": [ ],
|
||||
"volumes": [ ]
|
||||
}
|
||||
"apiVersion": "v1",
|
||||
"kind": "ReplicationController",
|
||||
"metadata": {
|
||||
"name": "bob"
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 2,
|
||||
"selector": {
|
||||
"app": "nginx"
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"name": "nginx",
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "nginx",
|
||||
"image": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
600003
fixtures/valid_large.yaml
Normal file
600003
fixtures/valid_large.yaml
Normal file
File diff suppressed because it is too large
Load diff
12
go.mod
12
go.mod
|
|
@ -1,10 +1,12 @@
|
|||
module github.com/yannh/kubeconform
|
||||
|
||||
go 1.14
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
|
||||
golang.org/x/text v0.25.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
require github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
|
|
|
|||
76
go.sum
76
go.sum
|
|
@ -1,52 +1,26 @@
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357 h1:Rem2+U35z1QtPQc6r+WolF7yXiefXqDKyk+lN2pE164=
|
||||
github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0 h1:j30noezaCfvNLcdMYSvHLv81DxYRSt1grlpseG67vhU=
|
||||
github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/instrumenta/kubeval v0.0.0-20200515185822-7721cbec724c h1:tF3B96upB2wECZMXZxrAMLiVUgT22sNNxhuOhrcg28s=
|
||||
github.com/instrumenta/kubeval v0.0.0-20200515185822-7721cbec724c/go.mod h1:cD+P/oZrBwOnaIHXrqvKPuN353KPxGomnsXSXf8pFJs=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o=
|
||||
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pelletier/go-toml v0.0.0-20180724185102-c2dbbc24a979/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/spf13/afero v1.1.1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cobra v0.0.0-20180820174524-ff0d02e85550 h1:LB9SHuuXO8gnsHtexOQSpsJrrAHYA35lvHUaE74kznU=
|
||||
github.com/spf13/cobra v0.0.0-20180820174524-ff0d02e85550/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v0.0.0-20180821114517-d929dcbb1086 h1:iU+nPfqRqK8ShQqnpZLv8cZ9oklo6NFUcmX1JT5Rudg=
|
||||
github.com/spf13/pflag v0.0.0-20180821114517-d929dcbb1086/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.1.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
golang.org/x/sys v0.0.0-20180821044426-4ea2f632f6e9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.0.0-20180810153555-6e3c4e7365dd/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
|
|
|||
4
pkg/cache/cache.go
vendored
4
pkg/cache/cache.go
vendored
|
|
@ -1,6 +1,6 @@
|
|||
package cache
|
||||
|
||||
type Cache interface {
|
||||
Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error)
|
||||
Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error
|
||||
Get(key string) (any, error)
|
||||
Set(key string, schema any) error
|
||||
}
|
||||
|
|
|
|||
18
pkg/cache/inmemory.go
vendored
18
pkg/cache/inmemory.go
vendored
|
|
@ -10,26 +10,21 @@ import (
|
|||
// - This cache caches the parsed Schemas
|
||||
type inMemory struct {
|
||||
sync.RWMutex
|
||||
schemas map[string]interface{}
|
||||
schemas map[string]any
|
||||
}
|
||||
|
||||
// New creates a new cache for downloaded schemas
|
||||
func NewInMemoryCache() Cache {
|
||||
return &inMemory{
|
||||
schemas: map[string]interface{}{},
|
||||
schemas: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
func key(resourceKind, resourceAPIVersion, k8sVersion string) string {
|
||||
return fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)
|
||||
}
|
||||
|
||||
// Get retrieves the JSON schema given a resource signature
|
||||
func (c *inMemory) Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error) {
|
||||
k := key(resourceKind, resourceAPIVersion, k8sVersion)
|
||||
func (c *inMemory) Get(key string) (any, error) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
schema, ok := c.schemas[k]
|
||||
schema, ok := c.schemas[key]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("schema not found in in-memory cache")
|
||||
|
|
@ -39,11 +34,10 @@ func (c *inMemory) Get(resourceKind, resourceAPIVersion, k8sVersion string) (int
|
|||
}
|
||||
|
||||
// Set adds a JSON schema to the schema cache
|
||||
func (c *inMemory) Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error {
|
||||
k := key(resourceKind, resourceAPIVersion, k8sVersion)
|
||||
func (c *inMemory) Set(key string, schema any) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.schemas[k] = schema
|
||||
c.schemas[key] = schema
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
24
pkg/cache/ondisk.go
vendored
24
pkg/cache/ondisk.go
vendored
|
|
@ -1,10 +1,9 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
|
@ -22,27 +21,32 @@ func NewOnDiskCache(cache string) Cache {
|
|||
}
|
||||
}
|
||||
|
||||
func cachePath(folder, resourceKind, resourceAPIVersion, k8sVersion string) string {
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)))
|
||||
func cachePath(folder, key string) string {
|
||||
hash := sha256.Sum256([]byte(key))
|
||||
return path.Join(folder, hex.EncodeToString(hash[:]))
|
||||
}
|
||||
|
||||
// Get retrieves the JSON schema given a resource signature
|
||||
func (c *onDisk) Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error) {
|
||||
func (c *onDisk) Get(key string) (any, error) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
f, err := os.Open(cachePath(c.folder, resourceKind, resourceAPIVersion, k8sVersion))
|
||||
f, err := os.Open(cachePath(c.folder, key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return ioutil.ReadAll(f)
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
// Set adds a JSON schema to the schema cache
|
||||
func (c *onDisk) Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error {
|
||||
func (c *onDisk) Set(key string, schema any) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
return ioutil.WriteFile(cachePath(c.folder, resourceKind, resourceAPIVersion, k8sVersion), schema.([]byte), 0644)
|
||||
|
||||
if _, err := os.Stat(cachePath(c.folder, key)); os.IsNotExist(err) {
|
||||
return os.WriteFile(cachePath(c.folder, key), schema.([]byte), 0644)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,28 +4,29 @@ import (
|
|||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Cache string
|
||||
CPUProfileFile string
|
||||
ExitOnError bool
|
||||
Files []string
|
||||
SchemaLocations []string
|
||||
SkipTLS bool
|
||||
SkipKinds map[string]struct{}
|
||||
RejectKinds map[string]struct{}
|
||||
OutputFormat string
|
||||
KubernetesVersion string
|
||||
NumberOfWorkers int
|
||||
Summary bool
|
||||
Strict bool
|
||||
Verbose bool
|
||||
IgnoreMissingSchemas bool
|
||||
IgnoreFilenamePatterns []string
|
||||
Help bool
|
||||
Cache string `yaml:"cache" json:"cache"`
|
||||
Debug bool `yaml:"debug" json:"debug"`
|
||||
ExitOnError bool `yaml:"exitOnError" json:"exitOnError"`
|
||||
Files []string `yaml:"files" json:"files"`
|
||||
Help bool `yaml:"help" json:"help"`
|
||||
IgnoreFilenamePatterns []string `yaml:"ignoreFilenamePatterns" json:"ignoreFilenamePatterns"`
|
||||
IgnoreMissingSchemas bool `yaml:"ignoreMissingSchemas" json:"ignoreMissingSchemas"`
|
||||
KubernetesVersion k8sVersionValue `yaml:"kubernetesVersion" json:"kubernetesVersion"`
|
||||
NumberOfWorkers int `yaml:"numberOfWorkers" json:"numberOfWorkers"`
|
||||
OutputFormat string `yaml:"output" json:"output"`
|
||||
RejectKinds map[string]struct{} `yaml:"reject" json:"reject"`
|
||||
SchemaLocations []string `yaml:"schemaLocations" json:"schemaLocations"`
|
||||
SkipKinds map[string]struct{} `yaml:"skip" json:"skip"`
|
||||
SkipTLS bool `yaml:"insecureSkipTLSVerify" json:"insecureSkipTLSVerify"`
|
||||
Strict bool `yaml:"strict" json:"strict"`
|
||||
Summary bool `yaml:"summary" json:"summary"`
|
||||
Verbose bool `yaml:"verbose" json:"verbose"`
|
||||
Version bool `yaml:"version" json:"version"`
|
||||
}
|
||||
|
||||
type arrayParam []string
|
||||
|
|
@ -39,11 +40,30 @@ func (ap *arrayParam) Set(value string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type k8sVersionValue string
|
||||
|
||||
func (kv *k8sVersionValue) String() string {
|
||||
return string(*kv)
|
||||
}
|
||||
|
||||
func (kv k8sVersionValue) MarshalText() ([]byte, error) {
|
||||
return []byte(kv), nil
|
||||
}
|
||||
|
||||
func (kv *k8sVersionValue) UnmarshalText(v []byte) error {
|
||||
if ok, _ := regexp.MatchString(`^(master|\d+\.\d+\.\d+)$`, string(v)); ok != true {
|
||||
return fmt.Errorf("%v is not a valid version. Valid values are \"master\" (default) or full version x.y.z (e.g. \"1.27.2\")", string(v))
|
||||
}
|
||||
*kv = k8sVersionValue(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitCSV(csvStr string) map[string]struct{} {
|
||||
splitValues := strings.Split(csvStr, ",")
|
||||
valuesMap := map[string]struct{}{}
|
||||
|
||||
for _, kind := range splitValues {
|
||||
kind = strings.TrimSpace(kind)
|
||||
if len(kind) > 0 {
|
||||
valuesMap[kind] = struct{}{}
|
||||
}
|
||||
|
|
@ -56,33 +76,32 @@ func splitCSV(csvStr string) map[string]struct{} {
|
|||
func FromFlags(progName string, args []string) (Config, string, error) {
|
||||
var schemaLocationsParam, ignoreFilenamePatterns arrayParam
|
||||
var skipKindsCSV, rejectKindsCSV string
|
||||
flags := flag.NewFlagSet(progName, flag.ExitOnError)
|
||||
flags := flag.NewFlagSet(progName, flag.ContinueOnError)
|
||||
var buf bytes.Buffer
|
||||
flags.SetOutput(&buf)
|
||||
|
||||
c := Config{}
|
||||
c.Files = []string{}
|
||||
|
||||
flags.StringVar(&c.KubernetesVersion, "kubernetes-version", "1.18.0", "version of Kubernetes to validate against")
|
||||
flags.TextVar(&c.KubernetesVersion, "kubernetes-version", k8sVersionValue("master"), "version of Kubernetes to validate against, e.g.: 1.18.0")
|
||||
flags.Var(&schemaLocationsParam, "schema-location", "override schemas location search path (can be specified multiple times)")
|
||||
flags.StringVar(&skipKindsCSV, "skip", "", "comma-separated list of kinds to ignore")
|
||||
flags.StringVar(&rejectKindsCSV, "reject", "", "comma-separated list of kinds to reject")
|
||||
flags.StringVar(&skipKindsCSV, "skip", "", "comma-separated list of kinds or GVKs to ignore")
|
||||
flags.StringVar(&rejectKindsCSV, "reject", "", "comma-separated list of kinds or GVKs to reject")
|
||||
flags.BoolVar(&c.Debug, "debug", false, "print debug information")
|
||||
flags.BoolVar(&c.ExitOnError, "exit-on-error", false, "immediately stop execution when the first error is encountered")
|
||||
flags.BoolVar(&c.IgnoreMissingSchemas, "ignore-missing-schemas", false, "skip files with missing schemas instead of failing")
|
||||
flags.Var(&ignoreFilenamePatterns, "ignore-filename-pattern", "regular expression specifying paths to ignore (can be specified multiple times)")
|
||||
flags.BoolVar(&c.Summary, "summary", false, "print a summary at the end (ignored for junit output)")
|
||||
flags.IntVar(&c.NumberOfWorkers, "n", 4, "number of goroutines to run concurrently")
|
||||
flags.BoolVar(&c.Strict, "strict", false, "disallow additional properties not in schema")
|
||||
flags.StringVar(&c.OutputFormat, "output", "text", "output format - json, junit, tap, text")
|
||||
flags.BoolVar(&c.Strict, "strict", false, "disallow additional properties not in schema or duplicated keys")
|
||||
flags.StringVar(&c.OutputFormat, "output", "text", "output format - json, junit, pretty, tap, text")
|
||||
flags.BoolVar(&c.Verbose, "verbose", false, "print results for all resources (ignored for tap and junit output)")
|
||||
flags.BoolVar(&c.SkipTLS, "insecure-skip-tls-verify", false, "disable verification of the server's SSL certificate. This will make your HTTPS connections insecure")
|
||||
flags.StringVar(&c.Cache, "cache", "", "cache schemas downloaded via HTTP to this folder")
|
||||
flags.StringVar(&c.CPUProfileFile, "cpu-prof", "", "debug - log CPU profiling to file")
|
||||
flags.BoolVar(&c.Help, "h", false, "show help information")
|
||||
flags.BoolVar(&c.Version, "v", false, "show version information")
|
||||
flags.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [OPTION]... [FILE OR FOLDER]...\n", progName)
|
||||
|
||||
flags.SetOutput(os.Stderr)
|
||||
fmt.Fprintf(&buf, "Usage: %s [OPTION]... [FILE OR FOLDER]...\n", progName)
|
||||
flags.PrintDefaults()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ func TestFromFlags(t *testing.T) {
|
|||
[]string{},
|
||||
Config{
|
||||
Files: []string{},
|
||||
KubernetesVersion: "1.18.0",
|
||||
KubernetesVersion: "master",
|
||||
NumberOfWorkers: 4,
|
||||
OutputFormat: "text",
|
||||
SchemaLocations: nil,
|
||||
|
|
@ -62,7 +62,20 @@ func TestFromFlags(t *testing.T) {
|
|||
Config{
|
||||
Files: []string{},
|
||||
Help: true,
|
||||
KubernetesVersion: "1.18.0",
|
||||
KubernetesVersion: "master",
|
||||
NumberOfWorkers: 4,
|
||||
OutputFormat: "text",
|
||||
SchemaLocations: nil,
|
||||
SkipKinds: map[string]struct{}{},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"-v"},
|
||||
Config{
|
||||
Files: []string{},
|
||||
Version: true,
|
||||
KubernetesVersion: "master",
|
||||
NumberOfWorkers: 4,
|
||||
OutputFormat: "text",
|
||||
SchemaLocations: nil,
|
||||
|
|
@ -74,7 +87,31 @@ func TestFromFlags(t *testing.T) {
|
|||
[]string{"-skip", "a,b,c"},
|
||||
Config{
|
||||
Files: []string{},
|
||||
KubernetesVersion: "1.18.0",
|
||||
KubernetesVersion: "master",
|
||||
NumberOfWorkers: 4,
|
||||
OutputFormat: "text",
|
||||
SchemaLocations: nil,
|
||||
SkipKinds: map[string]struct{}{"a": {}, "b": {}, "c": {}},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"-skip", "a, b, c"},
|
||||
Config{
|
||||
Files: []string{},
|
||||
KubernetesVersion: "master",
|
||||
NumberOfWorkers: 4,
|
||||
OutputFormat: "text",
|
||||
SchemaLocations: nil,
|
||||
SkipKinds: map[string]struct{}{"a": {}, "b": {}, "c": {}},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"-skip", "a,b, c"},
|
||||
Config{
|
||||
Files: []string{},
|
||||
KubernetesVersion: "master",
|
||||
NumberOfWorkers: 4,
|
||||
OutputFormat: "text",
|
||||
SchemaLocations: nil,
|
||||
|
|
@ -86,7 +123,7 @@ func TestFromFlags(t *testing.T) {
|
|||
[]string{"-summary", "-verbose", "file1", "file2"},
|
||||
Config{
|
||||
Files: []string{"file1", "file2"},
|
||||
KubernetesVersion: "1.18.0",
|
||||
KubernetesVersion: "master",
|
||||
NumberOfWorkers: 4,
|
||||
OutputFormat: "text",
|
||||
SchemaLocations: nil,
|
||||
|
|
@ -99,9 +136,10 @@ func TestFromFlags(t *testing.T) {
|
|||
{
|
||||
[]string{"-cache", "cache", "-ignore-missing-schemas", "-kubernetes-version", "1.16.0", "-n", "2", "-output", "json",
|
||||
"-schema-location", "folder", "-schema-location", "anotherfolder", "-skip", "kinda,kindb", "-strict",
|
||||
"-reject", "kindc,kindd", "-summary", "-verbose", "file1", "file2"},
|
||||
"-reject", "kindc,kindd", "-summary", "-debug", "-verbose", "file1", "file2"},
|
||||
Config{
|
||||
Cache: "cache",
|
||||
Debug: true,
|
||||
Files: []string{"file1", "file2"},
|
||||
IgnoreMissingSchemas: true,
|
||||
KubernetesVersion: "1.16.0",
|
||||
|
|
|
|||
65
pkg/loader/file.go
Normal file
65
pkg/loader/file.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
"github.com/yannh/kubeconform/pkg/cache"
|
||||
"io"
|
||||
gourl "net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileLoader loads json file url.
|
||||
type FileLoader struct {
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
func (l FileLoader) Load(url string) (any, error) {
|
||||
path, err := l.ToFile(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
msg := fmt.Sprintf("could not open file %s", path)
|
||||
return nil, NewNotFoundError(errors.New(msg))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
content, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return jsonschema.UnmarshalJSON(bytes.NewReader(content))
|
||||
}
|
||||
|
||||
// ToFile is helper method to convert file url to file path.
|
||||
func (l FileLoader) ToFile(url string) (string, error) {
|
||||
u, err := gourl.Parse(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if u.Scheme != "file" {
|
||||
return url, nil
|
||||
}
|
||||
path := u.Path
|
||||
if runtime.GOOS == "windows" {
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
path = filepath.FromSlash(path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func NewFileLoader() *FileLoader {
|
||||
return &FileLoader{}
|
||||
}
|
||||
85
pkg/loader/http.go
Normal file
85
pkg/loader/http.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
"github.com/yannh/kubeconform/pkg/cache"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HTTPURLLoader struct {
|
||||
client http.Client
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
func (l *HTTPURLLoader) Load(url string) (any, error) {
|
||||
if l.cache != nil {
|
||||
if cached, err := l.cache.Get(url); err == nil {
|
||||
return jsonschema.UnmarshalJSON(bytes.NewReader(cached.([]byte)))
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := l.client.Get(url)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("failed downloading schema at %s: %s", url, err)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
msg := fmt.Sprintf("could not find schema at %s", url)
|
||||
return nil, NewNotFoundError(errors.New(msg))
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg := fmt.Sprintf("error while downloading schema at %s - received HTTP status %d", url, resp.StatusCode)
|
||||
return nil, fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("failed parsing schema from %s: %s", url, err)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
if l.cache != nil {
|
||||
if err = l.cache.Set(url, body); err != nil {
|
||||
return nil, fmt.Errorf("failed to write cache to disk: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
s, err := jsonschema.UnmarshalJSON(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, NewNonJSONResponseError(err)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func NewHTTPURLLoader(skipTLS bool, cache cache.Cache) (*HTTPURLLoader, error) {
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 3 * time.Second,
|
||||
DisableCompression: true,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
|
||||
if skipTLS {
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
// retriable http client
|
||||
retryClient := retryablehttp.NewClient()
|
||||
retryClient.RetryMax = 2
|
||||
retryClient.HTTPClient = &http.Client{Transport: transport}
|
||||
retryClient.Logger = nil
|
||||
|
||||
httpLoader := HTTPURLLoader{client: *retryClient.StandardClient(), cache: cache}
|
||||
return &httpLoader, nil
|
||||
}
|
||||
216
pkg/loader/http_test.go
Normal file
216
pkg/loader/http_test.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockCache struct {
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
func (m *mockCache) Get(key string) (any, error) {
|
||||
if val, ok := m.data[key]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return nil, errors.New("cache miss")
|
||||
}
|
||||
|
||||
func (m *mockCache) Set(key string, value any) error {
|
||||
m.data[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test basic functionality of HTTPURLLoader
|
||||
func TestHTTPURLLoader_Load(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mockResponse string
|
||||
mockStatusCode int
|
||||
cacheEnabled bool
|
||||
expectError bool
|
||||
expectCacheHit bool
|
||||
}{
|
||||
{
|
||||
name: "successful load",
|
||||
mockResponse: `{"type": "object"}`,
|
||||
mockStatusCode: http.StatusOK,
|
||||
cacheEnabled: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "not found error",
|
||||
mockResponse: "",
|
||||
mockStatusCode: http.StatusNotFound,
|
||||
cacheEnabled: false,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "server error",
|
||||
mockResponse: "",
|
||||
mockStatusCode: http.StatusInternalServerError,
|
||||
cacheEnabled: false,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "cache hit",
|
||||
mockResponse: `{"type": "object"}`,
|
||||
mockStatusCode: http.StatusOK,
|
||||
cacheEnabled: true,
|
||||
expectError: false,
|
||||
expectCacheHit: true,
|
||||
},
|
||||
{
|
||||
name: "Partial response from server",
|
||||
mockResponse: `{"type": "objec`,
|
||||
mockStatusCode: http.StatusOK,
|
||||
cacheEnabled: false,
|
||||
expectError: true,
|
||||
expectCacheHit: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Mock HTTP server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create HTTPURLLoader
|
||||
loader := &HTTPURLLoader{
|
||||
client: *server.Client(),
|
||||
cache: nil,
|
||||
}
|
||||
|
||||
if tt.cacheEnabled {
|
||||
loader.cache = &mockCache{data: map[string]any{}}
|
||||
if tt.expectCacheHit {
|
||||
loader.cache.Set(server.URL, []byte(tt.mockResponse))
|
||||
}
|
||||
}
|
||||
|
||||
// Call Load and handle errors
|
||||
res, err := loader.Load(server.URL)
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if res == nil {
|
||||
t.Errorf("expected non-nil result, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test basic functionality of HTTPURLLoader
|
||||
func TestHTTPURLLoader_Load_Retries(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
expectError bool
|
||||
expectCallCount int
|
||||
consecutiveFailures int
|
||||
}{
|
||||
{
|
||||
name: "retries on 503",
|
||||
url: "/503",
|
||||
expectError: false,
|
||||
expectCallCount: 2,
|
||||
consecutiveFailures: 2,
|
||||
},
|
||||
{
|
||||
name: "fails when hitting max retries",
|
||||
url: "/503",
|
||||
expectError: true,
|
||||
expectCallCount: 3,
|
||||
consecutiveFailures: 5,
|
||||
},
|
||||
{
|
||||
name: "retry on connection reset",
|
||||
url: "/simulate-reset",
|
||||
expectError: false,
|
||||
expectCallCount: 2,
|
||||
consecutiveFailures: 1,
|
||||
},
|
||||
{
|
||||
name: "retry on connection reset",
|
||||
url: "/simulate-reset",
|
||||
expectError: true,
|
||||
expectCallCount: 3,
|
||||
consecutiveFailures: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ccMutex := &sync.Mutex{}
|
||||
callCounts := map[string]int{}
|
||||
// Mock HTTP server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ccMutex.Lock()
|
||||
callCounts[r.URL.Path]++
|
||||
callCount := callCounts[r.URL.Path]
|
||||
ccMutex.Unlock()
|
||||
|
||||
switch r.URL.Path {
|
||||
case "/simulate-reset":
|
||||
if callCount <= tt.consecutiveFailures {
|
||||
if hj, ok := w.(http.Hijacker); ok {
|
||||
conn, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
fmt.Printf("Hijacking failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
conn.Close() // Close the connection to simulate a reset
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"type": "object"}`))
|
||||
|
||||
case "/503":
|
||||
s := http.StatusServiceUnavailable
|
||||
if callCount >= tt.consecutiveFailures {
|
||||
s = http.StatusOK
|
||||
}
|
||||
w.WriteHeader(s)
|
||||
w.Write([]byte(`{"type": "object"}`))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create HTTPURLLoader
|
||||
loader, _ := NewHTTPURLLoader(false, nil)
|
||||
|
||||
fullurl := server.URL + tt.url
|
||||
// Call Load and handle errors
|
||||
_, err := loader.Load(fullurl)
|
||||
if tt.expectError && err == nil {
|
||||
t.Error("expected error, got none")
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
ccMutex.Lock()
|
||||
if callCounts[tt.url] != tt.expectCallCount {
|
||||
t.Errorf("expected %d calls, got: %d", tt.expectCallCount, callCounts[tt.url])
|
||||
}
|
||||
ccMutex.Unlock()
|
||||
})
|
||||
}
|
||||
}
|
||||
22
pkg/loader/loaders.go
Normal file
22
pkg/loader/loaders.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package loader
|
||||
|
||||
// NotFoundError is returned when the registry does not contain a schema for the resource
|
||||
type NotFoundError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewNotFoundError(err error) *NotFoundError {
|
||||
return &NotFoundError{err}
|
||||
}
|
||||
func (e *NotFoundError) Error() string { return e.err.Error() }
|
||||
func (e *NotFoundError) Retryable() bool { return false }
|
||||
|
||||
type NonJSONResponseError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewNonJSONResponseError(err error) *NotFoundError {
|
||||
return &NotFoundError{err}
|
||||
}
|
||||
func (e *NonJSONResponseError) Error() string { return e.err.Error() }
|
||||
func (e *NonJSONResponseError) Retryable() bool { return false }
|
||||
|
|
@ -9,12 +9,13 @@ import (
|
|||
)
|
||||
|
||||
type oresult struct {
|
||||
Filename string `json:"filename"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
Filename string `json:"filename"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
ValidationErrors []validator.ValidationError `json:"validationErrors,omitempty"`
|
||||
}
|
||||
|
||||
type jsono struct {
|
||||
|
|
@ -49,11 +50,15 @@ func (o *jsono) Write(result validator.Result) error {
|
|||
o.nValid++
|
||||
case validator.Invalid:
|
||||
st = "statusInvalid"
|
||||
msg = result.Err.Error()
|
||||
if result.Err != nil {
|
||||
msg = result.Err.Error()
|
||||
}
|
||||
o.nInvalid++
|
||||
case validator.Error:
|
||||
st = "statusError"
|
||||
msg = result.Err.Error()
|
||||
if result.Err != nil {
|
||||
msg = result.Err.Error()
|
||||
}
|
||||
o.nErrors++
|
||||
case validator.Skipped:
|
||||
st = "statusSkipped"
|
||||
|
|
@ -63,7 +68,15 @@ func (o *jsono) Write(result validator.Result) error {
|
|||
|
||||
if o.verbose || (result.Status != validator.Valid && result.Status != validator.Skipped && result.Status != validator.Empty) {
|
||||
sig, _ := result.Resource.Signature()
|
||||
o.results = append(o.results, oresult{Filename: result.Resource.Path, Kind: sig.Kind, Name: sig.Name, Version: sig.Version, Status: st, Msg: msg})
|
||||
o.results = append(o.results, oresult{
|
||||
Filename: result.Resource.Path,
|
||||
Kind: sig.Kind,
|
||||
Name: sig.Name,
|
||||
Version: sig.Version,
|
||||
Status: st,
|
||||
Msg: msg,
|
||||
ValidationErrors: result.ValidationErrors,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -93,6 +93,60 @@ metadata:
|
|||
"skipped": 0
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
"a single invalid deployment, verbose, with summary",
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
[]validator.Result{
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Invalid,
|
||||
Err: &validator.ValidationError{
|
||||
Path: "foo",
|
||||
Msg: "bar",
|
||||
},
|
||||
ValidationErrors: []validator.ValidationError{
|
||||
{
|
||||
Path: "foo",
|
||||
Msg: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
`{
|
||||
"resources": [
|
||||
{
|
||||
"filename": "deployment.yml",
|
||||
"kind": "Deployment",
|
||||
"name": "my-app",
|
||||
"version": "apps/v1",
|
||||
"status": "statusInvalid",
|
||||
"msg": "bar",
|
||||
"validationErrors": [
|
||||
{
|
||||
"path": "foo",
|
||||
"msg": "bar"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"valid": 0,
|
||||
"invalid": 1,
|
||||
"errors": 0,
|
||||
"skipped": 0
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
} {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ import (
|
|||
"bufio"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
)
|
||||
|
||||
type TestSuiteCollection struct {
|
||||
|
|
@ -32,22 +33,22 @@ type Property struct {
|
|||
}
|
||||
|
||||
type TestSuite struct {
|
||||
XMLName xml.Name `xml:"testsuite"`
|
||||
Properties []*Property `xml:"properties>property,omitempty"`
|
||||
Cases []TestCase `xml:"testcase"`
|
||||
Name string `xml:"name,attr"`
|
||||
Id int `xml:"id,attr"`
|
||||
Tests int `xml:"tests,attr"`
|
||||
Failures int `xml:"failures,attr"`
|
||||
Errors int `xml:"errors,attr"`
|
||||
Disabled int `xml:"disabled,attr"`
|
||||
Skipped int `xml:"skipped,attr"`
|
||||
XMLName xml.Name `xml:"testsuite"`
|
||||
Cases []TestCase `xml:"testcase"`
|
||||
Name string `xml:"name,attr"`
|
||||
Id int `xml:"id,attr"`
|
||||
Tests int `xml:"tests,attr"`
|
||||
Failures int `xml:"failures,attr"`
|
||||
Errors int `xml:"errors,attr"`
|
||||
Disabled int `xml:"disabled,attr"`
|
||||
Skipped int `xml:"skipped,attr"`
|
||||
}
|
||||
|
||||
type TestCase struct {
|
||||
XMLName xml.Name `xml:"testcase"`
|
||||
Name string `xml:"name,attr"`
|
||||
ClassName string `xml:"classname,attr"`
|
||||
Time int `xml:"time,attr"` // Optional, but for Buildkite support https://github.com/yannh/kubeconform/issues/127
|
||||
Skipped *TestCaseSkipped `xml:"skipped,omitempty"`
|
||||
Error *TestCaseError `xml:"error,omitempty"`
|
||||
Failure []TestCaseError `xml:"failure,omitempty"`
|
||||
|
|
@ -64,13 +65,13 @@ type TestCaseError struct {
|
|||
}
|
||||
|
||||
type junito struct {
|
||||
id int
|
||||
w io.Writer
|
||||
withSummary bool
|
||||
verbose bool
|
||||
suites map[string]*TestSuite // map filename to corresponding suite
|
||||
nValid, nInvalid, nErrors, nSkipped int
|
||||
startTime time.Time
|
||||
id int
|
||||
w io.Writer
|
||||
withSummary bool
|
||||
verbose bool
|
||||
suitesIndex map[string]int // map filename to index in suites
|
||||
suites []TestSuite
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func junitOutput(w io.Writer, withSummary bool, isStdin, verbose bool) Output {
|
||||
|
|
@ -79,34 +80,30 @@ func junitOutput(w io.Writer, withSummary bool, isStdin, verbose bool) Output {
|
|||
w: w,
|
||||
withSummary: withSummary,
|
||||
verbose: verbose,
|
||||
suites: make(map[string]*TestSuite),
|
||||
nValid: 0,
|
||||
nInvalid: 0,
|
||||
nErrors: 0,
|
||||
nSkipped: 0,
|
||||
suites: []TestSuite{},
|
||||
suitesIndex: make(map[string]int),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Write adds a result to the report.
|
||||
func (o *junito) Write(result validator.Result) error {
|
||||
var suite *TestSuite
|
||||
suite, found := o.suites[result.Resource.Path]
|
||||
var suite TestSuite
|
||||
i, found := o.suitesIndex[result.Resource.Path]
|
||||
|
||||
if !found {
|
||||
o.id++
|
||||
suite = &TestSuite{
|
||||
suite = TestSuite{
|
||||
Name: result.Resource.Path,
|
||||
Id: o.id,
|
||||
Tests: 0, Failures: 0, Errors: 0, Disabled: 0, Skipped: 0,
|
||||
Cases: make([]TestCase, 0),
|
||||
Properties: make([]*Property, 0),
|
||||
Cases: make([]TestCase, 0),
|
||||
}
|
||||
o.suites[result.Resource.Path] = suite
|
||||
o.suites = append(o.suites, suite)
|
||||
i = len(o.suites) - 1
|
||||
o.suitesIndex[result.Resource.Path] = i
|
||||
}
|
||||
|
||||
suite.Tests++
|
||||
|
||||
sig, _ := result.Resource.Signature()
|
||||
var objectName string
|
||||
if len(sig.Namespace) > 0 {
|
||||
|
|
@ -119,24 +116,22 @@ func (o *junito) Write(result validator.Result) error {
|
|||
|
||||
switch result.Status {
|
||||
case validator.Valid:
|
||||
o.nValid++
|
||||
case validator.Invalid:
|
||||
suite.Failures++
|
||||
o.nInvalid++
|
||||
o.suites[i].Failures++
|
||||
failure := TestCaseError{Message: result.Err.Error()}
|
||||
testCase.Failure = append(testCase.Failure, failure)
|
||||
case validator.Error:
|
||||
suite.Errors++
|
||||
o.nErrors++
|
||||
o.suites[i].Errors++
|
||||
testCase.Error = &TestCaseError{Message: result.Err.Error()}
|
||||
case validator.Skipped:
|
||||
suite.Skipped++
|
||||
testCase.Skipped = &TestCaseSkipped{}
|
||||
o.nSkipped++
|
||||
o.suites[i].Skipped++
|
||||
case validator.Empty:
|
||||
return nil
|
||||
}
|
||||
|
||||
suite.Cases = append(suite.Cases, testCase)
|
||||
o.suites[i].Tests++
|
||||
o.suites[i].Cases = append(o.suites[i].Cases, testCase)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -145,19 +140,33 @@ func (o *junito) Write(result validator.Result) error {
|
|||
func (o *junito) Flush() error {
|
||||
runtime := time.Now().Sub(o.startTime)
|
||||
|
||||
var suites = make([]TestSuite, 0)
|
||||
totalValid := 0
|
||||
totalInvalid := 0
|
||||
totalErrors := 0
|
||||
totalSkipped := 0
|
||||
|
||||
for _, suite := range o.suites {
|
||||
suites = append(suites, *suite)
|
||||
for _, tCase := range suite.Cases {
|
||||
if tCase.Error != nil {
|
||||
totalErrors++
|
||||
} else if tCase.Skipped != nil {
|
||||
totalSkipped++
|
||||
} else if len(tCase.Failure) > 0 {
|
||||
totalInvalid++
|
||||
} else {
|
||||
totalValid++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root := TestSuiteCollection{
|
||||
Name: "kubeconform",
|
||||
Time: runtime.Seconds(),
|
||||
Tests: o.nValid + o.nInvalid + o.nErrors + o.nSkipped,
|
||||
Failures: o.nInvalid,
|
||||
Errors: o.nErrors,
|
||||
Disabled: o.nSkipped,
|
||||
Suites: suites,
|
||||
Tests: totalValid + totalInvalid + totalErrors + totalSkipped,
|
||||
Failures: totalInvalid,
|
||||
Errors: totalErrors,
|
||||
Disabled: totalSkipped,
|
||||
Suites: o.suites,
|
||||
}
|
||||
|
||||
// 2-space indentation
|
||||
|
|
|
|||
|
|
@ -2,73 +2,37 @@ package output
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/yannh/kubeconform/pkg/resource"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
"github.com/yannh/kubeconform/pkg/resource"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
)
|
||||
|
||||
func isNumeric(s string) bool {
|
||||
matched, _ := regexp.MatchString("^\\d+(\\.\\d+)?$", s)
|
||||
return matched
|
||||
}
|
||||
|
||||
func TestJUnitWrite(t *testing.T) {
|
||||
func TestJunitWrite(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
withSummary bool
|
||||
isStdin bool
|
||||
verbose bool
|
||||
results []validator.Result
|
||||
evaluate func(d *etree.Document)
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
"empty document",
|
||||
false,
|
||||
"an empty result",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
[]validator.Result{},
|
||||
func(d *etree.Document) {
|
||||
root := d.FindElement("/testsuites")
|
||||
if root == nil {
|
||||
t.Errorf("Can't find root testsuite element")
|
||||
return
|
||||
}
|
||||
for _, attr := range root.Attr {
|
||||
switch attr.Key {
|
||||
case "time":
|
||||
case "tests":
|
||||
case "failures":
|
||||
case "disabled":
|
||||
case "errors":
|
||||
if !isNumeric(attr.Value) {
|
||||
t.Errorf("Expected a number for /testsuites/@%s", attr.Key)
|
||||
}
|
||||
continue
|
||||
case "name":
|
||||
if attr.Value != "kubeconform" {
|
||||
t.Errorf("Expected 'kubeconform' for /testsuites/@name")
|
||||
}
|
||||
continue
|
||||
default:
|
||||
t.Errorf("Unknown attribute /testsuites/@%s", attr.Key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
suites := root.SelectElements("testsuite")
|
||||
if len(suites) != 0 {
|
||||
t.Errorf("No testsuite elements should be generated when there are no resources")
|
||||
}
|
||||
},
|
||||
"<testsuites name=\"kubeconform\" time=\"\" tests=\"0\" failures=\"0\" disabled=\"0\" errors=\"0\"></testsuites>\n",
|
||||
},
|
||||
{
|
||||
"a single deployment, verbose, with summary",
|
||||
"a single deployment, summary, no verbose",
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
[]validator.Result{
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
|
|
@ -77,84 +41,111 @@ func TestJUnitWrite(t *testing.T) {
|
|||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
namespace: "my-namespace"
|
||||
`),
|
||||
},
|
||||
Status: validator.Valid,
|
||||
Err: nil,
|
||||
},
|
||||
},
|
||||
func(d *etree.Document) {
|
||||
suites := d.FindElements("//testsuites/testsuite")
|
||||
if len(suites) != 1 {
|
||||
t.Errorf("Expected exactly 1 testsuite element, got %d", len(suites))
|
||||
return
|
||||
}
|
||||
suite := suites[0]
|
||||
for _, attr := range suite.Attr {
|
||||
switch attr.Key {
|
||||
case "name":
|
||||
if attr.Value != "deployment.yml" {
|
||||
t.Errorf("Test suite name should be the resource path")
|
||||
}
|
||||
continue
|
||||
case "id":
|
||||
if attr.Value != "1" {
|
||||
t.Errorf("testsuite/@id should be 1")
|
||||
}
|
||||
continue
|
||||
case "tests":
|
||||
if attr.Value != "1" {
|
||||
t.Errorf("testsuite/@tests should be 1")
|
||||
}
|
||||
continue
|
||||
case "failures":
|
||||
if attr.Value != "0" {
|
||||
t.Errorf("testsuite/@failures should be 0")
|
||||
}
|
||||
continue
|
||||
case "errors":
|
||||
if attr.Value != "0" {
|
||||
t.Errorf("testsuite/@errors should be 0")
|
||||
}
|
||||
continue
|
||||
case "disabled":
|
||||
if attr.Value != "0" {
|
||||
t.Errorf("testsuite/@disabled should be 0")
|
||||
}
|
||||
continue
|
||||
case "skipped":
|
||||
if attr.Value != "0" {
|
||||
t.Errorf("testsuite/@skipped should be 0")
|
||||
}
|
||||
continue
|
||||
default:
|
||||
t.Errorf("Unknown testsuite attribute %s", attr.Key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
testcases := suite.SelectElements("testcase")
|
||||
if len(testcases) != 1 {
|
||||
t.Errorf("Expected exactly 1 testcase, got %d", len(testcases))
|
||||
return
|
||||
}
|
||||
testcase := testcases[0]
|
||||
if testcase.SelectAttrValue("name", "") != "my-namespace/my-app" {
|
||||
t.Errorf("Test case name should be namespace / name")
|
||||
}
|
||||
if testcase.SelectAttrValue("classname", "") != "Deployment@apps/v1" {
|
||||
t.Errorf("Test case class name should be resource kind @ api version")
|
||||
}
|
||||
if testcase.SelectElement("skipped") != nil {
|
||||
t.Errorf("skipped element should not be generated if the kind was not skipped")
|
||||
}
|
||||
if testcase.SelectElement("error") != nil {
|
||||
t.Errorf("error element should not be generated if there was no error")
|
||||
}
|
||||
if len(testcase.SelectElements("failure")) != 0 {
|
||||
t.Errorf("failure elements should not be generated if there were no failures")
|
||||
}
|
||||
"<testsuites name=\"kubeconform\" time=\"\" tests=\"1\" failures=\"0\" disabled=\"0\" errors=\"0\">\n" +
|
||||
" <testsuite name=\"deployment.yml\" id=\"1\" tests=\"1\" failures=\"0\" errors=\"0\" disabled=\"0\" skipped=\"0\">\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\"></testcase>\n" +
|
||||
" </testsuite>\n" +
|
||||
"</testsuites>\n",
|
||||
},
|
||||
{
|
||||
"a deployment, an empty resource, summary, no verbose",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
[]validator.Result{
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Valid,
|
||||
Err: nil,
|
||||
},
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment.yml",
|
||||
Bytes: []byte(`#A single comment`),
|
||||
},
|
||||
Status: validator.Empty,
|
||||
Err: nil,
|
||||
},
|
||||
},
|
||||
"<testsuites name=\"kubeconform\" time=\"\" tests=\"1\" failures=\"0\" disabled=\"0\" errors=\"0\">\n" +
|
||||
" <testsuite name=\"deployment.yml\" id=\"1\" tests=\"1\" failures=\"0\" errors=\"0\" disabled=\"0\" skipped=\"0\">\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\"></testcase>\n" +
|
||||
" </testsuite>\n" +
|
||||
"</testsuites>\n",
|
||||
},
|
||||
{
|
||||
"one error, one invalid",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
[]validator.Result{
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Error,
|
||||
Err: fmt.Errorf("error validating deployment.yml"),
|
||||
},
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment2.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Error,
|
||||
Err: fmt.Errorf("error validating deployment.yml"),
|
||||
},
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment3.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Invalid,
|
||||
Err: fmt.Errorf("deployment3.yml is invalid"),
|
||||
},
|
||||
},
|
||||
"<testsuites name=\"kubeconform\" time=\"\" tests=\"3\" failures=\"1\" disabled=\"0\" errors=\"2\">\n" +
|
||||
" <testsuite name=\"deployment.yml\" id=\"1\" tests=\"1\" failures=\"0\" errors=\"1\" disabled=\"0\" skipped=\"0\">\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\">\n" +
|
||||
" <error message=\"error validating deployment.yml\" type=\"\"></error>\n" +
|
||||
" </testcase>\n" +
|
||||
" </testsuite>\n" +
|
||||
" <testsuite name=\"deployment2.yml\" id=\"2\" tests=\"1\" failures=\"0\" errors=\"1\" disabled=\"0\" skipped=\"0\">\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\">\n" +
|
||||
" <error message=\"error validating deployment.yml\" type=\"\"></error>\n" +
|
||||
" </testcase>\n" +
|
||||
" </testsuite>\n" +
|
||||
" <testsuite name=\"deployment3.yml\" id=\"3\" tests=\"1\" failures=\"1\" errors=\"0\" disabled=\"0\" skipped=\"0\">\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\">\n" +
|
||||
" <failure message=\"deployment3.yml is invalid\" type=\"\"></failure>\n" +
|
||||
" </testcase>\n" +
|
||||
" </testsuite>\n" +
|
||||
"</testsuites>\n",
|
||||
},
|
||||
} {
|
||||
w := new(bytes.Buffer)
|
||||
|
|
@ -165,9 +156,13 @@ metadata:
|
|||
}
|
||||
o.Flush()
|
||||
|
||||
doc := etree.NewDocument()
|
||||
doc.ReadFromString(w.String())
|
||||
// We remove the time, which will be different every time, before the comparison
|
||||
output := w.String()
|
||||
r := regexp.MustCompile(`time="[^"]*"`)
|
||||
output = r.ReplaceAllString(output, "time=\"\"")
|
||||
|
||||
testCase.evaluate(doc)
|
||||
if output != testCase.expect {
|
||||
t.Errorf("%s - expected:, got:\n%s\n%s", testCase.name, testCase.expect, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package output
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"io"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
)
|
||||
|
|
@ -12,19 +12,19 @@ type Output interface {
|
|||
Flush() error
|
||||
}
|
||||
|
||||
func New(outputFormat string, printSummary, isStdin, verbose bool) (Output, error) {
|
||||
w := os.Stdout
|
||||
|
||||
func New(w io.Writer, outputFormat string, printSummary, isStdin, verbose bool) (Output, error) {
|
||||
switch {
|
||||
case outputFormat == "json":
|
||||
return jsonOutput(w, printSummary, isStdin, verbose), nil
|
||||
case outputFormat == "junit":
|
||||
return junitOutput(w, printSummary, isStdin, verbose), nil
|
||||
case outputFormat == "pretty":
|
||||
return prettyOutput(w, printSummary, isStdin, verbose), nil
|
||||
case outputFormat == "tap":
|
||||
return tapOutput(w, printSummary, isStdin, verbose), nil
|
||||
case outputFormat == "text":
|
||||
return textOutput(w, printSummary, isStdin, verbose), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("`outputFormat` must be 'json', 'tap' or 'text'")
|
||||
return nil, fmt.Errorf("'outputFormat' must be 'json', 'junit', 'pretty', 'tap' or 'text'")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
109
pkg/output/pretty.go
Normal file
109
pkg/output/pretty.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
)
|
||||
|
||||
type prettyo struct {
|
||||
sync.Mutex
|
||||
w io.Writer
|
||||
withSummary bool
|
||||
isStdin bool
|
||||
verbose bool
|
||||
files map[string]bool
|
||||
nValid, nInvalid, nErrors, nSkipped int
|
||||
}
|
||||
|
||||
// Text will output the results of the validation as a texto
|
||||
func prettyOutput(w io.Writer, withSummary, isStdin, verbose bool) Output {
|
||||
return &prettyo{
|
||||
w: w,
|
||||
withSummary: withSummary,
|
||||
isStdin: isStdin,
|
||||
verbose: verbose,
|
||||
files: map[string]bool{},
|
||||
nValid: 0,
|
||||
nInvalid: 0,
|
||||
nErrors: 0,
|
||||
nSkipped: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *prettyo) Write(result validator.Result) error {
|
||||
checkmark := "\u2714"
|
||||
multiplicationSign := "\u2716"
|
||||
reset := "\033[0m"
|
||||
cRed := "\033[31m"
|
||||
cGreen := "\033[32m"
|
||||
cYellow := "\033[33m"
|
||||
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
|
||||
var err error
|
||||
|
||||
sig, _ := result.Resource.Signature()
|
||||
|
||||
o.files[result.Resource.Path] = true
|
||||
switch result.Status {
|
||||
case validator.Valid:
|
||||
if o.verbose {
|
||||
fmt.Fprintf(o.w, "%s%s%s %s: %s%s %s is valid%s\n", cGreen, checkmark, reset, result.Resource.Path, cGreen, sig.Kind, sig.Name, reset)
|
||||
}
|
||||
o.nValid++
|
||||
case validator.Invalid:
|
||||
fmt.Fprintf(o.w, "%s%s%s %s: %s%s %s is invalid: %s%s\n", cRed, multiplicationSign, reset, result.Resource.Path, cRed, sig.Kind, sig.Name, result.Err.Error(), reset)
|
||||
|
||||
o.nInvalid++
|
||||
case validator.Error:
|
||||
fmt.Fprintf(o.w, "%s%s%s %s: ", cRed, multiplicationSign, reset, result.Resource.Path)
|
||||
if sig.Kind != "" && sig.Name != "" {
|
||||
fmt.Fprintf(o.w, "%s%s failed validation: %s %s%s\n", cRed, sig.Kind, sig.Name, result.Err.Error(), reset)
|
||||
} else {
|
||||
fmt.Fprintf(o.w, "%sfailed validation: %s %s%s\n", cRed, sig.Name, result.Err.Error(), reset)
|
||||
}
|
||||
o.nErrors++
|
||||
case validator.Skipped:
|
||||
if o.verbose {
|
||||
fmt.Fprintf(o.w, "%s-%s %s: ", cYellow, reset, result.Resource.Path)
|
||||
if sig.Kind != "" && sig.Name != "" {
|
||||
fmt.Fprintf(o.w, "%s%s %s skipped%s\n", cYellow, sig.Kind, sig.Name, reset)
|
||||
} else if sig.Kind != "" {
|
||||
fmt.Fprintf(o.w, "%s%s skipped%s\n", cYellow, sig.Kind, reset)
|
||||
} else {
|
||||
fmt.Fprintf(o.w, "%sskipped%s\n", cYellow, reset)
|
||||
}
|
||||
}
|
||||
o.nSkipped++
|
||||
case validator.Empty: // sent to ensure we count the filename as parsed
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *prettyo) Flush() error {
|
||||
var err error
|
||||
if o.withSummary {
|
||||
nFiles := len(o.files)
|
||||
nResources := o.nValid + o.nInvalid + o.nErrors + o.nSkipped
|
||||
resourcesPlural := ""
|
||||
if nResources > 1 {
|
||||
resourcesPlural = "s"
|
||||
}
|
||||
filesPlural := ""
|
||||
if nFiles > 1 {
|
||||
filesPlural = "s"
|
||||
}
|
||||
if o.isStdin {
|
||||
_, err = fmt.Fprintf(o.w, "Summary: %d resource%s found parsing stdin - Valid: %d, Invalid: %d, Errors: %d, Skipped: %d\n", nResources, resourcesPlural, o.nValid, o.nInvalid, o.nErrors, o.nSkipped)
|
||||
} else {
|
||||
_, err = fmt.Fprintf(o.w, "Summary: %d resource%s found in %d file%s - Valid: %d, Invalid: %d, Errors: %d, Skipped: %d\n", nResources, resourcesPlural, nFiles, filesPlural, o.nValid, o.nInvalid, o.nErrors, o.nSkipped)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
84
pkg/output/pretty_test.go
Normal file
84
pkg/output/pretty_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/resource"
|
||||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
)
|
||||
|
||||
func TestPrettyTextWrite(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
withSummary bool
|
||||
isStdin bool
|
||||
verbose bool
|
||||
results []validator.Result
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
"a single deployment, no summary, no verbose",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
[]validator.Result{},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"a single deployment, summary, no verbose",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
[]validator.Result{
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Valid,
|
||||
Err: nil,
|
||||
},
|
||||
},
|
||||
"Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0\n",
|
||||
},
|
||||
{
|
||||
"a single deployment, verbose, with summary",
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
[]validator.Result{
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Valid,
|
||||
Err: nil,
|
||||
},
|
||||
},
|
||||
"\033[32m✔\033[0m deployment.yml: \033[32mDeployment my-app is valid\033[0m\n" +
|
||||
"Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0\n",
|
||||
},
|
||||
} {
|
||||
w := new(bytes.Buffer)
|
||||
o := prettyOutput(w, testCase.withSummary, testCase.isStdin, testCase.verbose)
|
||||
|
||||
for _, res := range testCase.results {
|
||||
o.Write(res)
|
||||
}
|
||||
o.Flush()
|
||||
|
||||
if w.String() != testCase.expect {
|
||||
t.Errorf("%s - expected, but got:\n%s\n%s\n", testCase.name, testCase.expect, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,11 +40,11 @@ func (o *tapo) Write(res validator.Result) error {
|
|||
switch res.Status {
|
||||
case validator.Valid:
|
||||
sig, _ := res.Resource.Signature()
|
||||
fmt.Fprintf(o.w, "ok %d - %s (%s)\n", o.index, res.Resource.Path, sig.Kind)
|
||||
fmt.Fprintf(o.w, "ok %d - %s (%s)\n", o.index, res.Resource.Path, sig.QualifiedName())
|
||||
|
||||
case validator.Invalid:
|
||||
sig, _ := res.Resource.Signature()
|
||||
fmt.Fprintf(o.w, "not ok %d - %s (%s): %s\n", o.index, res.Resource.Path, sig.Kind, res.Err.Error())
|
||||
fmt.Fprintf(o.w, "not ok %d - %s (%s): %s\n", o.index, res.Resource.Path, sig.QualifiedName(), res.Err.Error())
|
||||
|
||||
case validator.Empty:
|
||||
fmt.Fprintf(o.w, "ok %d - %s (empty)\n", o.index, res.Resource.Path)
|
||||
|
|
@ -53,7 +53,8 @@ func (o *tapo) Write(res validator.Result) error {
|
|||
fmt.Fprintf(o.w, "not ok %d - %s: %s\n", o.index, res.Resource.Path, res.Err.Error())
|
||||
|
||||
case validator.Skipped:
|
||||
fmt.Fprintf(o.w, "ok %d #skip - %s\n", o.index, res.Resource.Path)
|
||||
sig, _ := res.Resource.Signature()
|
||||
fmt.Fprintf(o.w, "ok %d - %s (%s) # skip\n", o.index, res.Resource.Path, sig.QualifiedName())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
)
|
||||
|
||||
func TestTextWrite(t *testing.T) {
|
||||
func TestTapWrite(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
withSummary bool
|
||||
|
|
@ -36,7 +36,7 @@ metadata:
|
|||
Err: nil,
|
||||
},
|
||||
},
|
||||
"TAP version 13\nok 1 - deployment.yml (Deployment)\n1..1\n",
|
||||
"TAP version 13\nok 1 - deployment.yml (apps/v1/Deployment//my-app)\n1..1\n",
|
||||
},
|
||||
{
|
||||
"a single deployment, verbose, with summary",
|
||||
|
|
@ -57,7 +57,7 @@ metadata:
|
|||
Err: nil,
|
||||
},
|
||||
},
|
||||
"TAP version 13\nok 1 - deployment.yml (Deployment)\n1..1\n",
|
||||
"TAP version 13\nok 1 - deployment.yml (apps/v1/Deployment//my-app)\n1..1\n",
|
||||
},
|
||||
} {
|
||||
w := new(bytes.Buffer)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
)
|
||||
|
||||
func TestTapWrite(t *testing.T) {
|
||||
func TestTextWrite(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
withSummary bool
|
||||
|
|
|
|||
|
|
@ -1,97 +1,34 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/cache"
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
)
|
||||
|
||||
type httpGetter interface {
|
||||
Get(url string) (resp *http.Response, err error)
|
||||
}
|
||||
|
||||
// SchemaRegistry is a file repository (local or remote) that contains JSON schemas for Kubernetes resources
|
||||
type SchemaRegistry struct {
|
||||
c httpGetter
|
||||
schemaPathTemplate string
|
||||
cache cache.Cache
|
||||
strict bool
|
||||
debug bool
|
||||
loader jsonschema.URLLoader
|
||||
}
|
||||
|
||||
func newHTTPRegistry(schemaPathTemplate string, cacheFolder string, strict bool, skipTLS bool) (*SchemaRegistry, error) {
|
||||
reghttp := &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 3 * time.Second,
|
||||
DisableCompression: true,
|
||||
}
|
||||
|
||||
if skipTLS {
|
||||
reghttp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
var filecache cache.Cache = nil
|
||||
if cacheFolder != "" {
|
||||
fi, err := os.Stat(cacheFolder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed opening cache folder %s: %s", cacheFolder, err)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, fmt.Errorf("cache folder %s is not a directory", err)
|
||||
}
|
||||
|
||||
filecache = cache.NewOnDiskCache(cacheFolder)
|
||||
}
|
||||
|
||||
func newHTTPRegistry(schemaPathTemplate string, loader jsonschema.URLLoader, strict bool, debug bool) (*SchemaRegistry, error) {
|
||||
return &SchemaRegistry{
|
||||
c: &http.Client{Transport: reghttp},
|
||||
schemaPathTemplate: schemaPathTemplate,
|
||||
cache: filecache,
|
||||
strict: strict,
|
||||
loader: loader,
|
||||
debug: debug,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DownloadSchema downloads the schema for a particular resource from an HTTP server
|
||||
func (r SchemaRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) ([]byte, error) {
|
||||
func (r SchemaRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) (string, any, error) {
|
||||
url, err := schemaPath(r.schemaPathTemplate, resourceKind, resourceAPIVersion, k8sVersion, r.strict)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if r.cache != nil {
|
||||
if b, err := r.cache.Get(resourceKind, resourceAPIVersion, k8sVersion); err == nil {
|
||||
return b.([]byte), nil
|
||||
}
|
||||
}
|
||||
resp, err := r.loader.Load(url)
|
||||
|
||||
resp, err := r.c.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, newNotFoundError(fmt.Errorf("no schema found"))
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("error while downloading schema - received HTTP status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
|
||||
}
|
||||
|
||||
if r.cache != nil {
|
||||
if err := r.cache.Set(resourceKind, resourceAPIVersion, k8sVersion, body); err != nil {
|
||||
return nil, fmt.Errorf("failed writing schema to cache: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return body, nil
|
||||
return url, resp, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockHTTPGetter struct {
|
||||
httpGet func(string) (*http.Response, error)
|
||||
}
|
||||
|
||||
func newMockHTTPGetter(f func(string) (*http.Response, error)) *mockHTTPGetter {
|
||||
return &mockHTTPGetter{
|
||||
httpGet: f,
|
||||
}
|
||||
}
|
||||
func (m mockHTTPGetter) Get(url string) (resp *http.Response, err error) {
|
||||
return m.httpGet(url)
|
||||
}
|
||||
|
||||
func TestDownloadSchema(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
c httpGetter
|
||||
schemaPathTemplate string
|
||||
strict bool
|
||||
resourceKind, resourceAPIVersion, k8sversion string
|
||||
expect []byte
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
"error when downloading",
|
||||
newMockHTTPGetter(func(url string) (resp *http.Response, err error) {
|
||||
return nil, fmt.Errorf("failed downloading from registry")
|
||||
}),
|
||||
"http://kubernetesjson.dev",
|
||||
true,
|
||||
"Deployment",
|
||||
"v1",
|
||||
"1.18.0",
|
||||
nil,
|
||||
fmt.Errorf("failed downloading schema at http://kubernetesjson.dev: failed downloading from registry"),
|
||||
},
|
||||
{
|
||||
"getting 404",
|
||||
newMockHTTPGetter(func(url string) (resp *http.Response, err error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(strings.NewReader("http response mock body")),
|
||||
}, nil
|
||||
}),
|
||||
"http://kubernetesjson.dev",
|
||||
true,
|
||||
"Deployment",
|
||||
"v1",
|
||||
"1.18.0",
|
||||
nil,
|
||||
fmt.Errorf("no schema found"),
|
||||
},
|
||||
{
|
||||
"200",
|
||||
newMockHTTPGetter(func(url string) (resp *http.Response, err error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(strings.NewReader("http response mock body")),
|
||||
}, nil
|
||||
}),
|
||||
"http://kubernetesjson.dev",
|
||||
true,
|
||||
"Deployment",
|
||||
"v1",
|
||||
"1.18.0",
|
||||
[]byte("http response mock body"),
|
||||
nil,
|
||||
},
|
||||
} {
|
||||
reg := SchemaRegistry{
|
||||
c: testCase.c,
|
||||
schemaPathTemplate: testCase.schemaPathTemplate,
|
||||
strict: testCase.strict,
|
||||
}
|
||||
|
||||
res, err := reg.DownloadSchema(testCase.resourceKind, testCase.resourceAPIVersion, testCase.k8sversion)
|
||||
if err == nil || testCase.expectErr == nil {
|
||||
if err != testCase.expectErr {
|
||||
t.Errorf("during test '%s': expected error, got:\n%s\n%s\n", testCase.name, testCase.expectErr, err)
|
||||
}
|
||||
} else if err.Error() != testCase.expectErr.Error() {
|
||||
t.Errorf("during test '%s': expected error, got:\n%s\n%s\n", testCase.name, testCase.expectErr, err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(res, testCase.expect) {
|
||||
t.Errorf("during test '%s': expected %s, got %s", testCase.name, testCase.expect, res)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,43 +1,33 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
)
|
||||
|
||||
type LocalRegistry struct {
|
||||
pathTemplate string
|
||||
strict bool
|
||||
debug bool
|
||||
loader jsonschema.URLLoader
|
||||
}
|
||||
|
||||
// NewLocalSchemas creates a new "registry", that will serve schemas from files, given a list of schema filenames
|
||||
func newLocalRegistry(pathTemplate string, strict bool) (*LocalRegistry, error) {
|
||||
func newLocalRegistry(pathTemplate string, loader jsonschema.URLLoader, strict bool, debug bool) (*LocalRegistry, error) {
|
||||
return &LocalRegistry{
|
||||
pathTemplate,
|
||||
strict,
|
||||
debug,
|
||||
loader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DownloadSchema retrieves the schema from a file for the resource
|
||||
func (r LocalRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) ([]byte, error) {
|
||||
func (r LocalRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) (string, any, error) {
|
||||
schemaFile, err := schemaPath(r.pathTemplate, resourceKind, resourceAPIVersion, k8sVersion, r.strict)
|
||||
if err != nil {
|
||||
return []byte{}, nil
|
||||
}
|
||||
f, err := os.Open(schemaFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, newNotFoundError(fmt.Errorf("no schema found"))
|
||||
}
|
||||
return nil, fmt.Errorf("failed to open schema %s", schemaFile)
|
||||
return schemaFile, []byte{}, nil
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
content, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return content, nil
|
||||
s, err := r.loader.Load(schemaFile)
|
||||
return schemaFile, s, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package registry
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/yannh/kubeconform/pkg/cache"
|
||||
"github.com/yannh/kubeconform/pkg/loader"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
|
@ -13,25 +16,9 @@ type Manifest struct {
|
|||
|
||||
// Registry is an interface that should be implemented by any source of Kubernetes schemas
|
||||
type Registry interface {
|
||||
DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) ([]byte, error)
|
||||
DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) (string, any, error)
|
||||
}
|
||||
|
||||
// Retryable indicates whether an error is a temporary or a permanent failure
|
||||
type Retryable interface {
|
||||
IsNotFound() bool
|
||||
}
|
||||
|
||||
// NotFoundError is returned when the registry does not contain a schema for the resource
|
||||
type NotFoundError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func newNotFoundError(err error) *NotFoundError {
|
||||
return &NotFoundError{err}
|
||||
}
|
||||
func (e *NotFoundError) Error() string { return e.err.Error() }
|
||||
func (e *NotFoundError) Retryable() bool { return false }
|
||||
|
||||
func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict bool) (string, error) {
|
||||
normalisedVersion := k8sVersion
|
||||
if normalisedVersion != "master" {
|
||||
|
|
@ -61,12 +48,14 @@ func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict
|
|||
StrictSuffix string
|
||||
ResourceKind string
|
||||
ResourceAPIVersion string
|
||||
Group string
|
||||
KindSuffix string
|
||||
}{
|
||||
normalisedVersion,
|
||||
strictSuffix,
|
||||
strings.ToLower(resourceKind),
|
||||
groupParts[len(groupParts)-1],
|
||||
groupParts[0],
|
||||
kindSuffix,
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +68,7 @@ func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict
|
|||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func New(schemaLocation string, cache string, strict bool, skipTLS bool) (Registry, error) {
|
||||
func New(schemaLocation string, cacheFolder string, strict bool, skipTLS bool, debug bool) (Registry, error) {
|
||||
if schemaLocation == "default" {
|
||||
schemaLocation = "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json"
|
||||
} else if !strings.HasSuffix(schemaLocation, "json") { // If we dont specify a full templated path, we assume the paths of our fork of kubernetes-json-schema
|
||||
|
|
@ -87,13 +76,31 @@ func New(schemaLocation string, cache string, strict bool, skipTLS bool) (Regist
|
|||
}
|
||||
|
||||
// try to compile the schemaLocation template to ensure it is valid
|
||||
if _, err := schemaPath(schemaLocation, "Deployment", "v1", "1.18.0", true); err != nil {
|
||||
if _, err := schemaPath(schemaLocation, "Deployment", "v1", "master", true); err != nil {
|
||||
return nil, fmt.Errorf("failed initialising schema location registry: %s", err)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(schemaLocation, "http") {
|
||||
return newHTTPRegistry(schemaLocation, cache, strict, skipTLS)
|
||||
var c cache.Cache = nil
|
||||
if cacheFolder != "" {
|
||||
fi, err := os.Stat(cacheFolder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed opening cache folder %s: %s", cacheFolder, err)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, fmt.Errorf("cache folder %s is not a directory", err)
|
||||
}
|
||||
|
||||
c = cache.NewOnDiskCache(cacheFolder)
|
||||
}
|
||||
|
||||
return newLocalRegistry(schemaLocation, strict)
|
||||
if strings.HasPrefix(schemaLocation, "http") {
|
||||
httpLoader, err := loader.NewHTTPURLLoader(skipTLS, c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed creating HTTP loader: %s", err)
|
||||
}
|
||||
return newHTTPRegistry(schemaLocation, httpLoader, strict, debug)
|
||||
}
|
||||
|
||||
fileLoader := loader.NewFileLoader()
|
||||
return newLocalRegistry(schemaLocation, fileLoader, strict, debug)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,14 +88,20 @@ func findFilesInFolders(ctx context.Context, paths []string, ignoreFilePatterns
|
|||
}
|
||||
|
||||
func findResourcesInReader(p string, f io.Reader, resources chan<- Resource, errors chan<- error, buf []byte) {
|
||||
maxBufSize := 256 * 1024 * 1024
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(buf, len(buf))
|
||||
// We start with a buf that is 4MB, scanner will resize it up to 256MB if needed
|
||||
// https://github.com/golang/go/blob/aeea5bacbf79fb945edbeac6cd7630dd70c4d9ce/src/bufio/scan.go#L191
|
||||
scanner.Buffer(buf, maxBufSize)
|
||||
scanner.Split(SplitYAMLDocument)
|
||||
nRes := 0
|
||||
for scanner.Scan() {
|
||||
if len(scanner.Text()) > 0 {
|
||||
resources <- Resource{Path: p, Bytes: []byte(scanner.Text())}
|
||||
nRes++
|
||||
res := Resource{Path: p, Bytes: []byte(scanner.Text())}
|
||||
for _, subres := range res.Resources() {
|
||||
resources <- subres
|
||||
nRes++
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
|
|
@ -124,8 +130,8 @@ func FromFiles(ctx context.Context, paths []string, ignoreFilePatterns []string)
|
|||
files, errors := findFilesInFolders(ctx, paths, ignoreFilePatterns)
|
||||
|
||||
go func() {
|
||||
maxResourceSize := 4 * 1024 * 1024 // 4MB ought to be enough for everybody
|
||||
buf := make([]byte, maxResourceSize) // We reuse this to avoid multiple large memory allocations
|
||||
initialBufSize := 4 * 1024 * 1024 // This is the initial size - scanner will resize if needed
|
||||
buf := make([]byte, initialBufSize) // We reuse the same buffer to avoid multiple large memory allocations
|
||||
|
||||
for p := range files {
|
||||
findResourcesInFile(p, resources, errors, buf)
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@ package resource
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Resource represents a Kubernetes resource within a file
|
||||
type Resource struct {
|
||||
Path string
|
||||
Bytes []byte
|
||||
sig *Signature
|
||||
Path string
|
||||
Bytes []byte
|
||||
sig *Signature // Cache signature parsing
|
||||
sigErr error // Cache potential signature parsing error
|
||||
}
|
||||
|
||||
// Signature is a key representing a Kubernetes resource
|
||||
|
|
@ -18,10 +20,22 @@ type Signature struct {
|
|||
Kind, Version, Namespace, Name string
|
||||
}
|
||||
|
||||
// GroupVersionKind returns a string with the GVK encoding of a resource signature.
|
||||
// This encoding slightly differs from the Kubernetes upstream implementation
|
||||
// in order to be suitable for being used in the kubeconform command-line arguments.
|
||||
func (sig *Signature) GroupVersionKind() string {
|
||||
return fmt.Sprintf("%s/%s", sig.Version, sig.Kind)
|
||||
}
|
||||
|
||||
// QualifiedName returns a string for a signature in the format version/kind/namespace/name
|
||||
func (sig *Signature) QualifiedName() string {
|
||||
return fmt.Sprintf("%s/%s/%s/%s", sig.Version, sig.Kind, sig.Namespace, sig.Name)
|
||||
}
|
||||
|
||||
// Signature computes a signature for a resource, based on its Kind, Version, Namespace & Name
|
||||
func (res *Resource) Signature() (*Signature, error) {
|
||||
if res.sig != nil {
|
||||
return res.sig, nil
|
||||
return res.sig, res.sigErr
|
||||
}
|
||||
|
||||
resource := struct {
|
||||
|
|
@ -44,33 +58,38 @@ func (res *Resource) Signature() (*Signature, error) {
|
|||
res.sig = &Signature{Kind: resource.Kind, Version: resource.APIVersion, Namespace: resource.Metadata.Namespace, Name: name}
|
||||
|
||||
if err != nil { // Exit if there was an error unmarshalling
|
||||
return res.sig, err
|
||||
res.sigErr = err
|
||||
return res.sig, res.sigErr
|
||||
}
|
||||
|
||||
if resource.Kind == "" {
|
||||
return res.sig, fmt.Errorf("missing 'kind' key")
|
||||
if res.sig.Kind == "" {
|
||||
res.sigErr = fmt.Errorf("missing 'kind' key")
|
||||
return res.sig, res.sigErr
|
||||
}
|
||||
|
||||
if resource.APIVersion == "" {
|
||||
return res.sig, fmt.Errorf("missing 'apiVersion' key")
|
||||
if res.sig.Version == "" {
|
||||
res.sigErr = fmt.Errorf("missing 'apiVersion' key")
|
||||
return res.sig, res.sigErr
|
||||
}
|
||||
|
||||
return res.sig, err
|
||||
return res.sig, res.sigErr
|
||||
}
|
||||
|
||||
func (res *Resource) SignatureFromMap(m map[string]interface{}) (*Signature, error) {
|
||||
if res.sig != nil {
|
||||
return res.sig, nil
|
||||
return res.sig, res.sigErr
|
||||
}
|
||||
|
||||
Kind, ok := m["kind"].(string)
|
||||
if !ok {
|
||||
return res.sig, fmt.Errorf("missing 'kind' key")
|
||||
res.sigErr = fmt.Errorf("missing 'kind' key")
|
||||
return res.sig, res.sigErr
|
||||
}
|
||||
|
||||
APIVersion, ok := m["apiVersion"].(string)
|
||||
if !ok {
|
||||
return res.sig, fmt.Errorf("missing 'apiVersion' key")
|
||||
res.sigErr = fmt.Errorf("missing 'apiVersion' key")
|
||||
return res.sig, res.sigErr
|
||||
}
|
||||
|
||||
var name, ns string
|
||||
|
|
@ -87,3 +106,28 @@ func (res *Resource) SignatureFromMap(m map[string]interface{}) (*Signature, err
|
|||
res.sig = &Signature{Kind: Kind, Version: APIVersion, Namespace: ns, Name: name}
|
||||
return res.sig, nil
|
||||
}
|
||||
|
||||
// Resources returns a list of resources if the resource is of type List, a single resource otherwise
|
||||
// See https://github.com/yannh/kubeconform/issues/53
|
||||
func (res *Resource) Resources() []Resource {
|
||||
resources := []Resource{}
|
||||
if s, err := res.Signature(); err == nil && strings.ToLower(s.Kind) == "list" {
|
||||
// A single file of type List
|
||||
list := struct {
|
||||
Version string
|
||||
Kind string
|
||||
Items []interface{}
|
||||
}{}
|
||||
|
||||
yaml.Unmarshal(res.Bytes, &list)
|
||||
|
||||
for _, item := range list.Items {
|
||||
r := Resource{Path: res.Path}
|
||||
r.Bytes, _ = yaml.Marshal(item)
|
||||
resources = append(resources, r)
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
return []Resource{*res}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,3 +86,72 @@ func TestSignatureFromMap(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResources(t *testing.T) {
|
||||
testCases := []struct {
|
||||
b string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
`
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
`,
|
||||
0,
|
||||
},
|
||||
{
|
||||
`
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
Items: []
|
||||
`,
|
||||
0,
|
||||
},
|
||||
{
|
||||
`
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
Items:
|
||||
- apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: "bob"
|
||||
spec:
|
||||
replicas: 2
|
||||
`,
|
||||
1,
|
||||
},
|
||||
{
|
||||
`
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
Items:
|
||||
- apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: "bob"
|
||||
spec:
|
||||
replicas: 2
|
||||
- apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: "Jim"
|
||||
spec:
|
||||
replicas: 2
|
||||
`,
|
||||
2,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
res := resource.Resource{
|
||||
Path: "foo",
|
||||
Bytes: []byte(testCase.b),
|
||||
}
|
||||
|
||||
subres := res.Resources()
|
||||
if len(subres) != testCase.expected {
|
||||
t.Errorf("test %d: expected to find %d resources, found %d", i, testCase.expected, len(subres))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,10 +47,12 @@ func FromStream(ctx context.Context, path string, r io.Reader) (<-chan Resource,
|
|||
errors := make(chan error)
|
||||
|
||||
go func() {
|
||||
const initialBufSize = 4 * 1024 * 1024 // Start with 4MB
|
||||
const maxBufSize = 256 * 1024 * 1024 // Start with 4MB
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
const maxResourceSize = 4 * 1024 * 1024 // 4MB ought to be enough for everybody
|
||||
buf := make([]byte, maxResourceSize)
|
||||
scanner.Buffer(buf, maxResourceSize)
|
||||
buf := make([]byte, initialBufSize)
|
||||
scanner.Buffer(buf, maxBufSize) // Resize up to 256MB
|
||||
scanner.Split(SplitYAMLDocument)
|
||||
|
||||
SCAN:
|
||||
|
|
@ -60,7 +62,10 @@ func FromStream(ctx context.Context, path string, r io.Reader) (<-chan Resource,
|
|||
break SCAN
|
||||
default:
|
||||
}
|
||||
resources <- Resource{Path: path, Bytes: scanner.Bytes()}
|
||||
res := Resource{Path: path, Bytes: scanner.Bytes()}
|
||||
for _, subres := range res.Resources() {
|
||||
resources <- subres
|
||||
}
|
||||
}
|
||||
|
||||
close(resources)
|
||||
|
|
|
|||
|
|
@ -3,15 +3,20 @@ package validator
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
jsonschema "github.com/santhosh-tekuri/jsonschema/v6"
|
||||
"github.com/yannh/kubeconform/pkg/cache"
|
||||
"github.com/yannh/kubeconform/pkg/loader"
|
||||
"github.com/yannh/kubeconform/pkg/registry"
|
||||
"github.com/yannh/kubeconform/pkg/resource"
|
||||
|
||||
"github.com/xeipuuv/gojsonschema"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
"io"
|
||||
"os"
|
||||
"sigs.k8s.io/yaml"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Different types of validation results
|
||||
|
|
@ -26,11 +31,21 @@ const (
|
|||
Empty // resource is empty. Note: is triggered for files starting with a --- separator.
|
||||
)
|
||||
|
||||
type ValidationError struct {
|
||||
Path string `json:"path"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func (ve *ValidationError) Error() string {
|
||||
return ve.Msg
|
||||
}
|
||||
|
||||
// Result contains the details of the result of a resource validation
|
||||
type Result struct {
|
||||
Resource resource.Resource
|
||||
Err error
|
||||
Status Status
|
||||
Resource resource.Resource
|
||||
Err error
|
||||
Status Status
|
||||
ValidationErrors []ValidationError
|
||||
}
|
||||
|
||||
// Validator exposes multiple methods to validate your Kubernetes resources.
|
||||
|
|
@ -43,11 +58,12 @@ type Validator interface {
|
|||
// Opts contains a set of options for the validator.
|
||||
type Opts struct {
|
||||
Cache string // Cache schemas downloaded via HTTP to this folder
|
||||
Debug bool // Debug infos will be print here
|
||||
SkipTLS bool // skip TLS validation when downloading from an HTTP Schema Registry
|
||||
SkipKinds map[string]struct{} // List of resource Kinds to ignore
|
||||
RejectKinds map[string]struct{} // List of resource Kinds to reject
|
||||
KubernetesVersion string // Kubernetes Version - has to match one in https://github.com/instrumenta/kubernetes-json-schema
|
||||
Strict bool // thros an error if resources contain undocumented fields
|
||||
Strict bool // Throws an error if resources contain undocumented fields
|
||||
IgnoreMissingSchemas bool // skip a resource if no schema for that resource can be found
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +77,7 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
|
|||
|
||||
registries := []registry.Registry{}
|
||||
for _, schemaLocation := range schemaLocations {
|
||||
reg, err := registry.New(schemaLocation, opts.Cache, opts.Strict, opts.SkipTLS)
|
||||
reg, err := registry.New(schemaLocation, opts.Cache, opts.Strict, opts.SkipTLS, opts.Debug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -69,7 +85,7 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
|
|||
}
|
||||
|
||||
if opts.KubernetesVersion == "" {
|
||||
opts.KubernetesVersion = "1.18.0"
|
||||
opts.KubernetesVersion = "master"
|
||||
}
|
||||
|
||||
if opts.SkipKinds == nil {
|
||||
|
|
@ -79,30 +95,70 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
|
|||
opts.RejectKinds = map[string]struct{}{}
|
||||
}
|
||||
|
||||
var filecache cache.Cache = nil
|
||||
if opts.Cache != "" {
|
||||
fi, err := os.Stat(opts.Cache)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed opening cache folder %s: %s", opts.Cache, err)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, fmt.Errorf("cache folder %s is not a directory", err)
|
||||
}
|
||||
|
||||
filecache = cache.NewOnDiskCache(opts.Cache)
|
||||
}
|
||||
|
||||
httpLoader, err := loader.NewHTTPURLLoader(false, filecache)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed creating HTTP loader: %s", err)
|
||||
}
|
||||
|
||||
return &v{
|
||||
opts: opts,
|
||||
schemaDownload: downloadSchema,
|
||||
schemaCache: cache.NewInMemoryCache(),
|
||||
regs: registries,
|
||||
opts: opts,
|
||||
schemaDownload: downloadSchema,
|
||||
schemaMemoryCache: cache.NewInMemoryCache(),
|
||||
regs: registries,
|
||||
loader: jsonschema.SchemeURLLoader{
|
||||
"file": jsonschema.FileLoader{},
|
||||
"http": httpLoader,
|
||||
"https": httpLoader,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type v struct {
|
||||
opts Opts
|
||||
schemaCache cache.Cache
|
||||
schemaDownload func(registries []registry.Registry, kind, version, k8sVersion string) (*gojsonschema.Schema, error)
|
||||
regs []registry.Registry
|
||||
opts Opts
|
||||
schemaDiskCache cache.Cache
|
||||
schemaMemoryCache cache.Cache
|
||||
schemaDownload func(registries []registry.Registry, loader jsonschema.SchemeURLLoader, kind, version, k8sVersion string) (*jsonschema.Schema, error)
|
||||
regs []registry.Registry
|
||||
loader jsonschema.SchemeURLLoader
|
||||
}
|
||||
|
||||
func key(resourceKind, resourceAPIVersion, k8sVersion string) string {
|
||||
return fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)
|
||||
}
|
||||
|
||||
// ValidateResource validates a single resource. This allows to validate
|
||||
// large resource streams using multiple Go Routines.
|
||||
func (val *v) ValidateResource(res resource.Resource) Result {
|
||||
// For backward compatibility reasons when determining whether
|
||||
// a resource should be skipped or rejected we use both
|
||||
// the GVK encoding of the resource signatures (the recommended method
|
||||
// for skipping/rejecting resources) and the raw Kind.
|
||||
|
||||
skip := func(signature resource.Signature) bool {
|
||||
if _, ok := val.opts.SkipKinds[signature.GroupVersionKind()]; ok {
|
||||
return ok
|
||||
}
|
||||
_, ok := val.opts.SkipKinds[signature.Kind]
|
||||
return ok
|
||||
}
|
||||
|
||||
reject := func(signature resource.Signature) bool {
|
||||
if _, ok := val.opts.RejectKinds[signature.GroupVersionKind()]; ok {
|
||||
return ok
|
||||
}
|
||||
_, ok := val.opts.RejectKinds[signature.Kind]
|
||||
return ok
|
||||
}
|
||||
|
|
@ -112,7 +168,12 @@ func (val *v) ValidateResource(res resource.Resource) Result {
|
|||
}
|
||||
|
||||
var r map[string]interface{}
|
||||
if err := yaml.Unmarshal(res.Bytes, &r); err != nil {
|
||||
unmarshaller := yaml.Unmarshal
|
||||
if val.opts.Strict {
|
||||
unmarshaller = yaml.UnmarshalStrict
|
||||
}
|
||||
|
||||
if err := unmarshaller(res.Bytes, &r); err != nil {
|
||||
return Result{Resource: res, Status: Error, Err: fmt.Errorf("error unmarshalling resource: %s", err)}
|
||||
}
|
||||
|
||||
|
|
@ -134,23 +195,23 @@ func (val *v) ValidateResource(res resource.Resource) Result {
|
|||
}
|
||||
|
||||
cached := false
|
||||
var schema *gojsonschema.Schema
|
||||
var schema *jsonschema.Schema
|
||||
|
||||
if val.schemaCache != nil {
|
||||
s, err := val.schemaCache.Get(sig.Kind, sig.Version, val.opts.KubernetesVersion)
|
||||
if val.schemaMemoryCache != nil {
|
||||
s, err := val.schemaMemoryCache.Get(key(sig.Kind, sig.Version, val.opts.KubernetesVersion))
|
||||
if err == nil {
|
||||
cached = true
|
||||
schema = s.(*gojsonschema.Schema)
|
||||
schema = s.(*jsonschema.Schema)
|
||||
}
|
||||
}
|
||||
|
||||
if !cached {
|
||||
if schema, err = val.schemaDownload(val.regs, sig.Kind, sig.Version, val.opts.KubernetesVersion); err != nil {
|
||||
if schema, err = val.schemaDownload(val.regs, val.loader, sig.Kind, sig.Version, val.opts.KubernetesVersion); err != nil {
|
||||
return Result{Resource: res, Err: err, Status: Error}
|
||||
}
|
||||
|
||||
if val.schemaCache != nil {
|
||||
val.schemaCache.Set(sig.Kind, sig.Version, val.opts.KubernetesVersion, schema)
|
||||
if val.schemaMemoryCache != nil {
|
||||
val.schemaMemoryCache.Set(key(sig.Kind, sig.Version, val.opts.KubernetesVersion), schema)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,28 +223,33 @@ func (val *v) ValidateResource(res resource.Resource) Result {
|
|||
return Result{Resource: res, Err: fmt.Errorf("could not find schema for %s", sig.Kind), Status: Error}
|
||||
}
|
||||
|
||||
resourceLoader := gojsonschema.NewGoLoader(r)
|
||||
|
||||
results, err := schema.Validate(resourceLoader)
|
||||
err = schema.Validate(r)
|
||||
if err != nil {
|
||||
// This error can only happen if the Object to validate is poorly formed. There's no hope of saving this one
|
||||
return Result{Resource: res, Status: Error, Err: fmt.Errorf("problem validating schema. Check JSON formatting: %s", err)}
|
||||
}
|
||||
validationErrors := []ValidationError{}
|
||||
var e *jsonschema.ValidationError
|
||||
if errors.As(err, &e) {
|
||||
for _, ve := range e.Causes {
|
||||
path := ""
|
||||
for _, f := range ve.InstanceLocation {
|
||||
path = path + "/" + f
|
||||
}
|
||||
validationErrors = append(validationErrors, ValidationError{
|
||||
Path: path,
|
||||
Msg: ve.ErrorKind.LocalizedString(message.NewPrinter(language.English)),
|
||||
})
|
||||
}
|
||||
|
||||
if results.Valid() {
|
||||
return Result{Resource: res, Status: Valid}
|
||||
}
|
||||
|
||||
msg := ""
|
||||
for _, errMsg := range results.Errors() {
|
||||
if msg != "" {
|
||||
msg += " - "
|
||||
}
|
||||
details := errMsg.Details()
|
||||
msg += fmt.Sprintf("For field %s: %s", details["field"].(string), errMsg.Description())
|
||||
|
||||
return Result{
|
||||
Resource: res,
|
||||
Status: Invalid,
|
||||
Err: fmt.Errorf("problem validating schema. Check JSON formatting: %s", strings.ReplaceAll(err.Error(), "\n", " ")),
|
||||
ValidationErrors: validationErrors,
|
||||
}
|
||||
}
|
||||
|
||||
return Result{Resource: res, Status: Invalid, Err: fmt.Errorf("%s", msg)}
|
||||
return Result{Resource: res, Status: Valid}
|
||||
}
|
||||
|
||||
// ValidateWithContext validates resources found in r
|
||||
|
|
@ -194,8 +260,9 @@ func (val *v) ValidateWithContext(ctx context.Context, filename string, r io.Rea
|
|||
for {
|
||||
select {
|
||||
case res, ok := <-resourcesChan:
|
||||
validationResults = append(validationResults, val.ValidateResource(res))
|
||||
if !ok {
|
||||
if ok {
|
||||
validationResults = append(validationResults, val.ValidateResource(res))
|
||||
} else {
|
||||
resourcesChan = nil
|
||||
}
|
||||
|
||||
|
|
@ -218,18 +285,112 @@ func (val *v) Validate(filename string, r io.ReadCloser) []Result {
|
|||
return val.ValidateWithContext(context.Background(), filename, r)
|
||||
}
|
||||
|
||||
func downloadSchema(registries []registry.Registry, kind, version, k8sVersion string) (*gojsonschema.Schema, error) {
|
||||
// validateDuration is a custom validator for the duration format
|
||||
// as JSONSchema only supports the ISO 8601 format, i.e. `PT1H30M`,
|
||||
// while Kubernetes API machinery expects the Go duration format, i.e. `1h30m`
|
||||
// which is commonly used in Kubernetes operators for specifying intervals.
|
||||
// https://github.com/kubernetes/apiextensions-apiserver/blob/1ecd29f74da0639e2e6e3b8fac0c9bfd217e05eb/pkg/apis/apiextensions/v1/types_jsonschema.go#L71
|
||||
func validateDuration(v any) error {
|
||||
// Try validation with the Go duration format
|
||||
if _, err := time.ParseDuration(v.(string)); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// must start with 'P'
|
||||
s, ok = strings.CutPrefix(s, "P")
|
||||
if !ok {
|
||||
return fmt.Errorf("must start with P")
|
||||
}
|
||||
if s == "" {
|
||||
return fmt.Errorf("nothing after P")
|
||||
}
|
||||
|
||||
// dur-week
|
||||
if s, ok := strings.CutSuffix(s, "W"); ok {
|
||||
if s == "" {
|
||||
return fmt.Errorf("no number in week")
|
||||
}
|
||||
for _, ch := range s {
|
||||
if ch < '0' || ch > '9' {
|
||||
return fmt.Errorf("invalid week")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
allUnits := []string{"YMD", "HMS"}
|
||||
for i, s := range strings.Split(s, "T") {
|
||||
if i != 0 && s == "" {
|
||||
return fmt.Errorf("no time elements")
|
||||
}
|
||||
if i >= len(allUnits) {
|
||||
return fmt.Errorf("more than one T")
|
||||
}
|
||||
units := allUnits[i]
|
||||
for s != "" {
|
||||
digitCount := 0
|
||||
for _, ch := range s {
|
||||
if ch >= '0' && ch <= '9' {
|
||||
digitCount++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if digitCount == 0 {
|
||||
return fmt.Errorf("missing number")
|
||||
}
|
||||
s = s[digitCount:]
|
||||
if s == "" {
|
||||
return fmt.Errorf("missing unit")
|
||||
}
|
||||
unit := s[0]
|
||||
j := strings.IndexByte(units, unit)
|
||||
if j == -1 {
|
||||
if strings.IndexByte(allUnits[i], unit) != -1 {
|
||||
return fmt.Errorf("unit %q out of order", unit)
|
||||
}
|
||||
return fmt.Errorf("invalid unit %q", unit)
|
||||
}
|
||||
units = units[j+1:]
|
||||
s = s[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadSchema(registries []registry.Registry, l jsonschema.SchemeURLLoader, kind, version, k8sVersion string) (*jsonschema.Schema, error) {
|
||||
var err error
|
||||
var schemaBytes []byte
|
||||
var path string
|
||||
var s any
|
||||
|
||||
for _, reg := range registries {
|
||||
schemaBytes, err = reg.DownloadSchema(kind, version, k8sVersion)
|
||||
path, s, err = reg.DownloadSchema(kind, version, k8sVersion)
|
||||
if err == nil {
|
||||
return gojsonschema.NewSchema(gojsonschema.NewBytesLoader(schemaBytes))
|
||||
c := jsonschema.NewCompiler()
|
||||
c.RegisterFormat(&jsonschema.Format{"duration", validateDuration})
|
||||
c.UseLoader(l)
|
||||
c.DefaultDraft(jsonschema.Draft4)
|
||||
if err := c.AddResource(path, s); err != nil {
|
||||
continue
|
||||
}
|
||||
schema, err := c.Compile(path)
|
||||
// If we got a non-parseable response, we try the next registry
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
// If we get a 404, we try the next registry, but we exit if we get a real failure
|
||||
if _, notfound := err.(*registry.NotFoundError); notfound {
|
||||
if _, notfound := err.(*loader.NotFoundError); notfound {
|
||||
continue
|
||||
}
|
||||
if _, nonJSONError := err.(*loader.NonJSONResponseError); nonJSONError {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -238,11 +399,3 @@ func downloadSchema(registries []registry.Registry, kind, version, k8sVersion st
|
|||
|
||||
return nil, nil // No schema found - we don't consider it an error, resource will be skipped
|
||||
}
|
||||
|
||||
// From kubeval - let's see if absolutely necessary
|
||||
// func init () {
|
||||
// gojsonschema.FormatCheckers.Add("int64", ValidFormat{})
|
||||
// gojsonschema.FormatCheckers.Add("byte", ValidFormat{})
|
||||
// gojsonschema.FormatCheckers.Add("int32", ValidFormat{})
|
||||
// gojsonschema.FormatCheckers.Add("int-or-string", ValidFormat{})
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,19 +1,41 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
"github.com/yannh/kubeconform/pkg/loader"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/registry"
|
||||
"github.com/yannh/kubeconform/pkg/resource"
|
||||
|
||||
"github.com/xeipuuv/gojsonschema"
|
||||
"github.com/yannh/kubeconform/pkg/resource"
|
||||
)
|
||||
|
||||
type mockRegistry struct {
|
||||
SchemaDownloader func() (string, any, error)
|
||||
}
|
||||
|
||||
func newMockRegistry(f func() (string, any, error)) *mockRegistry {
|
||||
return &mockRegistry{
|
||||
SchemaDownloader: f,
|
||||
}
|
||||
}
|
||||
|
||||
func (m mockRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) (string, any, error) {
|
||||
return m.SchemaDownloader()
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
for i, testCase := range []struct {
|
||||
name string
|
||||
rawResource, schema []byte
|
||||
expect Status
|
||||
name string
|
||||
rawResource, schemaRegistry1 []byte
|
||||
schemaRegistry2 []byte
|
||||
ignoreMissingSchema bool
|
||||
strict bool
|
||||
expectStatus Status
|
||||
expectErrors []ValidationError
|
||||
}{
|
||||
{
|
||||
"valid resource",
|
||||
|
|
@ -44,7 +66,11 @@ lastName: bar
|
|||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"invalid resource",
|
||||
|
|
@ -75,7 +101,16 @@ lastName: bar
|
|||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Invalid,
|
||||
[]ValidationError{
|
||||
{
|
||||
Path: "/firstName",
|
||||
Msg: "got string, want number",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"missing required field",
|
||||
|
|
@ -105,7 +140,70 @@ firstName: foo
|
|||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Invalid,
|
||||
[]ValidationError{
|
||||
{
|
||||
Path: "",
|
||||
Msg: "missing property 'lastName'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"key \"firstName\" already set in map",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
firstName: bar
|
||||
`),
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["firstName"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
true,
|
||||
Error,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"key firstname already set in map in non-strict mode",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
firstName: bar
|
||||
`),
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["firstName"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"resource has invalid yaml",
|
||||
|
|
@ -139,26 +237,397 @@ lastName: bar
|
|||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Error,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"missing schema in 1st registry",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
lastName: bar
|
||||
`),
|
||||
nil,
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"age": {
|
||||
"description": "Age in years",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`),
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"non-json response in 1st registry",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
lastName: bar
|
||||
`),
|
||||
[]byte(`<html>error page</html>`),
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"age": {
|
||||
"description": "Age in years",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`),
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"missing schema in both registries, ignore missing",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
lastName: bar
|
||||
`),
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
false,
|
||||
Skipped,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"missing schema in both registries, do not ignore missing",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
lastName: bar
|
||||
`),
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Error,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"non-json response in both registries, ignore missing",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
lastName: bar
|
||||
`),
|
||||
[]byte(`<html>error page</html>`),
|
||||
[]byte(`<html>error page</html>`),
|
||||
true,
|
||||
false,
|
||||
Skipped,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"non-json response in both registries, do not ignore missing",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
lastName: bar
|
||||
`),
|
||||
[]byte(`<html>error page</html>`),
|
||||
[]byte(`<html>error page</html>`),
|
||||
false,
|
||||
false,
|
||||
Error,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"valid resource duration - go format",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
interval: 5s
|
||||
`),
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"interval": {
|
||||
"type": "string",
|
||||
"format": "duration"
|
||||
}
|
||||
},
|
||||
"required": ["interval"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"valid resource duration - iso8601 format",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
interval: PT1H
|
||||
`),
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"interval": {
|
||||
"type": "string",
|
||||
"format": "duration"
|
||||
}
|
||||
},
|
||||
"required": ["interval"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"invalid resource duration",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
interval: test
|
||||
`),
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"interval": {
|
||||
"type": "string",
|
||||
"format": "duration"
|
||||
}
|
||||
},
|
||||
"required": ["interval"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Invalid,
|
||||
[]ValidationError{{Path: "/interval", Msg: "'test' is not valid duration: must start with P"}},
|
||||
},
|
||||
} {
|
||||
val := v{
|
||||
opts: Opts{
|
||||
SkipKinds: map[string]struct{}{},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
SkipKinds: map[string]struct{}{},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
IgnoreMissingSchemas: testCase.ignoreMissingSchema,
|
||||
Strict: testCase.strict,
|
||||
},
|
||||
schemaCache: nil,
|
||||
schemaDownload: func(_ []registry.Registry, _, _, _ string) (*gojsonschema.Schema, error) {
|
||||
schema, err := gojsonschema.NewSchema(gojsonschema.NewBytesLoader(testCase.schema))
|
||||
if err != nil {
|
||||
t.Errorf("failed parsing test schema")
|
||||
}
|
||||
return schema, nil
|
||||
schemaDownload: downloadSchema,
|
||||
regs: []registry.Registry{
|
||||
newMockRegistry(func() (string, any, error) {
|
||||
if testCase.schemaRegistry1 == nil {
|
||||
return "", nil, loader.NewNotFoundError(nil)
|
||||
}
|
||||
s, err := jsonschema.UnmarshalJSON(bytes.NewReader(testCase.schemaRegistry1))
|
||||
if err != nil {
|
||||
return "", s, loader.NewNonJSONResponseError(err)
|
||||
}
|
||||
return "", s, err
|
||||
}),
|
||||
newMockRegistry(func() (string, any, error) {
|
||||
if testCase.schemaRegistry2 == nil {
|
||||
return "", nil, loader.NewNotFoundError(nil)
|
||||
}
|
||||
s, err := jsonschema.UnmarshalJSON(bytes.NewReader(testCase.schemaRegistry2))
|
||||
if err != nil {
|
||||
return "", s, loader.NewNonJSONResponseError(err)
|
||||
}
|
||||
return "", s, err
|
||||
}),
|
||||
},
|
||||
regs: nil,
|
||||
}
|
||||
if got := val.ValidateResource(resource.Resource{Bytes: testCase.rawResource}); got.Status != testCase.expect {
|
||||
t.Errorf("%d - expected %d, got %d: %s", i, testCase.expect, got.Status, got.Err.Error())
|
||||
got := val.ValidateResource(resource.Resource{Bytes: testCase.rawResource})
|
||||
if got.Status != testCase.expectStatus {
|
||||
if got.Err != nil {
|
||||
t.Errorf("Test '%s' - expected %d, got %d: %s", testCase.name, testCase.expectStatus, got.Status, got.Err.Error())
|
||||
} else {
|
||||
t.Errorf("Test '%s'- %d - expected %d, got %d", testCase.name, i, testCase.expectStatus, got.Status)
|
||||
}
|
||||
}
|
||||
|
||||
if len(got.ValidationErrors) != len(testCase.expectErrors) {
|
||||
t.Errorf("Test '%s': expected ValidationErrors: %+v, got: % v", testCase.name, testCase.expectErrors, got.ValidationErrors)
|
||||
}
|
||||
for i, _ := range testCase.expectErrors {
|
||||
if testCase.expectErrors[i] != got.ValidationErrors[i] {
|
||||
t.Errorf("Test '%s': expected ValidationErrors: %+v, got: % v", testCase.name, testCase.expectErrors, got.ValidationErrors)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationErrors(t *testing.T) {
|
||||
rawResource := []byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
age: not a number
|
||||
`)
|
||||
|
||||
schema := []byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"age": {
|
||||
"description": "Age in years",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`)
|
||||
|
||||
expectedErrors := []ValidationError{
|
||||
{Path: "", Msg: "missing property 'lastName'"},
|
||||
{Path: "/age", Msg: "got string, want integer"},
|
||||
}
|
||||
|
||||
val := v{
|
||||
opts: Opts{
|
||||
SkipKinds: map[string]struct{}{},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
schemaDownload: downloadSchema,
|
||||
regs: []registry.Registry{
|
||||
newMockRegistry(func() (string, any, error) {
|
||||
s, err := jsonschema.UnmarshalJSON(bytes.NewReader(schema))
|
||||
if err != nil {
|
||||
return "", s, loader.NewNonJSONResponseError(err)
|
||||
}
|
||||
return "", s, err
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
got := val.ValidateResource(resource.Resource{Bytes: rawResource})
|
||||
if !reflect.DeepEqual(expectedErrors, got.ValidationErrors) {
|
||||
t.Errorf("Expected %+v, got %+v", expectedErrors, got.ValidationErrors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFile(t *testing.T) {
|
||||
inputData := []byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: bar
|
||||
lastName: qux
|
||||
---
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
`)
|
||||
|
||||
schema := []byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`)
|
||||
|
||||
val := v{
|
||||
opts: Opts{
|
||||
SkipKinds: map[string]struct{}{},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
schemaDownload: downloadSchema,
|
||||
regs: []registry.Registry{
|
||||
newMockRegistry(func() (string, any, error) {
|
||||
s, err := jsonschema.UnmarshalJSON(bytes.NewReader(schema))
|
||||
return "", s, err
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
gotStatuses := []Status{}
|
||||
gotValidationErrors := []ValidationError{}
|
||||
for _, got := range val.Validate("test-file", io.NopCloser(bytes.NewReader(inputData))) {
|
||||
gotStatuses = append(gotStatuses, got.Status)
|
||||
gotValidationErrors = append(gotValidationErrors, got.ValidationErrors...)
|
||||
}
|
||||
|
||||
expectedStatuses := []Status{Valid, Invalid}
|
||||
expectedValidationErrors := []ValidationError{
|
||||
{Path: "", Msg: "missing property 'lastName'"},
|
||||
}
|
||||
if !reflect.DeepEqual(expectedStatuses, gotStatuses) {
|
||||
t.Errorf("Expected %+v, got %+v", expectedStatuses, gotStatuses)
|
||||
}
|
||||
if !reflect.DeepEqual(expectedValidationErrors, gotValidationErrors) {
|
||||
t.Errorf("Expected %+v, got %+v", expectedValidationErrors, gotValidationErrors)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
scripts/Dockerfile.bats
Normal file
7
scripts/Dockerfile.bats
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
FROM python:3.9.7-alpine3.14
|
||||
RUN apk --no-cache add bats
|
||||
COPY requirements.txt /code/
|
||||
RUN pip install -r /code/requirements.txt
|
||||
COPY fixtures /code/fixtures
|
||||
COPY acceptance.bats openapi2jsonschema.py /code/
|
||||
WORKDIR /code
|
||||
14
scripts/Makefile
Normal file
14
scripts/Makefile
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
# This is really early days
|
||||
|
||||
test: build-python-bats docker-test docker-acceptance
|
||||
|
||||
build-python-bats:
|
||||
docker build -t python-bats -f Dockerfile.bats .
|
||||
|
||||
docker-test: build-python-bats
|
||||
docker run --entrypoint "/usr/local/bin/pytest" -t python-bats openapi2jsonschema.py
|
||||
|
||||
docker-acceptance: build-python-bats
|
||||
docker run --entrypoint "/usr/bin/bats" -t python-bats /code/acceptance.bats
|
||||
81
scripts/acceptance.bats
Normal file
81
scripts/acceptance.bats
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
rm -f prometheus_v1.json
|
||||
rm -f prometheus-monitoring-v1.json
|
||||
}
|
||||
|
||||
@test "Should generate expected prometheus resource while disable ssl env var is set" {
|
||||
run export DISABLE_SSL_CERT_VALIDATION=true
|
||||
run ./openapi2jsonschema.py fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus_v1.json" ]
|
||||
run diff prometheus_v1.json ./fixtures/prometheus_v1-expected.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should generate expected prometheus resource from an HTTPS resource while disable ssl env var is set" {
|
||||
run export DISABLE_SSL_CERT_VALIDATION=true
|
||||
run ./openapi2jsonschema.py https://raw.githubusercontent.com/yannh/kubeconform/aebc298047c386116eeeda9b1ada83671a58aedd/scripts/fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus_v1.json" ]
|
||||
run diff prometheus_v1.json ./fixtures/prometheus_v1-expected.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should output filename in {kind}-{group}-{version} format while disable ssl env var is set" {
|
||||
run export DISABLE_SSL_CERT_VALIDATION=true
|
||||
FILENAME_FORMAT='{kind}-{group}-{version}' run ./openapi2jsonschema.py fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus-monitoring-v1.json" ]
|
||||
run diff prometheus-monitoring-v1.json ./fixtures/prometheus_v1-expected.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should set 'additionalProperties: false' at the root while disable ssl env var is set" {
|
||||
run export DISABLE_SSL_CERT_VALIDATION=true
|
||||
DENY_ROOT_ADDITIONAL_PROPERTIES='true' run ./openapi2jsonschema.py fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus_v1.json" ]
|
||||
run diff prometheus_v1.json ./fixtures/prometheus_v1-denyRootAdditionalProperties.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should generate expected prometheus resource" {
|
||||
run ./openapi2jsonschema.py fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus_v1.json" ]
|
||||
run diff prometheus_v1.json ./fixtures/prometheus_v1-expected.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should generate expected prometheus resource from an HTTP resource" {
|
||||
run ./openapi2jsonschema.py https://raw.githubusercontent.com/yannh/kubeconform/aebc298047c386116eeeda9b1ada83671a58aedd/scripts/fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus_v1.json" ]
|
||||
run diff prometheus_v1.json ./fixtures/prometheus_v1-expected.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should output filename in {kind}-{group}-{version} format" {
|
||||
FILENAME_FORMAT='{kind}-{group}-{version}' run ./openapi2jsonschema.py fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus-monitoring-v1.json" ]
|
||||
run diff prometheus-monitoring-v1.json ./fixtures/prometheus_v1-expected.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should set 'additionalProperties: false' at the root" {
|
||||
DENY_ROOT_ADDITIONAL_PROPERTIES='true' run ./openapi2jsonschema.py fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus_v1.json" ]
|
||||
run diff prometheus_v1.json ./fixtures/prometheus_v1-denyRootAdditionalProperties.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should output an error if no file is passed" {
|
||||
run ./openapi2jsonschema.py
|
||||
[ "$status" -eq 1 ]
|
||||
[ "${lines[0]}" == 'Missing FILE parameter.' ]
|
||||
[ "${lines[1]}" == 'Usage: ./openapi2jsonschema.py [FILE]' ]
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
7098
scripts/fixtures/prometheus_v1-denyRootAdditionalProperties.json
Normal file
7098
scripts/fixtures/prometheus_v1-denyRootAdditionalProperties.json
Normal file
File diff suppressed because it is too large
Load diff
7097
scripts/fixtures/prometheus_v1-expected.json
Normal file
7097
scripts/fixtures/prometheus_v1-expected.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Derived from https://github.com/instrumenta/openapi2jsonschema
|
||||
import yaml
|
||||
|
|
@ -6,37 +6,44 @@ import json
|
|||
import sys
|
||||
import os
|
||||
import urllib.request
|
||||
if 'DISABLE_SSL_CERT_VALIDATION' in os.environ:
|
||||
import ssl
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
def iteritems(d):
|
||||
if hasattr(dict, "iteritems"):
|
||||
return d.iteritems()
|
||||
else:
|
||||
return iter(d.items())
|
||||
def test_additional_properties():
|
||||
for test in iter([{
|
||||
"input": {"something": {"properties": {}}},
|
||||
"expect": {'something': {'properties': {}, "additionalProperties": False}}
|
||||
},{
|
||||
"input": {"something": {"somethingelse": {}}},
|
||||
"expect": {'something': {'somethingelse': {}}}
|
||||
}]):
|
||||
assert additional_properties(test["input"]) == test["expect"]
|
||||
|
||||
|
||||
def additional_properties(data):
|
||||
def additional_properties(data, skip=False):
|
||||
"This recreates the behaviour of kubectl at https://github.com/kubernetes/kubernetes/blob/225b9119d6a8f03fcbe3cc3d590c261965d928d0/pkg/kubectl/validation/schema.go#L312"
|
||||
new = {}
|
||||
try:
|
||||
for k, v in iteritems(data):
|
||||
new_v = v
|
||||
if isinstance(v, dict):
|
||||
if "properties" in v:
|
||||
if "additionalProperties" not in v:
|
||||
v["additionalProperties"] = False
|
||||
new_v = additional_properties(v)
|
||||
else:
|
||||
new_v = v
|
||||
new[k] = new_v
|
||||
return new
|
||||
except AttributeError:
|
||||
return data
|
||||
if isinstance(data, dict):
|
||||
if "properties" in data and not skip:
|
||||
if "additionalProperties" not in data:
|
||||
data["additionalProperties"] = False
|
||||
for _, v in data.items():
|
||||
additional_properties(v)
|
||||
return data
|
||||
|
||||
def test_replace_int_or_string():
|
||||
for test in iter([{
|
||||
"input": {"something": {"format": "int-or-string"}},
|
||||
"expect": {'something': {'oneOf': [{'type': 'string'}, {'type': 'integer'}]}}
|
||||
},{
|
||||
"input": {"something": {"format": "string"}},
|
||||
"expect": {"something": {"format": "string"}},
|
||||
}]):
|
||||
assert replace_int_or_string(test["input"]) == test["expect"]
|
||||
|
||||
def replace_int_or_string(data):
|
||||
new = {}
|
||||
try:
|
||||
for k, v in iteritems(data):
|
||||
for k, v in iter(data.items()):
|
||||
new_v = v
|
||||
if isinstance(v, dict):
|
||||
if "format" in v and v["format"] == "int-or-string":
|
||||
|
|
@ -54,11 +61,10 @@ def replace_int_or_string(data):
|
|||
except AttributeError:
|
||||
return data
|
||||
|
||||
|
||||
def allow_null_optional_fields(data, parent=None, grand_parent=None, key=None):
|
||||
new = {}
|
||||
try:
|
||||
for k, v in iteritems(data):
|
||||
for k, v in iter(data.items()):
|
||||
new_v = v
|
||||
if isinstance(v, dict):
|
||||
new_v = allow_null_optional_fields(v, data, parent, k)
|
||||
|
|
@ -69,7 +75,7 @@ def allow_null_optional_fields(data, parent=None, grand_parent=None, key=None):
|
|||
elif isinstance(v, str):
|
||||
is_non_null_type = k == "type" and v != "null"
|
||||
has_required_fields = grand_parent and "required" in grand_parent
|
||||
if is_non_null_type and not has_required_field:
|
||||
if is_non_null_type and not has_required_fields:
|
||||
new_v = [v, "null"]
|
||||
new[k] = new_v
|
||||
return new
|
||||
|
|
@ -91,55 +97,89 @@ def append_no_duplicates(obj, key, value):
|
|||
def write_schema_file(schema, filename):
|
||||
schemaJSON = ""
|
||||
|
||||
schema = additional_properties(schema)
|
||||
schema = additional_properties(schema, skip=not os.getenv("DENY_ROOT_ADDITIONAL_PROPERTIES"))
|
||||
schema = replace_int_or_string(schema)
|
||||
schemaJSON = json.dumps(schema, indent=2)
|
||||
|
||||
# Dealing with user input here..
|
||||
filename = os.path.basename(filename)
|
||||
f = open(filename, "w")
|
||||
f.write(schemaJSON)
|
||||
print(schemaJSON, file=f)
|
||||
f.close()
|
||||
print("JSON schema written to {filename}".format(filename=filename))
|
||||
|
||||
|
||||
if len(sys.argv) == 0:
|
||||
print("missing file")
|
||||
exit(1)
|
||||
def construct_value(load, node):
|
||||
# Handle nodes that start with '='
|
||||
# See https://github.com/yaml/pyyaml/issues/89
|
||||
if not isinstance(node, yaml.ScalarNode):
|
||||
raise yaml.constructor.ConstructorError(
|
||||
"while constructing a value",
|
||||
node.start_mark,
|
||||
"expected a scalar, but found %s" % node.id, node.start_mark
|
||||
)
|
||||
yield str(node.value)
|
||||
|
||||
for crdFile in sys.argv[1:]:
|
||||
if crdFile.startswith("http"):
|
||||
f = urllib.request.urlopen(crdFile)
|
||||
else:
|
||||
f = open(crdFile)
|
||||
with f:
|
||||
for y in yaml.load_all(f, Loader=yaml.SafeLoader):
|
||||
if "kind" not in y:
|
||||
continue
|
||||
if y["kind"] != "CustomResourceDefinition":
|
||||
continue
|
||||
|
||||
filename_format = os.getenv("FILENAME_FORMAT", "{kind}_{version}")
|
||||
filename = ""
|
||||
if "spec" in y and "validation" in y["spec"] and "openAPIV3Schema" in y["spec"]["validation"]:
|
||||
filename = filename_format.format(
|
||||
kind=y["spec"]["names"]["kind"],
|
||||
group=y["spec"]["group"].split(".")[0],
|
||||
version=y["spec"]["version"],
|
||||
).lower() + ".json"
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print('Missing FILE parameter.\nUsage: %s [FILE]' % sys.argv[0])
|
||||
exit(1)
|
||||
|
||||
schema = y["spec"]["validation"]["openAPIV3Schema"]
|
||||
write_schema_file(schema, filename)
|
||||
elif "spec" in y and "versions" in y["spec"]:
|
||||
for version in y["spec"]["versions"]:
|
||||
if "schema" in version and "openAPIV3Schema" in version["schema"]:
|
||||
filename = filename_format.format(
|
||||
kind=y["spec"]["names"]["kind"],
|
||||
group=y["spec"]["group"].split(".")[0],
|
||||
version=version["name"],
|
||||
).lower() + ".json"
|
||||
for crdFile in sys.argv[1:]:
|
||||
if crdFile.startswith("http"):
|
||||
f = urllib.request.urlopen(crdFile)
|
||||
else:
|
||||
f = open(crdFile)
|
||||
with f:
|
||||
defs = []
|
||||
yaml.SafeLoader.add_constructor(u'tag:yaml.org,2002:value', construct_value)
|
||||
for y in yaml.load_all(f, Loader=yaml.SafeLoader):
|
||||
if y is None:
|
||||
continue
|
||||
if "items" in y:
|
||||
defs.extend(y["items"])
|
||||
if "kind" not in y:
|
||||
continue
|
||||
if y["kind"] != "CustomResourceDefinition":
|
||||
continue
|
||||
else:
|
||||
defs.append(y)
|
||||
|
||||
schema = version["schema"]["openAPIV3Schema"]
|
||||
write_schema_file(schema, filename)
|
||||
for y in defs:
|
||||
filename_format = os.getenv("FILENAME_FORMAT", "{kind}_{version}")
|
||||
filename = ""
|
||||
if "spec" in y and "versions" in y["spec"] and y["spec"]["versions"]:
|
||||
for version in y["spec"]["versions"]:
|
||||
if "schema" in version and "openAPIV3Schema" in version["schema"]:
|
||||
filename = filename_format.format(
|
||||
kind=y["spec"]["names"]["kind"],
|
||||
group=y["spec"]["group"].split(".")[0],
|
||||
fullgroup=y["spec"]["group"],
|
||||
version=version["name"],
|
||||
).lower() + ".json"
|
||||
|
||||
exit(0)
|
||||
schema = version["schema"]["openAPIV3Schema"]
|
||||
write_schema_file(schema, filename)
|
||||
elif "validation" in y["spec"] and "openAPIV3Schema" in y["spec"]["validation"]:
|
||||
filename = filename_format.format(
|
||||
kind=y["spec"]["names"]["kind"],
|
||||
group=y["spec"]["group"].split(".")[0],
|
||||
fullgroup=y["spec"]["group"],
|
||||
version=version["name"],
|
||||
).lower() + ".json"
|
||||
|
||||
schema = y["spec"]["validation"]["openAPIV3Schema"]
|
||||
write_schema_file(schema, filename)
|
||||
elif "spec" in y and "validation" in y["spec"] and "openAPIV3Schema" in y["spec"]["validation"]:
|
||||
filename = filename_format.format(
|
||||
kind=y["spec"]["names"]["kind"],
|
||||
group=y["spec"]["group"].split(".")[0],
|
||||
fullgroup=y["spec"]["group"],
|
||||
version=y["spec"]["version"],
|
||||
).lower() + ".json"
|
||||
|
||||
schema = y["spec"]["validation"]["openAPIV3Schema"]
|
||||
write_schema_file(schema, filename)
|
||||
|
||||
exit(0)
|
||||
|
|
|
|||
2
scripts/requirements.txt
Normal file
2
scripts/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pyyaml
|
||||
pytest
|
||||
6
site/archetypes/default.md
Normal file
6
site/archetypes/default.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
|
||||
6
site/config.toml
Normal file
6
site/config.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
baseURL = 'http://kubeconform.mandragor.org/'
|
||||
languageCode = 'en-us'
|
||||
title = 'Kubeconform - Fast Kubernetes manifests validation!'
|
||||
theme = 'kubeconform'
|
||||
contentDir = "content"
|
||||
staticDir = ["static"]
|
||||
19
site/content/about.md
Normal file
19
site/content/about.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
title: "About"
|
||||
date: 2021-07-02T00:00:00Z
|
||||
draft: false
|
||||
tags: ["Kubeconform", "About"]
|
||||
---
|
||||
|
||||
Kubeconform is a Kubernetes manifests validation tool. Build it into your CI to validate your Kubernetes
|
||||
configuration!
|
||||
|
||||
It is inspired by, contains code from and is designed to stay close to
|
||||
[Kubeval](https://github.com/instrumenta/kubeval), but with the following improvements:
|
||||
* **high performance**: will validate & download manifests over multiple routines, caching
|
||||
downloaded files in memory
|
||||
* configurable list of **remote, or local schemas locations**, enabling validating Kubernetes
|
||||
custom resources (CRDs) and offline validation capabilities
|
||||
* uses by default a [self-updating fork](https://github.com/yannh/kubernetes-json-schema) of the schemas registry maintained
|
||||
by the [kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) project - which guarantees
|
||||
up-to-date **schemas for all recent versions of Kubernetes**.
|
||||
45
site/content/docs/crd-support.md
Normal file
45
site/content/docs/crd-support.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: "Custom Resources support"
|
||||
date: 2021-07-02T00:00:00Z
|
||||
draft: false
|
||||
tags: ["Kubeconform", "Usage"]
|
||||
weight: 4
|
||||
---
|
||||
|
||||
When the `-schema-location` parameter is not used, or set to "default", kubeconform will default to downloading
|
||||
schemas from `https://github.com/yannh/kubernetes-json-schema`. Kubeconform however supports passing one, or multiple,
|
||||
schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions
|
||||
in each of them, in order, stopping as soon as a matching file is found.
|
||||
|
||||
* If the -schema-location value does not end with '.json', Kubeconform will assume filenames / a file
|
||||
structure identical to that of kubernetesjsonschema.dev or github.com/yannh/kubernetes-json-schema.
|
||||
* if the -schema-location value ends with '.json' - Kubeconform assumes the value is a Go templated
|
||||
string that indicates how to search for JSON schemas.
|
||||
* the -schema-location value of "default" is an alias for https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json.
|
||||
Both following command lines are equivalent:
|
||||
|
||||
{{< prism >}}$ ./bin/kubeconform fixtures/valid.yaml
|
||||
$ ./bin/kubeconform -schema-location default fixtures/valid.yaml
|
||||
$ ./bin/kubeconform -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
{{< /prism >}}
|
||||
|
||||
To support validating CRDs, we need to convert OpenAPI files to JSON schema, storing the JSON schemas
|
||||
in a local folder - for example schemas. Then we specify this folder as an additional registry to lookup:
|
||||
|
||||
{{< prism >}}# If the resource Kind is not found in kubernetesjsonschema.dev, also lookup in the schemas/ folder for a matching file
|
||||
$ ./bin/kubeconform -schema-location default -schema-location 'schemas/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/custom-resource.yaml
|
||||
{{< /prism >}}
|
||||
|
||||
You can validate Openshift manifests using a custom schema location. Set the OpenShift version to validate
|
||||
against using -kubernetes-version.
|
||||
|
||||
{{< prism >}}$ ./bin/kubeconform -kubernetes-version 3.8.0 -schema-location 'https://raw.githubusercontent.com/garethr/openshift-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}.json' -summary fixtures/valid.yaml
|
||||
Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
|
||||
{{< /prism >}}
|
||||
|
||||
Here are the variables you can use in -schema-location:
|
||||
* *NormalizedKubernetesVersion* - Kubernetes Version, prefixed by v
|
||||
* *StrictSuffix* - "-strict" or "" depending on whether validation is running in strict mode or not
|
||||
* *ResourceKind* - Kind of the Kubernetes Resource
|
||||
* *ResourceAPIVersion* - Version of API used for the resource - "v1" in "apiVersion: monitoring.coreos.com/v1"
|
||||
* *KindSuffix* - suffix computed from apiVersion - for compatibility with Kubeval schema registries
|
||||
28
site/content/docs/installation.md
Normal file
28
site/content/docs/installation.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: "Installation"
|
||||
date: 2021-07-02T00:00:00Z
|
||||
draft: false
|
||||
tags: ["Kubeconform", "Installation"]
|
||||
weight: 2
|
||||
---
|
||||
|
||||
## Linux
|
||||
|
||||
Download the latest release from our [release page](https://github.com/yannh/kubeconform/releases).
|
||||
|
||||
For example, for Linux on x86_64 architecture:
|
||||
|
||||
{{< prism >}}curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xvzf - && \
|
||||
sudo mv kubeconform /usr/local/bin/
|
||||
{{< /prism >}}
|
||||
|
||||
|
||||
## MacOs
|
||||
|
||||
Kubeconform is available to install using [Homebrew](https://brew.sh/):
|
||||
{{< prism >}}$ brew install kubeconform
|
||||
{{< /prism >}}
|
||||
|
||||
## Windows
|
||||
|
||||
Download the latest release from our [release page](https://github.com/yannh/kubeconform/releases).
|
||||
25
site/content/docs/json-schema-conversion.md
Normal file
25
site/content/docs/json-schema-conversion.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
title: "OpenAPI to JSON Schema conversion"
|
||||
date: 2021-07-02T00:00:00Z
|
||||
draft: false
|
||||
tags: ["Kubeconform", "Usage"]
|
||||
weight: 5
|
||||
---
|
||||
|
||||
Kubeconform uses JSON schemas to validate Kubernetes resources. For custom resources, the CustomResourceDefinition
|
||||
first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions
|
||||
to JSON schema. Here is an example how to use it:
|
||||
|
||||
{{< prism >}}#!/bin/bash
|
||||
$ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
JSON schema written to trainingjob_v1.json
|
||||
{{< /prism >}}
|
||||
|
||||
The `FILENAME_FORMAT` environment variable can be used to change the output file name (Available variables: `kind`, `group`, `version`) (Default: `{kind}_{version}`).
|
||||
|
||||
{{< prism >}}$ export FILENAME_FORMAT='{kind}-{group}-{version}'
|
||||
$ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
JSON schema written to trainingjob-sagemaker-v1.json
|
||||
{{< /prism >}}
|
||||
|
||||
Some CRD schemas do not have explicit validation for fields implicitly validated by the Kubernetes API like `apiVersion`, `kind`, and `metadata`, thus additional properties are allowed at the root of the JSON schema by default, if this is not desired the `DENY_ROOT_ADDITIONAL_PROPERTIES` environment variable can be set to any non-empty value.
|
||||
49
site/content/docs/overview.md
Normal file
49
site/content/docs/overview.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
title: "Overview"
|
||||
date: 2021-07-02T00:00:00Z
|
||||
draft: false
|
||||
tags: ["Kubeconform", "Overview"]
|
||||
weight: 1
|
||||
---
|
||||
|
||||
Kubeconform is a Kubernetes manifests validation tool, and checks whether your Kubernetes manifests
|
||||
are valid, according to Kubernetes resources definitions.
|
||||
|
||||
It is inspired by, contains code from and is designed to stay close to
|
||||
[Kubeval](https://github.com/instrumenta/kubeval), but with the following improvements:
|
||||
* **high performance**: will validate & download manifests over multiple routines, caching
|
||||
downloaded files in memory
|
||||
* configurable list of **remote, or local schemas locations**, enabling validating Kubernetes
|
||||
custom resources (CRDs) and offline validation capabilities
|
||||
* uses by default a [self-updating fork](https://github.com/yannh/kubernetes-json-schema) of the schemas registry maintained
|
||||
by the [kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) project - which guarantees
|
||||
up-to-date **schemas for all recent versions of Kubernetes**.
|
||||
* improved logging: support for more formats (Tap, Junit, JSON).
|
||||
|
||||
### A small overview of Kubernetes manifest validation
|
||||
|
||||
Kubernetes's API is described using the [OpenAPI (formerly swagger) specification](https://www.openapis.org),
|
||||
in a [file](https://github.com/kubernetes/kubernetes/blob/master/api/openapi-spec/swagger.json) checked into
|
||||
the main Kubernetes repository.
|
||||
|
||||
Because of the state of the tooling to perform validation against OpenAPI schemas, projects usually convert
|
||||
the OpenAPI schemas to [JSON schemas](https://json-schema.org/) first. Kubeval relies on
|
||||
[instrumenta/OpenApi2JsonSchema](https://github.com/instrumenta/openapi2jsonschema) to convert Kubernetes' Swagger file
|
||||
and break it down into multiple JSON schemas, stored in github at
|
||||
[instrumenta/kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) and published on
|
||||
[kubernetesjsonschema.dev](https://kubernetesjsonschema.dev/).
|
||||
|
||||
Kubeconform relies on [a fork of kubernetes-json-schema](https://github.com/yannh/kubernetes-json-schema/)
|
||||
that is more aggressively kept up-to-date, and contains schemas for all recent versions of Kubernetes.
|
||||
|
||||
### Limits of Kubeconform validation
|
||||
|
||||
Kubeconform, similarly to kubeval, only validates manifests using the OpenAPI specifications. In some
|
||||
cases, the Kubernetes controllers might perform additional validation - so that manifests passing kubeval
|
||||
validation would still error when being deployed. See for example these bugs against kubeval:
|
||||
[#253](https://github.com/instrumenta/kubeval/issues/253)
|
||||
[#256](https://github.com/instrumenta/kubeval/issues/256)
|
||||
[#257](https://github.com/instrumenta/kubeval/issues/257)
|
||||
[#259](https://github.com/instrumenta/kubeval/issues/259). The validation logic mentioned in these
|
||||
bug reports is not part of Kubernetes' OpenAPI spec, and therefore kubeconform/kubeval will not detect the
|
||||
configuration errors.
|
||||
31
site/content/docs/usage-as-github-action.md
Normal file
31
site/content/docs/usage-as-github-action.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: "GitHub Action"
|
||||
date: 2021-07-02T00:00:00Z
|
||||
draft: false
|
||||
tags: ["Kubeconform", "Usage"]
|
||||
weight: 6
|
||||
---
|
||||
|
||||
Kubeconform is publishes Docker Images to GitHub's new Container Registry, ghcr.io. These images
|
||||
can be used directly in a GitHub Action, once logged in using a [_GitHub Token_](https://github.blog/changelog/2021-03-24-packages-container-registry-now-supports-github_token/).
|
||||
|
||||
{{< prism >}}name: kubeconform
|
||||
on: push
|
||||
jobs:
|
||||
kubeconform:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: login to GitHub Packages
|
||||
run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker://ghcr.io/yannh/kubeconform:master
|
||||
with:
|
||||
entrypoint: '/kubeconform'
|
||||
args: "-summary -output json kubeconfigs/"
|
||||
{{< /prism >}}
|
||||
|
||||
_Note on pricing_: Kubeconform relies on GitHub Container Registry which is currently in Beta. During that period,
|
||||
[bandwidth is free](https://docs.github.com/en/packages/guides/about-github-container-registry). After that period,
|
||||
bandwidth costs might be applicable. Since bandwidth from GitHub Packages within GitHub Actions is free, I expect
|
||||
GitHub Container Registry to also be usable for free within GitHub Actions in the future. If that were not to be the
|
||||
case, I might publish the Docker image to a different platform.
|
||||
86
site/content/docs/usage.md
Normal file
86
site/content/docs/usage.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
title: "Usage"
|
||||
date: 2021-07-02T00:00:00Z
|
||||
draft: false
|
||||
tags: ["Kubeconform", "Usage"]
|
||||
weight: 3
|
||||
---
|
||||
|
||||
{{< prism >}}$ ./bin/kubeconform -h
|
||||
Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
||||
-cache string
|
||||
cache schemas downloaded via HTTP to this folder
|
||||
-cpu-prof string
|
||||
debug - log CPU profiling to file
|
||||
-exit-on-error
|
||||
immediately stop execution when the first error is encountered
|
||||
-h show help information
|
||||
-ignore-filename-pattern value
|
||||
regular expression specifying paths to ignore (can be specified multiple times)
|
||||
-ignore-missing-schemas
|
||||
skip files with missing schemas instead of failing
|
||||
-insecure-skip-tls-verify
|
||||
disable verification of the server's SSL certificate. This will make your HTTPS connections insecure
|
||||
-kubernetes-version string
|
||||
version of Kubernetes to validate against, e.g.: 1.18.0 (default "master")
|
||||
-n int
|
||||
number of goroutines to run concurrently (default 4)
|
||||
-output string
|
||||
output format - json, junit, tap, text (default "text")
|
||||
-reject string
|
||||
comma-separated list of kinds to reject
|
||||
-schema-location value
|
||||
override schemas location search path (can be specified multiple times)
|
||||
-skip string
|
||||
comma-separated list of kinds to ignore
|
||||
-strict
|
||||
disallow additional properties not in schema or duplicated keys
|
||||
-summary
|
||||
print a summary at the end (ignored for junit output)
|
||||
-v show version information
|
||||
-verbose
|
||||
print results for all resources (ignored for tap and junit output)
|
||||
{{< /prism >}}
|
||||
|
||||
### Validating a single, valid file
|
||||
|
||||
{{< prism >}}$ ./bin/kubeconform fixtures/valid.yaml
|
||||
$ echo $?
|
||||
0
|
||||
{{< /prism >}}
|
||||
|
||||
### Validating a single invalid file, setting output to json, and printing a summary
|
||||
{{< prism >}}$ ./bin/kubeconform -summary -output json fixtures/invalid.yaml
|
||||
{
|
||||
"resources": [
|
||||
{
|
||||
"filename": "fixtures/invalid.yaml",
|
||||
"kind": "ReplicationController",
|
||||
"version": "v1",
|
||||
"status": "INVALID",
|
||||
"msg": "Additional property templates is not allowed - Invalid type. Expected: [integer,null], given: string"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"valid": 0,
|
||||
"invalid": 1,
|
||||
"errors": 0,
|
||||
"skipped": 0
|
||||
}
|
||||
}
|
||||
$ echo $?
|
||||
1
|
||||
{{< /prism >}}
|
||||
|
||||
### Passing manifests via Stdin
|
||||
{{< prism >}}cat fixtures/valid.yaml | ./bin/kubeconform -summary
|
||||
Summary: 1 resource found parsing stdin - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
|
||||
{{< /prism >}}
|
||||
|
||||
### Validating a folder, increasing the number of parallel workers
|
||||
{{< prism >}}$ ./bin/kubeconform -summary -n 16 fixtures
|
||||
fixtures/crd_schema.yaml - CustomResourceDefinition trainingjobs.sagemaker.aws.amazon.com failed validation: could not find schema for CustomResourceDefinition
|
||||
fixtures/invalid.yaml - ReplicationController bob is invalid: Invalid type. Expected: [integer,null], given: string
|
||||
[...]
|
||||
Summary: 65 resources found in 34 files - Valid: 55, Invalid: 2, Errors: 8 Skipped: 0
|
||||
{{< /prism >}}
|
||||
14
site/content/docs/using-as-a-go-module.md
Normal file
14
site/content/docs/using-as-a-go-module.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: "Kubeconform as a Go module"
|
||||
date: 2021-07-02T00:00:00Z
|
||||
draft: false
|
||||
tags: ["Kubeconform", "Usage"]
|
||||
weight: 7
|
||||
---
|
||||
|
||||
**Warning**: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
|
||||
Kubeconform contains a package that can be used as a library.
|
||||
An example of usage can be found in [examples/main.go](https://github.com/yannh/kubeconform/tree/master/examples/main.go)
|
||||
|
||||
Additional documentation on [pkg.go.dev](https://pkg.go.dev/github.com/yannh/kubeconform/pkg/validator)
|
||||
51
site/public/about/index.html
Normal file
51
site/public/about/index.html
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<!doctype html><html><head>
|
||||
<meta charset=utf-8>
|
||||
<meta name=author content="Yann Hamon">
|
||||
<link rel=stylesheet type=text/css href=/css/style.css><link rel=stylesheet type=text/css href=/css/prism.css>
|
||||
<title>Kubeconform - Fast Kubernetes manifests validation! | About</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
<h1>Kubeconform</h1>
|
||||
<h2>A fast Kubernetes manifests validator</h2>
|
||||
</div>
|
||||
<div id=content><div id=main>
|
||||
<div class=navig>
|
||||
<a href=# id=prev></a>
|
||||
<a href=# id=prev></a>
|
||||
</div>
|
||||
<div id=content-text>
|
||||
<h1>About</h1>
|
||||
<p>Kubeconform is a Kubernetes manifests validation tool. Build it into your CI to validate your Kubernetes
|
||||
configuration!</p>
|
||||
<p>It is inspired by, contains code from and is designed to stay close to
|
||||
<a href=https://github.com/instrumenta/kubeval>Kubeval</a>, but with the following improvements:</p>
|
||||
<ul>
|
||||
<li><strong>high performance</strong>: will validate & download manifests over multiple routines, caching
|
||||
downloaded files in memory</li>
|
||||
<li>configurable list of <strong>remote, or local schemas locations</strong>, enabling validating Kubernetes
|
||||
custom resources (CRDs) and offline validation capabilities</li>
|
||||
<li>uses by default a <a href=https://github.com/yannh/kubernetes-json-schema>self-updating fork</a> of the schemas registry maintained
|
||||
by the <a href=https://github.com/instrumenta/kubernetes-json-schema>kubernetes-json-schema</a> project - which guarantees
|
||||
up-to-date <strong>schemas for all recent versions of Kubernetes</strong>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class=navig>
|
||||
<a href=# id=prev></a>
|
||||
<a href=# id=prev></a>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</div>
|
||||
</div><div id=footer>
|
||||
Website powered by <a href=https://gohugo.io/>Hugo</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</body>
|
||||
</html>
|
||||
1
site/public/categories/index.xml
Normal file
1
site/public/categories/index.xml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Categories on Kubeconform - Fast Kubernetes manifests validation!</title><link>http://kubeconform.mandragor.org/categories/</link><description>Recent content in Categories on Kubeconform - Fast Kubernetes manifests validation!</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><atom:link href="http://kubeconform.mandragor.org/categories/index.xml" rel="self" type="application/rss+xml"/></channel></rss>
|
||||
122
site/public/css/prism.css
Normal file
122
site/public/css/prism.css
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
/**
|
||||
* okaidia theme for JavaScript, CSS and HTML
|
||||
* Loosely based on Monokai textmate theme by http://www.monokai.nl/
|
||||
* @author ocodia
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #f8f8f2;
|
||||
background: none;
|
||||
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #272822;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #f92672;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.number {
|
||||
color: #ae81ff;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #a6e22e;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string,
|
||||
.token.variable {
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #e6db74;
|
||||
}
|
||||
|
||||
.token.keyword {
|
||||
color: #66d9ef;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: #fd971f;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
215
site/public/css/style.css
Normal file
215
site/public/css/style.css
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
|
||||
/* Colors */
|
||||
body { background-color: white; }
|
||||
a { color: black }
|
||||
hr { border-color: #ddd; }
|
||||
#header, #footer { background-color: #002036; color: white }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
}
|
||||
|
||||
/* Font sizes */
|
||||
body { font-size: 1.4rem; line-height: 1.9rem; text-size-adjust: 100%; }
|
||||
h1 { font-size: 2.7rem; line-height: 3.8rem; font-weight: 400 }
|
||||
h2 { font-size: 2rem; line-height: 2.5rem; font-weight: 400 }
|
||||
h3 { font-size: 1.7rem; line-height: 2rem; font-weight: 300 }
|
||||
|
||||
#header h1 { font-size: 4rem; line-height: 4.5rem; font-weight: 500; margin-top: 0.2em; margin-left: 30px }
|
||||
#header h2 { font-size: 1.7rem; line-height: 2.2rem; font-weight: 300; font-style: italic; margin: 0 0 0.5em 30px}
|
||||
|
||||
/* We default all margins/paddings to 0 */
|
||||
* { margin: 0; padding: 0 }
|
||||
a { text-decoration: none }
|
||||
#content-text a { text-decoration: underline }
|
||||
#content-text a:hover { text-decoration: none }
|
||||
p {
|
||||
font-weight: 400;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 500;
|
||||
margin: 3rem 0 0.8rem 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
margin: 1.5rem 0 1.5rem 0;
|
||||
}
|
||||
pre {
|
||||
margin: 1rem 0 1rem 0
|
||||
}
|
||||
|
||||
#main-container {
|
||||
padding: 0;
|
||||
font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-feature-settings: "kern", "liga";
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
margin: 3rem 0 3rem 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#header, #footer {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#header {
|
||||
padding: 0.5em 0 0.5em 0em;
|
||||
}
|
||||
|
||||
#content {
|
||||
display:flex;
|
||||
}
|
||||
|
||||
#menu {
|
||||
flex: 15;
|
||||
min-width: 15%;
|
||||
background-color: #ddd;
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#menu li {
|
||||
line-height: 1.9rem;
|
||||
padding-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
#menu li::marker {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
#menu li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#main {
|
||||
flex: 85;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#main h1 {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: scroll;
|
||||
min-width: 0
|
||||
}
|
||||
|
||||
#footer {
|
||||
padding: 0.5em 0;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
#footer a {
|
||||
font-style: italic;
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#navigation {
|
||||
float: right;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
#navigation li {
|
||||
display: block;
|
||||
width: 100px;
|
||||
float: right;
|
||||
padding-top: 0.2em;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
#navigation li a{
|
||||
color: white
|
||||
}
|
||||
|
||||
#navigation li a:hover{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#motto {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
font-size: 1.4em;
|
||||
margin: 2em auto 2em auto;
|
||||
}
|
||||
|
||||
#demo{
|
||||
font-size: smaller;
|
||||
margin: 2em auto 2em auto;
|
||||
border-radius: 1em;
|
||||
display: table;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#kc-pros {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin: 0 auto;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
#kc-pros > div {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
|
||||
#kc-pros h2 {
|
||||
font-size: 1.4em;
|
||||
line-height: 1.2em;
|
||||
padding: 0 5% 0.3em 5%;
|
||||
}
|
||||
|
||||
#kc-pros p {
|
||||
font-size: 1.1em;
|
||||
padding: 0 5% 2em 5%;
|
||||
}
|
||||
|
||||
#get {
|
||||
display: table;
|
||||
border: 1px solid black;
|
||||
padding: 0.5em 2em;
|
||||
border-radius: 0.8em;
|
||||
clear: both;
|
||||
margin: 3em auto 5em auto;
|
||||
background-color: #0594cb;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#get:active {
|
||||
background-color: #002036;
|
||||
}
|
||||
|
||||
.navig {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.navig > a {
|
||||
flex-basis: 50%;
|
||||
text-align: center;
|
||||
background-color: #eee;
|
||||
padding: 0.4em 0;
|
||||
font-size: smaller
|
||||
}
|
||||
|
||||
#content-text {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#main ul {
|
||||
margin: 1em 0 2em 3em;
|
||||
}
|
||||
81
site/public/docs/crd-support/index.html
Normal file
81
site/public/docs/crd-support/index.html
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<!doctype html><html><head>
|
||||
<meta charset=utf-8>
|
||||
<meta name=author content="Yann Hamon">
|
||||
<link rel=stylesheet type=text/css href=/css/style.css><link rel=stylesheet type=text/css href=/css/prism.css>
|
||||
<title>Kubeconform - Fast Kubernetes manifests validation! | Custom Resources support</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
<h1>Kubeconform</h1>
|
||||
<h2>A fast Kubernetes manifests validator</h2>
|
||||
</div>
|
||||
<div id=content><ul id=menu>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/overview/>Overview</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/installation/>Installation</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage/ id=prev>< Usage</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/ id=next>OpenAPI to JSON Schema conversion ></a>
|
||||
</div>
|
||||
<div id=content-text>
|
||||
<h1>Custom Resources support</h1>
|
||||
<p>When the <code>-schema-location</code> parameter is not used, or set to “default”, kubeconform will default to downloading
|
||||
schemas from <code>https://github.com/yannh/kubernetes-json-schema</code>. Kubeconform however supports passing one, or multiple,
|
||||
schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions
|
||||
in each of them, in order, stopping as soon as a matching file is found.</p>
|
||||
<ul>
|
||||
<li>If the -schema-location value does not end with ‘.json’, Kubeconform will assume filenames / a file
|
||||
structure identical to that of kubernetesjsonschema.dev or github.com/yannh/kubernetes-json-schema.</li>
|
||||
<li>if the -schema-location value ends with ‘.json’ - Kubeconform assumes the value is a Go templated
|
||||
string that indicates how to search for JSON schemas.</li>
|
||||
<li>the -schema-location value of “default” is an alias for <a href=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/%7B%7B>https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{</a> .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json.
|
||||
Both following command lines are equivalent:</li>
|
||||
</ul>
|
||||
<pre><code class=language-bash>$ ./bin/kubeconform fixtures/valid.yaml
|
||||
$ ./bin/kubeconform -schema-location default fixtures/valid.yaml
|
||||
$ ./bin/kubeconform -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
</code></pre>
|
||||
<p>To support validating CRDs, we need to convert OpenAPI files to JSON schema, storing the JSON schemas
|
||||
in a local folder - for example schemas. Then we specify this folder as an additional registry to lookup:</p>
|
||||
<pre><code class=language-bash># If the resource Kind is not found in kubernetesjsonschema.dev, also lookup in the schemas/ folder for a matching file
|
||||
$ ./bin/kubeconform -schema-location default -schema-location 'schemas/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/custom-resource.yaml
|
||||
</code></pre>
|
||||
<p>You can validate Openshift manifests using a custom schema location. Set the OpenShift version to validate
|
||||
against using -kubernetes-version.</p>
|
||||
<pre><code class=language-bash>$ ./bin/kubeconform -kubernetes-version 3.8.0 -schema-location 'https://raw.githubusercontent.com/garethr/openshift-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}.json' -summary fixtures/valid.yaml
|
||||
Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
|
||||
</code></pre>
|
||||
<p>Here are the variables you can use in -schema-location:</p>
|
||||
<ul>
|
||||
<li><em>NormalizedKubernetesVersion</em> - Kubernetes Version, prefixed by v</li>
|
||||
<li><em>StrictSuffix</em> - “-strict” or "" depending on whether validation is running in strict mode or not</li>
|
||||
<li><em>ResourceKind</em> - Kind of the Kubernetes Resource</li>
|
||||
<li><em>ResourceAPIVersion</em> - Version of API used for the resource - “v1” in “apiVersion: monitoring.coreos.com/v1”</li>
|
||||
<li><em>KindSuffix</em> - suffix computed from apiVersion - for compatibility with Kubeval schema registries</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage/ id=prev>< Usage</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/ id=next>OpenAPI to JSON Schema conversion ></a>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</div>
|
||||
</div><div id=footer>
|
||||
Website powered by <a href=https://gohugo.io/>Hugo</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</body>
|
||||
</html>
|
||||
11
site/public/docs/index.xml
Normal file
11
site/public/docs/index.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Docs on Kubeconform - Fast Kubernetes manifests validation!</title><link>http://kubeconform.mandragor.org/docs/</link><description>Recent content in Docs on Kubeconform - Fast Kubernetes manifests validation!</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 02 Jul 2021 00:00:00 +0000</lastBuildDate><atom:link href="http://kubeconform.mandragor.org/docs/index.xml" rel="self" type="application/rss+xml"/><item><title>Overview</title><link>http://kubeconform.mandragor.org/docs/overview/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/overview/</guid><description>Kubeconform is a Kubernetes manifests validation tool, and checks whether your Kubernetes manifests are valid, according to Kubernetes resources definitions.
|
||||
It is inspired by, contains code from and is designed to stay close to Kubeval, but with the following improvements:
|
||||
high performance: will validate &amp; download manifests over multiple routines, caching downloaded files in memory configurable list of remote, or local schemas locations, enabling validating Kubernetes custom resources (CRDs) and offline validation capabilities uses by default a self-updating fork of the schemas registry maintained by the kubernetes-json-schema project - which guarantees up-to-date schemas for all recent versions of Kubernetes.</description></item><item><title>Installation</title><link>http://kubeconform.mandragor.org/docs/installation/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/installation/</guid><description>Linux Download the latest release from our release page.
|
||||
For example, for Linux on x86_64 architecture:
|
||||
curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xvzf - && \ sudo mv kubeconform /usr/local/bin/ MacOs Kubeconform is available to install using Homebrew: $ brew install kubeconform
|
||||
Windows Download the latest release from our release page.</description></item><item><title>Usage</title><link>http://kubeconform.mandragor.org/docs/usage/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage/</guid><description>$ ./bin/kubeconform -h Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]... -cache string cache schemas downloaded via HTTP to this folder -cpu-prof string debug - log CPU profiling to file -exit-on-error immediately stop execution when the first error is encountered -h show help information -ignore-filename-pattern value regular expression specifying paths to ignore (can be specified multiple times) -ignore-missing-schemas skip files with missing schemas instead of failing -insecure-skip-tls-verify disable verification of the server's SSL certificate.</description></item><item><title>Custom Resources support</title><link>http://kubeconform.mandragor.org/docs/crd-support/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/crd-support/</guid><description>When the -schema-location parameter is not used, or set to &ldquo;default&rdquo;, kubeconform will default to downloading schemas from https://github.com/yannh/kubernetes-json-schema. Kubeconform however supports passing one, or multiple, schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions in each of them, in order, stopping as soon as a matching file is found.
|
||||
If the -schema-location value does not end with &lsquo;.json&rsquo;, Kubeconform will assume filenames / a file structure identical to that of kubernetesjsonschema.</description></item><item><title>OpenAPI to JSON Schema conversion</title><link>http://kubeconform.mandragor.org/docs/json-schema-conversion/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/json-schema-conversion/</guid><description>Kubeconform uses JSON schemas to validate Kubernetes resources. For custom resources, the CustomResourceDefinition first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions to JSON schema. Here is an example how to use it:
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>GitHub Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to GitHub&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a GitHub Action, once logged in using a GitHub Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to GitHub Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on GitHub Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
Kubeconform contains a package that can be used as a library. An example of usage can be found in examples/main.go
|
||||
Additional documentation on pkg.go.dev</description></item></channel></rss>
|
||||
59
site/public/docs/installation/index.html
Normal file
59
site/public/docs/installation/index.html
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<!doctype html><html><head>
|
||||
<meta charset=utf-8>
|
||||
<meta name=author content="Yann Hamon">
|
||||
<link rel=stylesheet type=text/css href=/css/style.css><link rel=stylesheet type=text/css href=/css/prism.css>
|
||||
<title>Kubeconform - Fast Kubernetes manifests validation! | Installation</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
<h1>Kubeconform</h1>
|
||||
<h2>A fast Kubernetes manifests validator</h2>
|
||||
</div>
|
||||
<div id=content><ul id=menu>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/overview/>Overview</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/installation/>Installation</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/overview/ id=prev>< Overview</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage/ id=next>Usage ></a>
|
||||
</div>
|
||||
<div id=content-text>
|
||||
<h1>Installation</h1>
|
||||
<h2 id=linux>Linux</h2>
|
||||
<p>Download the latest release from our <a href=https://github.com/yannh/kubeconform/releases>release page</a>.</p>
|
||||
<p>For example, for Linux on x86_64 architecture:</p>
|
||||
<pre><code class=language-bash>curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xvzf - && \
|
||||
sudo mv kubeconform /usr/local/bin/
|
||||
</code></pre>
|
||||
<h2 id=macos>MacOs</h2>
|
||||
<p>Kubeconform is available to install using <a href=https://brew.sh/>Homebrew</a>:
|
||||
<pre><code class=language-bash>$ brew install kubeconform
|
||||
</code></pre></p>
|
||||
<h2 id=windows>Windows</h2>
|
||||
<p>Download the latest release from our <a href=https://github.com/yannh/kubeconform/releases>release page</a>.</p>
|
||||
</div>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/overview/ id=prev>< Overview</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage/ id=next>Usage ></a>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</div>
|
||||
</div><div id=footer>
|
||||
Website powered by <a href=https://gohugo.io/>Hugo</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</body>
|
||||
</html>
|
||||
60
site/public/docs/json-schema-conversion/index.html
Normal file
60
site/public/docs/json-schema-conversion/index.html
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<!doctype html><html><head>
|
||||
<meta charset=utf-8>
|
||||
<meta name=author content="Yann Hamon">
|
||||
<link rel=stylesheet type=text/css href=/css/style.css><link rel=stylesheet type=text/css href=/css/prism.css>
|
||||
<title>Kubeconform - Fast Kubernetes manifests validation! | OpenAPI to JSON Schema conversion</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
<h1>Kubeconform</h1>
|
||||
<h2>A fast Kubernetes manifests validator</h2>
|
||||
</div>
|
||||
<div id=content><ul id=menu>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/overview/>Overview</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/installation/>Installation</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/crd-support/ id=prev>< Custom Resources support</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=next>GitHub Action ></a>
|
||||
</div>
|
||||
<div id=content-text>
|
||||
<h1>OpenAPI to JSON Schema conversion</h1>
|
||||
<p>Kubeconform uses JSON schemas to validate Kubernetes resources. For custom resources, the CustomResourceDefinition
|
||||
first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions
|
||||
to JSON schema. Here is an example how to use it:</p>
|
||||
<pre><code class=language-bash>#!/bin/bash
|
||||
$ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
JSON schema written to trainingjob_v1.json
|
||||
</code></pre>
|
||||
<p>The <code>FILENAME_FORMAT</code> environment variable can be used to change the output file name (Available variables: <code>kind</code>, <code>group</code>, <code>version</code>) (Default: <code>{kind}_{version}</code>).</p>
|
||||
<pre><code class=language-bash>$ export FILENAME_FORMAT='{kind}-{group}-{version}'
|
||||
$ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
JSON schema written to trainingjob-sagemaker-v1.json
|
||||
</code></pre>
|
||||
<p>Some CRD schemas do not have explicit validation for fields implicitly validated by the Kubernetes API like <code>apiVersion</code>, <code>kind</code>, and <code>metadata</code>, thus additional properties are allowed at the root of the JSON schema by default, if this is not desired the <code>DENY_ROOT_ADDITIONAL_PROPERTIES</code> environment variable can be set to any non-empty value.</p>
|
||||
</div>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/crd-support/ id=prev>< Custom Resources support</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=next>GitHub Action ></a>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</div>
|
||||
</div><div id=footer>
|
||||
Website powered by <a href=https://gohugo.io/>Hugo</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</body>
|
||||
</html>
|
||||
68
site/public/docs/usage-as-github-action/index.html
Normal file
68
site/public/docs/usage-as-github-action/index.html
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<!doctype html><html><head>
|
||||
<meta charset=utf-8>
|
||||
<meta name=author content="Yann Hamon">
|
||||
<link rel=stylesheet type=text/css href=/css/style.css><link rel=stylesheet type=text/css href=/css/prism.css>
|
||||
<title>Kubeconform - Fast Kubernetes manifests validation! | GitHub Action</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
<h1>Kubeconform</h1>
|
||||
<h2>A fast Kubernetes manifests validator</h2>
|
||||
</div>
|
||||
<div id=content><ul id=menu>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/overview/>Overview</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/installation/>Installation</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/ id=prev>< OpenAPI to JSON Schema conversion</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/ id=next>Kubeconform as a Go module ></a>
|
||||
</div>
|
||||
<div id=content-text>
|
||||
<h1>GitHub Action</h1>
|
||||
<p>Kubeconform is publishes Docker Images to GitHub’s new Container Registry, ghcr.io. These images
|
||||
can be used directly in a GitHub Action, once logged in using a <a href=https://github.blog/changelog/2021-03-24-packages-container-registry-now-supports-github_token/><em>GitHub Token</em></a>.</p>
|
||||
<pre><code class=language-bash>name: kubeconform
|
||||
on: push
|
||||
jobs:
|
||||
kubeconform:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: login to GitHub Packages
|
||||
run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker://ghcr.io/yannh/kubeconform:master
|
||||
with:
|
||||
entrypoint: '/kubeconform'
|
||||
args: "-summary -output json kubeconfigs/"
|
||||
</code></pre>
|
||||
<p><em>Note on pricing</em>: Kubeconform relies on GitHub Container Registry which is currently in Beta. During that period,
|
||||
<a href=https://docs.github.com/en/packages/guides/about-github-container-registry>bandwidth is free</a>. After that period,
|
||||
bandwidth costs might be applicable. Since bandwidth from GitHub Packages within GitHub Actions is free, I expect
|
||||
GitHub Container Registry to also be usable for free within GitHub Actions in the future. If that were not to be the
|
||||
case, I might publish the Docker image to a different platform.</p>
|
||||
</div>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/ id=prev>< OpenAPI to JSON Schema conversion</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/ id=next>Kubeconform as a Go module ></a>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</div>
|
||||
</div><div id=footer>
|
||||
Website powered by <a href=https://gohugo.io/>Hugo</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</body>
|
||||
</html>
|
||||
120
site/public/docs/usage/index.html
Normal file
120
site/public/docs/usage/index.html
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<!doctype html><html><head>
|
||||
<meta charset=utf-8>
|
||||
<meta name=author content="Yann Hamon">
|
||||
<link rel=stylesheet type=text/css href=/css/style.css><link rel=stylesheet type=text/css href=/css/prism.css>
|
||||
<title>Kubeconform - Fast Kubernetes manifests validation! | Usage</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
<h1>Kubeconform</h1>
|
||||
<h2>A fast Kubernetes manifests validator</h2>
|
||||
</div>
|
||||
<div id=content><ul id=menu>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/overview/>Overview</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/installation/>Installation</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/installation/ id=prev>< Installation</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/crd-support/ id=next>Custom Resources support ></a>
|
||||
</div>
|
||||
<div id=content-text>
|
||||
<h1>Usage</h1>
|
||||
<pre><code class=language-bash>$ ./bin/kubeconform -h
|
||||
Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
||||
-cache string
|
||||
cache schemas downloaded via HTTP to this folder
|
||||
-cpu-prof string
|
||||
debug - log CPU profiling to file
|
||||
-exit-on-error
|
||||
immediately stop execution when the first error is encountered
|
||||
-h show help information
|
||||
-ignore-filename-pattern value
|
||||
regular expression specifying paths to ignore (can be specified multiple times)
|
||||
-ignore-missing-schemas
|
||||
skip files with missing schemas instead of failing
|
||||
-insecure-skip-tls-verify
|
||||
disable verification of the server's SSL certificate. This will make your HTTPS connections insecure
|
||||
-kubernetes-version string
|
||||
version of Kubernetes to validate against, e.g.: 1.18.0 (default "master")
|
||||
-n int
|
||||
number of goroutines to run concurrently (default 4)
|
||||
-output string
|
||||
output format - json, junit, tap, text (default "text")
|
||||
-reject string
|
||||
comma-separated list of kinds to reject
|
||||
-schema-location value
|
||||
override schemas location search path (can be specified multiple times)
|
||||
-skip string
|
||||
comma-separated list of kinds to ignore
|
||||
-strict
|
||||
disallow additional properties not in schema or duplicated keys
|
||||
-summary
|
||||
print a summary at the end (ignored for junit output)
|
||||
-v show version information
|
||||
-verbose
|
||||
print results for all resources (ignored for tap and junit output)
|
||||
</code></pre>
|
||||
<h3 id=validating-a-single-valid-file>Validating a single, valid file</h3>
|
||||
<pre><code class=language-bash>$ ./bin/kubeconform fixtures/valid.yaml
|
||||
$ echo $?
|
||||
0
|
||||
</code></pre>
|
||||
<h3 id=validating-a-single-invalid-file-setting-output-to-json-and-printing-a-summary>Validating a single invalid file, setting output to json, and printing a summary</h3>
|
||||
<pre><code class=language-bash>$ ./bin/kubeconform -summary -output json fixtures/invalid.yaml
|
||||
{
|
||||
"resources": [
|
||||
{
|
||||
"filename": "fixtures/invalid.yaml",
|
||||
"kind": "ReplicationController",
|
||||
"version": "v1",
|
||||
"status": "INVALID",
|
||||
"msg": "Additional property templates is not allowed - Invalid type. Expected: [integer,null], given: string"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"valid": 0,
|
||||
"invalid": 1,
|
||||
"errors": 0,
|
||||
"skipped": 0
|
||||
}
|
||||
}
|
||||
$ echo $?
|
||||
1
|
||||
</code></pre>
|
||||
<h3 id=passing-manifests-via-stdin>Passing manifests via Stdin</h3>
|
||||
<pre><code class=language-bash>cat fixtures/valid.yaml | ./bin/kubeconform -summary
|
||||
Summary: 1 resource found parsing stdin - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
|
||||
</code></pre>
|
||||
<h3 id=validating-a-folder-increasing-the-number-of-parallel-workers>Validating a folder, increasing the number of parallel workers</h3>
|
||||
<pre><code class=language-bash>$ ./bin/kubeconform -summary -n 16 fixtures
|
||||
fixtures/crd_schema.yaml - CustomResourceDefinition trainingjobs.sagemaker.aws.amazon.com failed validation: could not find schema for CustomResourceDefinition
|
||||
fixtures/invalid.yaml - ReplicationController bob is invalid: Invalid type. Expected: [integer,null], given: string
|
||||
[...]
|
||||
Summary: 65 resources found in 34 files - Valid: 55, Invalid: 2, Errors: 8 Skipped: 0
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/installation/ id=prev>< Installation</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/crd-support/ id=next>Custom Resources support ></a>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</div>
|
||||
</div><div id=footer>
|
||||
Website powered by <a href=https://gohugo.io/>Hugo</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</body>
|
||||
</html>
|
||||
51
site/public/docs/using-as-a-go-module/index.html
Normal file
51
site/public/docs/using-as-a-go-module/index.html
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<!doctype html><html><head>
|
||||
<meta charset=utf-8>
|
||||
<meta name=author content="Yann Hamon">
|
||||
<link rel=stylesheet type=text/css href=/css/style.css><link rel=stylesheet type=text/css href=/css/prism.css>
|
||||
<title>Kubeconform - Fast Kubernetes manifests validation! | Kubeconform as a Go module</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
<h1>Kubeconform</h1>
|
||||
<h2>A fast Kubernetes manifests validator</h2>
|
||||
</div>
|
||||
<div id=content><ul id=menu>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/overview/>Overview</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/installation/>Installation</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=prev>< GitHub Action</a>
|
||||
<a href=# id=prev></a>
|
||||
</div>
|
||||
<div id=content-text>
|
||||
<h1>Kubeconform as a Go module</h1>
|
||||
<p><strong>Warning</strong>: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.</p>
|
||||
<p>Kubeconform contains a package that can be used as a library.
|
||||
An example of usage can be found in <a href=https://github.com/yannh/kubeconform/tree/master/examples/main.go>examples/main.go</a></p>
|
||||
<p>Additional documentation on <a href=https://pkg.go.dev/github.com/yannh/kubeconform/pkg/validator>pkg.go.dev</a></p>
|
||||
</div>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=prev>< GitHub Action</a>
|
||||
<a href=# id=prev></a>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</div>
|
||||
</div><div id=footer>
|
||||
Website powered by <a href=https://gohugo.io/>Hugo</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</body>
|
||||
</html>
|
||||
52
site/public/index.html
Normal file
52
site/public/index.html
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<!doctype html><html><head>
|
||||
<meta name=generator content="Hugo 0.91.0">
|
||||
<meta charset=utf-8>
|
||||
<meta name=author content="Yann Hamon">
|
||||
<link rel=stylesheet type=text/css href=/css/style.css><link rel=stylesheet type=text/css href=/css/prism.css>
|
||||
<title>Kubeconform - Fast Kubernetes manifests validation!</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
<h1>Kubeconform</h1>
|
||||
<h2>A fast Kubernetes manifests validator</h2>
|
||||
</div>
|
||||
<div id=content><div id=main>
|
||||
<p id=motto>Validate your Kubernetes manifests instead of deploying broken configuration</p>
|
||||
<pre id=demo><code class=language-bash>$ kubeconform -summary myapp/deployment.yaml
|
||||
Summary: 5 resources found in 1 file - Valid: 5, Invalid: 0, Errors: 0, Skipped: 0
|
||||
</code></pre>
|
||||
<a href=/docs/installation/ id=get>
|
||||
Get Started!
|
||||
</a>
|
||||
<div id=kc-pros>
|
||||
<div>
|
||||
<h2>Easy-to-use</h2>
|
||||
<p>Single binary, super-easy installation for Windows, Mac & Linux. It takes seconds to get started.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Lightning fast</h2>
|
||||
<p>Kubeconform makes heavy use of Golang's concurrency capabilities, and will spread its workload across multiple cores.</pa>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Support for Kubernetes CRDs</h2>
|
||||
<p>Validate ALL your Kubernetes resources with Kubeconform's CRD support</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Flexible</h2>
|
||||
<p>With support for JSON, Junit, TAP output, and leveraging the easy-to-use Docker image, you can run Kubeconform in any CI system.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><div id=footer>
|
||||
Website powered by <a href=https://gohugo.io/>Hugo</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</body>
|
||||
</html>
|
||||
13
site/public/index.xml
Normal file
13
site/public/index.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kubeconform - Fast Kubernetes manifests validation!</title><link>http://kubeconform.mandragor.org/</link><description>Recent content on Kubeconform - Fast Kubernetes manifests validation!</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 02 Jul 2021 00:00:00 +0000</lastBuildDate><atom:link href="http://kubeconform.mandragor.org/index.xml" rel="self" type="application/rss+xml"/><item><title>Overview</title><link>http://kubeconform.mandragor.org/docs/overview/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/overview/</guid><description>Kubeconform is a Kubernetes manifests validation tool, and checks whether your Kubernetes manifests are valid, according to Kubernetes resources definitions.
|
||||
It is inspired by, contains code from and is designed to stay close to Kubeval, but with the following improvements:
|
||||
high performance: will validate &amp; download manifests over multiple routines, caching downloaded files in memory configurable list of remote, or local schemas locations, enabling validating Kubernetes custom resources (CRDs) and offline validation capabilities uses by default a self-updating fork of the schemas registry maintained by the kubernetes-json-schema project - which guarantees up-to-date schemas for all recent versions of Kubernetes.</description></item><item><title>Installation</title><link>http://kubeconform.mandragor.org/docs/installation/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/installation/</guid><description>Linux Download the latest release from our release page.
|
||||
For example, for Linux on x86_64 architecture:
|
||||
curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xvzf - && \ sudo mv kubeconform /usr/local/bin/ MacOs Kubeconform is available to install using Homebrew: $ brew install kubeconform
|
||||
Windows Download the latest release from our release page.</description></item><item><title>Usage</title><link>http://kubeconform.mandragor.org/docs/usage/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage/</guid><description>$ ./bin/kubeconform -h Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]... -cache string cache schemas downloaded via HTTP to this folder -cpu-prof string debug - log CPU profiling to file -exit-on-error immediately stop execution when the first error is encountered -h show help information -ignore-filename-pattern value regular expression specifying paths to ignore (can be specified multiple times) -ignore-missing-schemas skip files with missing schemas instead of failing -insecure-skip-tls-verify disable verification of the server's SSL certificate.</description></item><item><title>Custom Resources support</title><link>http://kubeconform.mandragor.org/docs/crd-support/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/crd-support/</guid><description>When the -schema-location parameter is not used, or set to &ldquo;default&rdquo;, kubeconform will default to downloading schemas from https://github.com/yannh/kubernetes-json-schema. Kubeconform however supports passing one, or multiple, schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions in each of them, in order, stopping as soon as a matching file is found.
|
||||
If the -schema-location value does not end with &lsquo;.json&rsquo;, Kubeconform will assume filenames / a file structure identical to that of kubernetesjsonschema.</description></item><item><title>OpenAPI to JSON Schema conversion</title><link>http://kubeconform.mandragor.org/docs/json-schema-conversion/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/json-schema-conversion/</guid><description>Kubeconform uses JSON schemas to validate Kubernetes resources. For custom resources, the CustomResourceDefinition first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions to JSON schema. Here is an example how to use it:
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>GitHub Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to GitHub&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a GitHub Action, once logged in using a GitHub Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to GitHub Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on GitHub Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
Kubeconform contains a package that can be used as a library. An example of usage can be found in examples/main.go
|
||||
Additional documentation on pkg.go.dev</description></item><item><title>About</title><link>http://kubeconform.mandragor.org/about/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/about/</guid><description>Kubeconform is a Kubernetes manifests validation tool. Build it into your CI to validate your Kubernetes configuration!
|
||||
It is inspired by, contains code from and is designed to stay close to Kubeval, but with the following improvements:
|
||||
high performance: will validate &amp; download manifests over multiple routines, caching downloaded files in memory configurable list of remote, or local schemas locations, enabling validating Kubernetes custom resources (CRDs) and offline validation capabilities uses by default a self-updating fork of the schemas registry maintained by the kubernetes-json-schema project - which guarantees up-to-date schemas for all recent versions of Kubernetes.</description></item></channel></rss>
|
||||
31
site/public/installation/index.html
Normal file
31
site/public/installation/index.html
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html><head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="author" content="Yann Hamon">
|
||||
<link rel="stylesheet" type="text/css" href="/css/style.css">
|
||||
<title>Kubeconform - Fast Kubernetes manifests validation! | The execution model of AWS Lambda@edge with Cloudfront's two- and three-tiered architecture</title>
|
||||
</head>
|
||||
<body><div id="title">
|
||||
<h1>Kubeconform</h1>
|
||||
<h2>A FAST Kubernetes manifests validator</h2>
|
||||
</div>
|
||||
<div id="main-container">
|
||||
|
||||
<div id="post">
|
||||
|
||||
<a href="/" id="back">← Back</a>
|
||||
<h1>The execution model of AWS Lambda@edge with Cloudfront's two- and three-tiered architecture <div class="date">July 2, 2021</div></h1>
|
||||
|
||||
<p>Installation</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div><div id="footer">
|
||||
<h3>GitHub</h3>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
4
site/public/js/prism.js
Normal file
4
site/public/js/prism.js
Normal file
File diff suppressed because one or more lines are too long
1
site/public/sitemap.xml
Normal file
1
site/public/sitemap.xml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"><url><loc>http://kubeconform.mandragor.org/docs/overview/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/docs/installation/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/docs/usage/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/docs/crd-support/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/docs/json-schema-conversion/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/docs/usage-as-github-action/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/tags/about/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/about/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/docs/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/tags/installation/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/tags/kubeconform/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/tags/overview/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/tags/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/tags/usage/</loc><lastmod>2021-07-02T00:00:00+00:00</lastmod></url><url><loc>http://kubeconform.mandragor.org/categories/</loc></url></urlset>
|
||||
3
site/public/tags/about/index.xml
Normal file
3
site/public/tags/about/index.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>About on Kubeconform - Fast Kubernetes manifests validation!</title><link>http://kubeconform.mandragor.org/tags/about/</link><description>Recent content in About on Kubeconform - Fast Kubernetes manifests validation!</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 02 Jul 2021 00:00:00 +0000</lastBuildDate><atom:link href="http://kubeconform.mandragor.org/tags/about/index.xml" rel="self" type="application/rss+xml"/><item><title>About</title><link>http://kubeconform.mandragor.org/about/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/about/</guid><description>Kubeconform is a Kubernetes manifests validation tool. Build it into your CI to validate your Kubernetes configuration!
|
||||
It is inspired by, contains code from and is designed to stay close to Kubeval, but with the following improvements:
|
||||
high performance: will validate &amp; download manifests over multiple routines, caching downloaded files in memory configurable list of remote, or local schemas locations, enabling validating Kubernetes custom resources (CRDs) and offline validation capabilities uses by default a self-updating fork of the schemas registry maintained by the kubernetes-json-schema project - which guarantees up-to-date schemas for all recent versions of Kubernetes.</description></item></channel></rss>
|
||||
20
site/public/tags/cloudfront/index.xml
Normal file
20
site/public/tags/cloudfront/index.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Cloudfront on Kubeconform - Fast Kubernetes manifests validation!</title>
|
||||
<link>http://localhost/tags/cloudfront/</link>
|
||||
<description>Recent content in Cloudfront on Kubeconform - Fast Kubernetes manifests validation!</description>
|
||||
<generator>Hugo -- gohugo.io</generator>
|
||||
<language>en-us</language>
|
||||
<lastBuildDate>Fri, 02 Jul 2021 00:00:00 +0000</lastBuildDate><atom:link href="http://localhost/tags/cloudfront/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>The execution model of AWS Lambda@edge with Cloudfront's two- and three-tiered architecture</title>
|
||||
<link>http://localhost/installation/</link>
|
||||
<pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate>
|
||||
|
||||
<guid>http://localhost/installation/</guid>
|
||||
<description>Installation</description>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
1
site/public/tags/index.xml
Normal file
1
site/public/tags/index.xml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Tags on Kubeconform - Fast Kubernetes manifests validation!</title><link>http://kubeconform.mandragor.org/tags/</link><description>Recent content in Tags on Kubeconform - Fast Kubernetes manifests validation!</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 02 Jul 2021 00:00:00 +0000</lastBuildDate><atom:link href="http://kubeconform.mandragor.org/tags/index.xml" rel="self" type="application/rss+xml"/><item><title>About</title><link>http://kubeconform.mandragor.org/tags/about/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/tags/about/</guid><description/></item><item><title>Installation</title><link>http://kubeconform.mandragor.org/tags/installation/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/tags/installation/</guid><description/></item><item><title>Kubeconform</title><link>http://kubeconform.mandragor.org/tags/kubeconform/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/tags/kubeconform/</guid><description/></item><item><title>Overview</title><link>http://kubeconform.mandragor.org/tags/overview/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/tags/overview/</guid><description/></item><item><title>Usage</title><link>http://kubeconform.mandragor.org/tags/usage/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/tags/usage/</guid><description/></item></channel></rss>
|
||||
4
site/public/tags/installation/index.xml
Normal file
4
site/public/tags/installation/index.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Installation on Kubeconform - Fast Kubernetes manifests validation!</title><link>http://kubeconform.mandragor.org/tags/installation/</link><description>Recent content in Installation on Kubeconform - Fast Kubernetes manifests validation!</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 02 Jul 2021 00:00:00 +0000</lastBuildDate><atom:link href="http://kubeconform.mandragor.org/tags/installation/index.xml" rel="self" type="application/rss+xml"/><item><title>Installation</title><link>http://kubeconform.mandragor.org/docs/installation/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/installation/</guid><description>Linux Download the latest release from our release page.
|
||||
For example, for Linux on x86_64 architecture:
|
||||
curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xvzf - && \ sudo mv kubeconform /usr/local/bin/ MacOs Kubeconform is available to install using Homebrew: $ brew install kubeconform
|
||||
Windows Download the latest release from our release page.</description></item></channel></rss>
|
||||
13
site/public/tags/kubeconform/index.xml
Normal file
13
site/public/tags/kubeconform/index.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kubeconform on Kubeconform - Fast Kubernetes manifests validation!</title><link>http://kubeconform.mandragor.org/tags/kubeconform/</link><description>Recent content in Kubeconform on Kubeconform - Fast Kubernetes manifests validation!</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 02 Jul 2021 00:00:00 +0000</lastBuildDate><atom:link href="http://kubeconform.mandragor.org/tags/kubeconform/index.xml" rel="self" type="application/rss+xml"/><item><title>Overview</title><link>http://kubeconform.mandragor.org/docs/overview/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/overview/</guid><description>Kubeconform is a Kubernetes manifests validation tool, and checks whether your Kubernetes manifests are valid, according to Kubernetes resources definitions.
|
||||
It is inspired by, contains code from and is designed to stay close to Kubeval, but with the following improvements:
|
||||
high performance: will validate &amp; download manifests over multiple routines, caching downloaded files in memory configurable list of remote, or local schemas locations, enabling validating Kubernetes custom resources (CRDs) and offline validation capabilities uses by default a self-updating fork of the schemas registry maintained by the kubernetes-json-schema project - which guarantees up-to-date schemas for all recent versions of Kubernetes.</description></item><item><title>Installation</title><link>http://kubeconform.mandragor.org/docs/installation/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/installation/</guid><description>Linux Download the latest release from our release page.
|
||||
For example, for Linux on x86_64 architecture:
|
||||
curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xvzf - && \ sudo mv kubeconform /usr/local/bin/ MacOs Kubeconform is available to install using Homebrew: $ brew install kubeconform
|
||||
Windows Download the latest release from our release page.</description></item><item><title>Usage</title><link>http://kubeconform.mandragor.org/docs/usage/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage/</guid><description>$ ./bin/kubeconform -h Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]... -cache string cache schemas downloaded via HTTP to this folder -cpu-prof string debug - log CPU profiling to file -exit-on-error immediately stop execution when the first error is encountered -h show help information -ignore-filename-pattern value regular expression specifying paths to ignore (can be specified multiple times) -ignore-missing-schemas skip files with missing schemas instead of failing -insecure-skip-tls-verify disable verification of the server's SSL certificate.</description></item><item><title>Custom Resources support</title><link>http://kubeconform.mandragor.org/docs/crd-support/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/crd-support/</guid><description>When the -schema-location parameter is not used, or set to &ldquo;default&rdquo;, kubeconform will default to downloading schemas from https://github.com/yannh/kubernetes-json-schema. Kubeconform however supports passing one, or multiple, schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions in each of them, in order, stopping as soon as a matching file is found.
|
||||
If the -schema-location value does not end with &lsquo;.json&rsquo;, Kubeconform will assume filenames / a file structure identical to that of kubernetesjsonschema.</description></item><item><title>OpenAPI to JSON Schema conversion</title><link>http://kubeconform.mandragor.org/docs/json-schema-conversion/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/json-schema-conversion/</guid><description>Kubeconform uses JSON schemas to validate Kubernetes resources. For custom resources, the CustomResourceDefinition first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions to JSON schema. Here is an example how to use it:
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>GitHub Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to GitHub&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a GitHub Action, once logged in using a GitHub Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to GitHub Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on GitHub Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
Kubeconform contains a package that can be used as a library. An example of usage can be found in examples/main.go
|
||||
Additional documentation on pkg.go.dev</description></item><item><title>About</title><link>http://kubeconform.mandragor.org/about/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/about/</guid><description>Kubeconform is a Kubernetes manifests validation tool. Build it into your CI to validate your Kubernetes configuration!
|
||||
It is inspired by, contains code from and is designed to stay close to Kubeval, but with the following improvements:
|
||||
high performance: will validate &amp; download manifests over multiple routines, caching downloaded files in memory configurable list of remote, or local schemas locations, enabling validating Kubernetes custom resources (CRDs) and offline validation capabilities uses by default a self-updating fork of the schemas registry maintained by the kubernetes-json-schema project - which guarantees up-to-date schemas for all recent versions of Kubernetes.</description></item></channel></rss>
|
||||
20
site/public/tags/lambdaedge/index.xml
Normal file
20
site/public/tags/lambdaedge/index.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Lambda@edge on Kubeconform - Fast Kubernetes manifests validation!</title>
|
||||
<link>http://localhost/tags/lambdaedge/</link>
|
||||
<description>Recent content in Lambda@edge on Kubeconform - Fast Kubernetes manifests validation!</description>
|
||||
<generator>Hugo -- gohugo.io</generator>
|
||||
<language>en-us</language>
|
||||
<lastBuildDate>Fri, 02 Jul 2021 00:00:00 +0000</lastBuildDate><atom:link href="http://localhost/tags/lambdaedge/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>The execution model of AWS Lambda@edge with Cloudfront's two- and three-tiered architecture</title>
|
||||
<link>http://localhost/installation/</link>
|
||||
<pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate>
|
||||
|
||||
<guid>http://localhost/installation/</guid>
|
||||
<description>Installation</description>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
6
site/public/tags/usage/index.xml
Normal file
6
site/public/tags/usage/index.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Usage on Kubeconform - Fast Kubernetes manifests validation!</title><link>http://kubeconform.mandragor.org/tags/usage/</link><description>Recent content in Usage on Kubeconform - Fast Kubernetes manifests validation!</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 02 Jul 2021 00:00:00 +0000</lastBuildDate><atom:link href="http://kubeconform.mandragor.org/tags/usage/index.xml" rel="self" type="application/rss+xml"/><item><title>Usage</title><link>http://kubeconform.mandragor.org/docs/usage/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage/</guid><description>$ ./bin/kubeconform -h Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]... -cache string cache schemas downloaded via HTTP to this folder -cpu-prof string debug - log CPU profiling to file -exit-on-error immediately stop execution when the first error is encountered -h show help information -ignore-filename-pattern value regular expression specifying paths to ignore (can be specified multiple times) -ignore-missing-schemas skip files with missing schemas instead of failing -insecure-skip-tls-verify disable verification of the server's SSL certificate.</description></item><item><title>Custom Resources support</title><link>http://kubeconform.mandragor.org/docs/crd-support/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/crd-support/</guid><description>When the -schema-location parameter is not used, or set to &ldquo;default&rdquo;, kubeconform will default to downloading schemas from https://github.com/yannh/kubernetes-json-schema. Kubeconform however supports passing one, or multiple, schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions in each of them, in order, stopping as soon as a matching file is found.
|
||||
If the -schema-location value does not end with &lsquo;.json&rsquo;, Kubeconform will assume filenames / a file structure identical to that of kubernetesjsonschema.</description></item><item><title>OpenAPI to JSON Schema conversion</title><link>http://kubeconform.mandragor.org/docs/json-schema-conversion/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/json-schema-conversion/</guid><description>Kubeconform uses JSON schemas to validate Kubernetes resources. For custom resources, the CustomResourceDefinition first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions to JSON schema. Here is an example how to use it:
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>GitHub Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to GitHub&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a GitHub Action, once logged in using a GitHub Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to GitHub Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on GitHub Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
Kubeconform contains a package that can be used as a library. An example of usage can be found in examples/main.go
|
||||
Additional documentation on pkg.go.dev</description></item></channel></rss>
|
||||
20
site/themes/kubeconform/LICENSE
Normal file
20
site/themes/kubeconform/LICENSE
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Yann Hamon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
3
site/themes/kubeconform/archetypes/default.md
Normal file
3
site/themes/kubeconform/archetypes/default.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
+++
|
||||
|
||||
+++
|
||||
0
site/themes/kubeconform/layouts/404.html
Normal file
0
site/themes/kubeconform/layouts/404.html
Normal file
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue