Compare commits

...

148 commits

Author SHA1 Message Date
Tomasz Janiszewski
e60892483e
Fix typo (#339) 2025-10-13 12:36:27 +02:00
George Gaál
c7f8490e52
fix: Github -> GitHub (#340)
Signed-off-by: George Gaál <gb12335@gmail.com>
2025-10-13 12:36:06 +02:00
Yann Hamon
e65429b1e5
Add support for duration (#328)
* Add custom validation logic for durations
2025-05-12 11:15:53 +02:00
Yann Hamon
a23275d5ca
Invalid JSON should not be considered an error - see https://github.com/yannh/kubeconform/issues/67 (#327) 2025-05-12 10:21:02 +02:00
Yann Hamon
3134f4477e
Add acceptance tests for caching of references (#326) 2025-05-12 08:14:28 +02:00
Yann Hamon
9f04fec268
Add tests for the HTTP loader (#325)
Add another test case, remove accidental double memory caching
2025-05-11 04:13:07 +02:00
Yann Hamon
31e9679c96
Update jsonschema library to v6 (#324) 2025-05-11 02:05:01 +02:00
Yann Hamon
df26febc54
Update go/goreleaser (#322) 2025-05-10 18:20:42 +02:00
Yann Hamon
1bd44986dd
Update some dependencies (#283) 2024-07-30 23:34:40 +02:00
Yann Hamon
43a2445cb4
Retry (#282)
* fix: use hashicorp/go-retryablehttp to retry failed schema downloads


---------

Co-authored-by: Carlos Sanchez <carlos@apache.org>
2024-07-30 23:22:45 +02:00
Yann Hamon
706cd56e87
Revert "fix: retry on download errors (#274)" (#279)
This reverts commit 50ce5f8ecb.
2024-07-29 10:37:43 +02:00
Carlos Sanchez
50ce5f8ecb
fix: retry on download errors (#274)
* Retry on download errors

---------

Co-authored-by: Yann Hamon <yann@mandragor.org>
2024-07-29 10:10:44 +02:00
guoguangwu
347cd5e4c9
fix: close resource (#272) 2024-06-30 15:15:53 +02:00
Yann Hamon
142517fc45 fix go.mod 2024-05-09 23:48:09 +02:00
Yann Hamon
7062384492
Slightly improve the coverage of the validator test (#271) 2024-05-09 16:37:20 +02:00
Michael Lee
20805f652c
Stop validating output of closed channel in Validate (#265)
Currently, Validate and ValidateWithContext always returns a
result with status `Empty` and a `missing 'kind' key` error as
the final item in the returned slice.

This is because ValidateWithContext currently will parse the output of
`resourcesChan`, even when the context is finished and we get back
a default `Resource` struct.

This PR modifies the code to skip validating this case.
2024-05-09 16:27:59 +02:00
Yann Hamon
9627dd185b
Update go version in go.mod (#269)
* Downgrade to latest version supported by goreleaser
2024-05-09 15:43:13 +02:00
Yann Hamon
14053aaa54
Update Go & Base images (#268)
* Update go and base images
2024-05-09 15:19:30 +02:00
Yann Hamon
71a59d74f2
Remove deprecated Maintainer tag in Dockerfile (#267) 2024-05-09 15:09:22 +02:00
PatDyn
ad166c7f0d
Sanitize csv strings (#258)
* Support spaces before/after commas when passing list of Kinds
2024-05-09 14:21:36 +02:00
David Ongaro
a8000fd445
Update kubeconform -h output (#260)
In particular, the `-output pretty` option is missing.
2024-02-02 22:17:49 +01:00
Yann Hamon
b6728f181c
Fix junit output, also ensure junit output is deterministic (#253) 2023-12-24 18:06:03 +01:00
Yann Hamon
a4d74ce7d2
Fail early on incorrect version of k8s (#254)
* Fail early on incorrect version of k8s

* fix tests
2023-12-24 18:05:04 +01:00
Yann Hamon
808e6d4aa5
Update GH actions and goreleaser (#252)
* Update GH actions and goreleaser
2023-12-24 14:26:09 +01:00
Yann Hamon
d8f00a3a30
Update Golang to 1.21.4 (#245) 2023-11-18 18:31:34 +01:00
Yann Hamon
6ae8c45bc1
openapi2jsonschema.py now correctly fails if no FILE is passed (#244)
* openapi2jsonschema.py now correctly fails if no FILE is passed
* Update acceptance tests
2023-11-18 18:08:18 +01:00
Lucien Boix
b7d7b4d0dc
Update Readme.md (#232)
* Update Readme.md

Just adding details for how to use the script command with args for the Gitlab CI usecase

* Update Readme.md

Good catch!

Co-authored-by: Yann Hamon <yannh@users.noreply.github.com>

---------

Co-authored-by: Yann Hamon <yannh@users.noreply.github.com>
2023-09-19 00:54:24 +02:00
Yann Hamon
2e50b79b16
Update Go and Goreleaser to 1.20, update dependencies (#231) 2023-09-04 00:11:25 +02:00
Yann Hamon
13a78ebad8
Avoid unnecessary type conversions (#222) 2023-07-16 10:27:26 +02:00
Yann Hamon
ae67bb4709
Force Draft version of JsonSchema (#221)
* Force Draft version of JsonSchema
* Add test validating using CRD that misses explicit draft version
2023-07-16 09:42:11 +02:00
Yamamoto, Hirotaka
278385f4c9
Update Readme.md to add fullgroup explanation (#220)
This is a follow-up to #219 to add a short description on how to use the `fullgroup` variable.
2023-07-09 13:46:12 +02:00
Yamamoto, Hirotaka
452f1fe1db
Fix #130 in a backward-compatible way (#219)
This is an alternative way to fix #130.
Instead of changing the `group` variable content, this commit adds
a new variable `fullgroup` that does not split the group components.

With this, users can specify the filename format like:

    FILENAME_FORMAT='{fullgroup}-{version}-{kind}'
2023-07-09 12:51:30 +02:00
Yann Hamon
f0a7d5203d
Update Readme.md 2023-06-26 14:48:15 +02:00
Will Yardley
71fd5f8386
fix: add missing output formats in error message (#213)
- Add missing 'junit' and 'pretty' output formats.
- Use quotes vs. backticks around command name
2023-06-14 22:41:51 +02:00
Denis N. Antonioli
c8bce62898
Fix for 196: Multi-architecture image (#204)
* 196: qemu

* 196: multi-arch; see also https://blog.devgenius.io/goreleaser-build-multi-arch-docker-images-8dd9a7903675
2023-06-07 12:26:22 +02:00
Juan Ignacio Donoso
065fad003f
Fix anchored link on Readme.md (#205)
The in page link to the CustomResourceDefinition (CRD) Support section is not working
2023-06-07 12:22:58 +02:00
Yann Hamon
c1a2c159de Revert "Add support for Arm64 Docker images (#201)"
This reverts commit 65cfe7e16e.
2023-05-14 12:52:13 +02:00
Yann Hamon
65cfe7e16e Add support for Arm64 Docker images (#201) 2023-05-14 12:51:53 +02:00
Yann Hamon
ce2f6de185 Move cfg parsing out of realmain, rename realmain to kubeconform 2023-04-23 14:34:25 +02:00
Yann Hamon
ad935b7e32 Add JSON/YAML annotations to Config struct
Co-authored-by: Ahmed AbouZaid <6760103+aabouzaid@users.noreply.github.com>
2023-04-23 13:55:09 +02:00
Yann Hamon
d038bf8840 Do not hardcode output stream in pkg/output
Co-authored-by: Ahmed AbouZaid <6760103+aabouzaid@users.noreply.github.com>
2023-04-23 13:39:43 +02:00
Yann Hamon
8bc9f42f39
Add support for "pretty" output (#195)
* feat: add pretty output format

---------

Co-authored-by: William Yardley <wyardley@users.noreply.github.com>
2023-04-22 18:41:51 +02:00
Mateusz Łoskot
9294e94a8d
docs: Add winget as installation method on Windows (#192)
The kubeconform package has been accepted to winget,
see https://github.com/microsoft/winget-pkgs/pull/101691
2023-04-13 09:56:49 +02:00
Benjamin Muschko
16d52804d4
Fix CI badge image (#184)
I guess the name of the CI build has been changed which led to a broken image.
2023-03-28 18:53:49 +02:00
Aleksey Levenstein
e3bb34851d
fix: expose error instance path instead of schema path (#177) 2023-02-27 16:16:00 +01:00
w7089
aaecabe0b7
support disabling ssl validation in openapi2jsonschema.py (#167)
* support disabling ssl validation in openapi2jsonschema.py
* added acceptance tests for disable ssl feature
* speed up bats docker build
2023-02-26 12:33:54 +01:00
Yann Hamon
563e1db94c
Try to expose JSON paths (#173)
* Try to expose JSON paths
* update validationErrors format in json output
* Add test to JSON output with validationError
2023-02-26 12:32:51 +01:00
Rick
9860cde144
feat: support to set an alternative image owner (#164)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2023-01-25 01:26:53 +01:00
Yann Hamon
ee7c498580
Migrate to santhosh-tekuri/jsonschema (#168)
* Migrate to santhosh-tekuri/jsonschema
2023-01-23 19:22:20 +01:00
Jiri Tyr
84afe70659
Documenting Helm support via 3rd party repo (#158) 2022-12-27 17:21:12 +01:00
Jeffrey Ying
752a33eaeb
Fix typo in readme (#153) 2022-12-02 19:34:19 +01:00
Yann Hamon
33cdbf16a4 Update LICENSE 2022-11-28 10:02:25 +01:00
Eyar Zilberman
f94844183f
update anchor links in readme (#150)
* add constructor to handle equal sign (=)

Equal sign (=) was not parsed properly by pyyaml.
Added constructor to parse equal sign as string.
Related issue: https://github.com/yannh/kubeconform/issues/103

* Update Readme.md

* Update Readme.md
2022-11-10 13:28:58 +01:00
Eyar Zilberman
9d34445328
update readme structure and info (#149)
* add constructor to handle equal sign (=)

Equal sign (=) was not parsed properly by pyyaml.
Added constructor to parse equal sign as string.
Related issue: https://github.com/yannh/kubeconform/issues/103

* Update Readme.md
2022-11-10 12:36:28 +01:00
Yann Hamon
a31707ca58
Add opencontainers label (#138) 2022-10-16 15:50:02 +02:00
Yann Hamon
46b7622a08
Add informations how to use Kubeconform in Gitlab-CI (#139) 2022-10-16 15:46:22 +02:00
Andrea Tosatto
d8e348a597
Allow to skip resources using the GVK notation (#92)
* Allow to skip resources using the GVK notation
* Update flags description, add integration tests and update readme

Co-authored-by: Yann Hamon <yann@mandragor.org>
2022-10-16 14:59:48 +02:00
Yann Hamon
466ec73ed7
Update goreleaser (#137)
* Update goreleaser
2022-10-16 14:14:09 +02:00
Yann Hamon
dbcd787256
Do not expose internal CPU profiling option as flag (#136) 2022-10-16 14:06:42 +02:00
Yann Hamon
f5338b07f9
Validate JUnit output against Jenkins JUnit XSD (#134)
* Validate JUnit output against Jenkins JUnix XSD

* Add missing Jenkins JUnit XSD

* Add time to TestCase for #127
2022-10-16 13:52:04 +02:00
Yann Hamon
f68d6ec6ea
Add debug information to help understand failures finding schemas (#133)
* Add debug information to help understand failures finding schemas

* Add debug information to help understand failures finding schemas
2022-10-16 12:28:11 +02:00
Yann Hamon
3cb76bc5e6 Update fixture file to use no hash function 2022-09-02 10:39:16 +02:00
John-Michael Mulesa
5cbbd1a898
Upgrade hash to sha256. (#126) 2022-09-02 10:28:07 +02:00
Thomas Güttler
321cc0ea1d
remove link to dead project (#125)
the old repo has not updates since two years. It looks dead.
2022-08-31 09:08:01 +02:00
hadar-co
5e63bc5ad7
add {{ .Group }} variable to schema location options (#120) 2022-07-16 14:13:17 +02:00
Bill Franklin
9a6fff13cb
Forbid duplicate keys in strict mode (#121)
* Forbid duplicate keys in strict mode

Prevents specifying duplicate keys using
UnmarshallStrict: https://pkg.go.dev/gopkg.in/yaml.v2#UnmarshalStrict

* Add acceptance tests for duplicated properties
2022-07-15 14:23:10 +02:00
Calle Pettersson
7bf1e01dec
fix: change flag parse error handling to return errors instead of exiting (#107)
* fix: change flag parse error handling to return errors instead of exiting

Having ExitOnError in combination with SetOutput to a buffer instead of
stdout/stderr means flags.Parse output is swallowed and kubeconform silently
exits directly with exit code 2 instead of returning the error.

Setting ContinueOnError instead returns the error, and writes usage help to
the buffer, so error handling code in main is reached.

* Add test for parsing incorrect flags

Co-authored-by: Yann Hamon <yann@mandragor.org>
2022-06-19 18:38:52 +02:00
Damian Kula
014cbf754f
Adds info about installation via built in Go package manger (#116) 2022-06-19 18:23:23 +02:00
Andrzej Theodorko
f8ab9ae49e
Fix grammar in README.md (#114) 2022-06-03 13:11:11 +02:00
Eyar Zilberman
b5f34caa70
add constructor to handle equal sign (=) (#104)
Equal sign (=) was not parsed properly by pyyaml.
Added constructor to parse equal sign as string.
Related issue: https://github.com/yannh/kubeconform/issues/103
2022-04-06 09:12:48 +02:00
Carlos Sanchez
932b35d71f
chore: print the url of failed download (#96) 2022-02-22 23:59:39 +01:00
Yann Hamon
c5f7348af8 remove accidentally commited binary 2022-02-07 01:20:38 +01:00
Yann Hamon
85b30f8c2a
Improve handling of cmdline errors (#91)
Improve handling of cmdline errors
2022-01-06 12:39:24 +01:00
Yann Hamon
607c90a0a9 Minor css updates 2021-12-20 00:43:36 +01:00
Yann Hamon
d10c9bde67 Update website 2021-12-19 23:46:04 +01:00
Yann Hamon
2b3139b1db Add cname to website 2021-12-19 22:57:50 +01:00
Yann Hamon
3a3d05b27c Publish site 2021-12-19 22:53:04 +01:00
Yann Hamon
6c1fa513e9 Fixes acceptance test with new TAP output 2021-12-19 01:15:31 +01:00
Yann Hamon
048ccf125e
Merge pull request #87 from eyarz/master
add hero image to readme
2021-12-18 23:51:01 +01:00
Yann Hamon
c32562e742
Merge pull request #88 from maxbrunet/feature/output-tap/qualified-name
feat(output/tap): Output qualified resource name
2021-12-18 23:49:27 +01:00
Maxime Brunet
ff2ab3d770
feat(output/tap): Output qualified resource name 2021-12-17 19:18:55 -08:00
Eyar Zilberman
e38ff8efd4
add horizontal line 2021-12-13 22:51:06 +02:00
Eyar Zilberman
836b6e5b14
set hero image width 2021-12-13 22:48:21 +02:00
Eyar Zilberman
fe79a7cfff
Merge branch 'yannh:master' into master 2021-12-13 22:45:35 +02:00
Yann Hamon
c1b3e93a75
Merge pull request #86 from PaytmLabs/hotfix/scripts/root-additionalProperties
scripts: Optionally disallow additionalProperties at the root
2021-12-13 00:03:38 +01:00
Maxime Brunet
30528c5671
scripts: Add tests for options 2021-12-12 11:44:09 -08:00
Maxime Brunet
67a73a9315
scripts: Optionally disallow additionalProperties at the root 2021-12-11 11:40:06 -08:00
Eyar Zilberman
c7894baed6
Update Readme.md 2021-11-18 14:02:43 +02:00
Yann Hamon
f2e47c3596
Merge pull request #83 from z0mbix/master
Add -v flag to output version
2021-11-16 18:21:53 +01:00
z0mbix
c3e6205f15
Move version to main package 2021-11-16 17:19:54 +00:00
z0mbix
9410471142
Update README 2021-11-16 15:33:04 +00:00
z0mbix
d63de52458
Add -v to output version number 2021-11-16 15:30:33 +00:00
Yann Hamon
d536a659bd Fix broken release notes 2021-09-27 01:15:38 +02:00
Yann Hamon
ea3c592d63 HTTP acceptance test for openapi2jsonschema 2021-09-26 23:27:03 +02:00
Yann Hamon
aebc298047
Merge pull request #78 from tarioch/patch-1
Add missing import urllib.request
2021-09-26 23:06:52 +02:00
Patrick Ruckstuhl
a30381c6aa
Add missing import urllib.request 2021-09-26 23:00:44 +02:00
Yann Hamon
ab4745ddf0 Small clean-up 2021-09-26 22:55:01 +02:00
Yann Hamon
b571b18f8d Rename acceptance tests for consistency 2021-09-26 18:00:36 +02:00
Yann Hamon
563106ede1
Merge pull request #76 from yannh/support-for-larger-files
Support for larger files
2021-09-26 17:49:44 +02:00
Yann Hamon
b5c823d6b5 Also read up to 256MB from stdin 2021-09-26 17:45:27 +02:00
Yann Hamon
4a8aace275 Add acceptance test 2021-09-26 17:40:58 +02:00
Yann Hamon
8bb87b9a63 Allow bufio.Scanner to resize buffer when reading large files 2021-09-26 17:27:20 +02:00
Yann Hamon
ff7da0f73b
Merge pull request #75 from yannh/openapi2jsonschema-tests
Add simple openapi2jsonschema unit tests
2021-09-26 17:04:04 +02:00
Yann Hamon
19be42b9a6 simple openapi2jsonschema tests 2021-09-26 17:02:34 +02:00
Yann Hamon
dcc77ac3a3
Merge pull request #72 from tarioch/feature/handle_items_in_openapi2jsonschema
Also support items elements in yaml
2021-09-26 14:07:52 +02:00
Yann Hamon
f8dcb19789
Merge pull request #64 from PaytmLabs/hotfix/scripts/versions-before-version
I added some _very_ basic regression test, which checks that the output I am currently getting for a single resource from the prometheus operator does not change. It's not perfect, but changes that do not break that test are unlikely to break for me.
2021-09-26 14:00:06 +02:00
Yann Hamon
72d648a5d1
Merge pull request #74 from yannh/add-regression-tests-openapi2jsonschema
Add regression tests openapi2jsonschema
2021-09-26 13:51:08 +02:00
Yann Hamon
468d42f556 Add simple regression test for openapi2jsonschema 2021-09-26 13:49:37 +02:00
Yann Hamon
dfd7a5a102 Add simple regression tests for openapi2jsonschema 2021-09-26 13:46:05 +02:00
Yann Hamon
0bf6b97269
Merge pull request #71 from schuhu/httpproxy
FIX: enable http_proxy environment variable
2021-09-26 12:18:47 +02:00
Yann Hamon
54e0b8f5bb Update README.md with Proxy support infos 2021-09-26 12:14:53 +02:00
Yann Hamon
73f65d7530 Add acceptance test for HTTPS_PROXY support 2021-09-26 12:11:42 +02:00
Yann Hamon
4e83800979 gofmt 2021-09-26 11:58:09 +02:00
Patrick Ruckstuhl
9228dba915 Also support items elements in yaml, e.g. for kubectl get crds -o yaml 2021-09-25 01:09:08 +02:00
Christian Brauchli
dee75355d0 FIX: enable http_proxy environment variable 2021-09-17 09:32:36 +02:00
Yann Hamon
4544f45fa1 Also release an alpine variant of the Docker image for Gitlab CI 2021-08-29 13:06:30 +02:00
Yann Hamon
2eefa7fc22 Update Junit tests, fix #45 2021-08-29 12:57:25 +02:00
Yann Hamon
1b01a42488 Fix Makefile - remove duplicate target 2021-08-29 12:33:24 +02:00
Yann Hamon
80d2203a5a Add project_name to goreleaser file 2021-08-29 12:07:06 +02:00
Yann Hamon
aadb07a585 Fix CI 2021-08-29 12:00:20 +02:00
Yann Hamon
5bce1d4180 Generate all release artifacts using Goreleaser, update Makefile 2021-08-29 11:55:17 +02:00
Yann Hamon
ec52b39db3 Update goreleaser 2021-08-29 11:41:20 +02:00
Yann Hamon
ab7fd7f97d Go mod tidy 2021-08-22 19:13:13 +02:00
Yann Hamon
ae74fa6e53
Merge pull request #69 from chenrui333/go-1.17
build: bump to use go1.17
2021-08-22 19:04:16 +02:00
Rui Chen
cf50a6c9da merge require blocks
Signed-off-by: Rui Chen <rui@chenrui.dev>
2021-08-22 02:15:34 -04:00
Rui Chen
727f5fe57e build: bump to use go1.17
Signed-off-by: Rui Chen <rui@chenrui.dev>
2021-08-17 01:31:46 -04:00
Yann Hamon
98c80939c1
Merge pull request #68 from vcardenas/handle-invalid-schemas-http-registries
Properly handle successful http requests to registries sending invalid schema responses
2021-08-07 16:17:17 +02:00
Victor Cardenas
44b7ba9aef Properly handle successful http requests to registries sending invalid schema responses 2021-08-03 19:46:18 -04:00
Maxime Brunet
c489a69a4c
scripts: Check versions before version
CRD versions can have multiple schemas, but they can also share the
same.

https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#specify-multiple-versions
2021-07-12 17:23:42 -07:00
Yann Hamon
f8ffb2f9e3
Merge pull request #62 from yannh/use-master-by-default
Validate against master by default, not 1.18.0
2021-07-11 23:23:22 +02:00
Yann Hamon
8721f4c32a
Merge pull request #63 from yannh/updates-deps
Update dependencies, add Makefile target
2021-07-11 23:15:53 +02:00
Yann Hamon
7be447f44f Update dependencies, add Makefile target 2021-07-11 23:14:05 +02:00
Yann Hamon
0e22547520 Fix acceptance tests 2021-07-11 23:01:31 +02:00
Yann Hamon
53a1689bac Fix unit tests 2021-07-11 22:57:20 +02:00
Yann Hamon
3a697f3ce2 validate against master by default, not 1.18.0 2021-07-11 22:53:23 +02:00
Yann Hamon
0a14aae014
Merge pull request #56 from chenrui333/go-1.16
chore: update to use go 1.16
2021-07-04 20:49:19 +02:00
Rui Chen
892b3807fe chore: update makefile to use go1.16
Signed-off-by: Rui Chen <rui@chenrui.dev>
2021-07-03 17:15:55 -04:00
Rui Chen
affc63bb18 deps: use go 1.16 2021-07-03 17:14:51 -04:00
Yann Hamon
c4b044f3be
Merge pull request #54 from yannh/list-support
Support for lists
2021-07-03 16:03:21 +02:00
Yann Hamon
df386fc1fd Reformat strings 2021-07-03 16:01:51 +02:00
Yann Hamon
a053bbabe1 Merge branch 'master' of github.com:yannh/kubeconform into list-support 2021-07-03 15:58:25 +02:00
Yann Hamon
4eb75860d9 support for lists 2021-07-03 15:49:37 +02:00
Yann Hamon
5ed3ddac8a
Merge pull request #52 from chenrui333/doc/add-homebrew-install
doc: add homebrew install instruction
2021-07-01 17:16:03 +02:00
Yann Hamon
49d434dc4a
Update Readme.md 2021-07-01 17:15:31 +02:00
Rui Chen
803bf58797 add homebrew release badge
Signed-off-by: Rui Chen <rui@chenrui.dev>
2021-07-01 10:51:17 -04:00
Rui Chen
f1db99db6a doc: add homebrew install instruction
Signed-off-by: Rui Chen <rui@chenrui.dev>
2021-06-30 13:42:33 -04:00
Yann Hamon
2e7c2bfe33
Merge pull request #49 from davidholsgrove/skip-empty-docs
Skip empty yaml docs
2021-05-24 11:53:49 +02:00
David Holsgrove
d94454920b
Skip empty yaml docs - for yaml files with a structure like
```
# comment
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
```
2021-05-24 16:05:19 +10:00
291 changed files with 676107 additions and 10534 deletions

View file

@ -1,95 +1,50 @@
name: ci name: ci
on: push on: push
jobs: jobs:
test: kubeconform-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: test - name: test
run: make docker-test run: make docker-test
- name: build - name: build
run: make docker-build-static run: make goreleaser-build-static
- name: acceptance-test - name: acceptance-test
run: make docker-acceptance run: make docker-acceptance
- name: build-image openapi2jsonschema-test:
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:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
needs: test
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Download kubeconform image - name: test
uses: actions/download-artifact@v2 working-directory: ./scripts
with: run: make docker-test docker-acceptance
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
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: test needs:
- kubeconform-test
- openapi2jsonschema-test
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
steps: steps:
- name: checkout - 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 - 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: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

30
.github/workflows/site.yml vendored Normal file
View 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
View file

@ -1,2 +1,4 @@
dist/ dist/
bin/ bin/
.idea/
**/*.pyc

View file

@ -1,31 +1,103 @@
project_name: kubeconform
builds: builds:
- main: ./cmd/kubeconform - main: ./cmd/kubeconform
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
- GOFLAGS = -mod=vendor - GOFLAGS = -mod=vendor
- GO111MODULE = on - GO111MODULE = on
- GIT_OWNER = yannh
goos: goos:
- windows - windows
- linux - linux
- darwin - darwin
goarch:
- 386
- amd64
- arm
- arm64
flags: flags:
- -trimpath - -trimpath
- -tags=netgo - -tags=netgo
- -a - -a
ldflags: ldflags:
- -extldflags "-static" - -extldflags "-static"
- -X main.version={{.Tag}}
archives: archives:
- format: tar.gz - format: tar.gz
format_overrides: format_overrides:
- goos: windows - goos: windows
format: zip 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: checksum:
name_template: 'CHECKSUMS' name_template: 'CHECKSUMS'
snapshot: snapshot:
name_template: "{{ .Tag }}-next" name_template: "{{ .Tag }}-next"
changelog: changelog:
sort: asc sort: asc
filters: filters:
exclude: exclude:
- '^test:' - '^test:'

View file

@ -1,8 +1,14 @@
FROM alpine:latest as certs FROM alpine:3.21.3 as certs
RUN apk add ca-certificates RUN apk add ca-certificates
FROM scratch AS kubeconform 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 --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY bin/kubeconform / COPY kubeconform /
ENTRYPOINT ["/kubeconform"] ENTRYPOINT ["/kubeconform"]

12
Dockerfile-alpine Normal file
View 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"]

View file

@ -1,5 +1,5 @@
FROM bats/bats:v1.2.1 FROM bats/bats:1.11.0
RUN apk --no-cache add ca-certificates parallel RUN apk --no-cache add ca-certificates parallel libxml2-utils
COPY bin/kubeconform /code/bin/ COPY bin/kubeconform /code/bin/
COPY acceptance.bats acceptance-nonetwork.bats /code/ COPY acceptance.bats acceptance-nonetwork.bats /code/
COPY fixtures /code/fixtures COPY fixtures /code/fixtures

193
LICENSE
View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at 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 Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,

View file

@ -2,34 +2,27 @@
RELEASE_VERSION ?= latest 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: local-build:
go test -race ./... git config --global --add safe.directory $$PWD
build:
go build -o bin/ ./... go build -o bin/ ./...
docker-image: local-build-static:
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:
CGO_ENABLED=0 GOFLAGS=-mod=vendor GOOS=linux GOARCH=amd64 GO111MODULE=on go build -trimpath -tags=netgo -ldflags "-extldflags=\"-static\"" -a -o bin/ ./... 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-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-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: build-bats:
docker build -t bats -f Dockerfile.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 -t bats -p acceptance.bats
docker run --network none -t bats -p acceptance-nonetwork.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: 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
View file

@ -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>
[![Build status](https://github.com/yannh/kubeconform/workflows/build/badge.svg?branch=master)](https://github.com/yannh/kubeconform/actions?query=branch%3Amaster) [![Build status](https://github.com/yannh/kubeconform/actions/workflows/main.yml/badge.svg)](https://github.com/yannh/kubeconform/actions?query=branch%3Amaster)
[![Homebrew](https://img.shields.io/badge/dynamic/json.svg?url=https://formulae.brew.sh/api/formula/kubeconform.json&query=$.versions.stable&label=homebrew)](https://formulae.brew.sh/formula/kubeconform)
[![Go Report card](https://goreportcard.com/badge/github.com/yannh/kubeconform)](https://goreportcard.com/report/github.com/yannh/kubeconform) [![Go Report card](https://goreportcard.com/badge/github.com/yannh/kubeconform)](https://goreportcard.com/report/github.com/yannh/kubeconform)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/yannh/kubeconform/pkg/validator)](https://pkg.go.dev/github.com/yannh/kubeconform/pkg/validator) [![PkgGoDev](https://pkg.go.dev/badge/github.com/yannh/kubeconform/pkg/validator)](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 `Kubeconform` is a Kubernetes manifest validation tool. Incorporate it into your CI, or use it locally to validate your Kubernetes configuration!
configuration!
It is inspired by, contains code from and is designed to stay close to 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: [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 * configurable list of **remote, or local schemas locations**, enabling validating Kubernetes
custom resources (CRDs) and offline validation capabilities 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 * 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**. 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), 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 in a [file](https://github.com/kubernetes/kubernetes/blob/master/api/openapi-spec/swagger.json) checked into
the main Kubernetes repository. the main Kubernetes repository.
Because of the state of the tooling to perform validation against OpenAPI schemas, projects usually convert 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 [instrumenta/OpenApi2JsonSchema](https://github.com/instrumenta/openapi2jsonschema) to convert Kubernetes' Swagger file
and break it down into multiple JSON schemas, stored in github at 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 [instrumenta/kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) and published on
[kubernetesjsonschema.dev](https://kubernetesjsonschema.dev/). [kubernetesjsonschema.dev](https://kubernetesjsonschema.dev/).
Kubeconform relies on [a fork of kubernetes-json-schema](https://github.com/yannh/kubernetes-json-schema/) `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. that is more meticulously kept up-to-date, and contains schemas for all recent versions of Kubernetes.
### Limits of Kubeconform validation ### Limits of Kubeconform validation
Kubeconform, similarly to kubeval, only validates manifests using the OpenAPI specifications. In some `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.
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.
## 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 $ kubeconform -h
Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]... Usage: kubeconform [OPTION]... [FILE OR FOLDER]...
-cache string -cache string
cache schemas downloaded via HTTP to this folder cache schemas downloaded via HTTP to this folder
-cpu-prof string -debug
debug - log CPU profiling to file print debug information
-exit-on-error -exit-on-error
immediately stop execution when the first error is encountered immediately stop execution when the first error is encountered
-h show help information -h show help information
-ignore-filename-pattern value -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 -ignore-missing-schemas
skip files with missing schemas instead of failing skip files with missing schemas instead of failing
-insecure-skip-tls-verify -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 -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 -n int
number of goroutines to run concurrently (default 4) number of goroutines to run concurrently (default 4)
-output string -output string
output format - json, junit, tap, text (default "text") output format - json, junit, pretty, tap, text (default "text")
-reject string -reject string
comma-separated list of kinds to reject comma-separated list of kinds or GVKs to reject
-schema-location value -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 -skip string
comma-separated list of kinds to ignore comma-separated list of kinds or GVKs to ignore
-strict -strict
disallow additional properties not in schema disallow additional properties not in schema or duplicated keys
-summary -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 -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 ### Usage examples
* Validating a single, valid file * Validating a single, valid file
``` ```bash
$ ./bin/kubeconform fixtures/valid.yaml $ kubeconform fixtures/valid.yaml
$ echo $? $ echo $?
0 0
``` ```
* 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
``` ```bash
$ ./bin/kubeconform -summary -output json fixtures/invalid.yaml $ kubeconform -summary -output json fixtures/invalid.yaml
{ {
"resources": [ "resources": [
{ {
@ -118,88 +172,133 @@ $ echo $?
``` ```
* Passing manifests via Stdin * Passing manifests via Stdin
``` ```bash
cat fixtures/valid.yaml | ./bin/kubeconform -summary cat fixtures/valid.yaml | ./bin/kubeconform -summary
Summary: 1 resource found parsing stdin - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0 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 * 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/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 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 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 `Kubeconform` will respect the **HTTPS_PROXY** variable when downloading schema files.
schemas from `https://github.com/yannh/kubernetes-json-schema`. Kubeconform however supports passing one, or multiple,
```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 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. 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 * 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. 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 * 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. 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 `-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:
**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: Here are the variables you can use in -schema-location:
* *NormalizedKubernetesVersion* - Kubernetes Version, prefixed by v * *NormalizedKubernetesVersion* - Kubernetes Version, prefixed by v
* *StrictSuffix* - "-strict" or "" depending on whether validation is running in strict mode or not * *StrictSuffix* - "-strict" or "" depending on whether validation is running in strict mode or not
* *ResourceKind* - Kind of the Kubernetes Resource * *ResourceKind* - Kind of the Kubernetes Resource
* *ResourceAPIVersion* - Version of API used for the resource - "v1" in "apiVersion: monitoring.coreos.com/v1" * *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 Because Custom Resources (CR) are not native Kubernetes objects, they are not included in the default schema.
first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions 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: to JSON schema. Here is an example how to use it:
``` ```bash
$ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml $ 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 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}' $ 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 $ ./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 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/). 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: Example:
``` ```yaml
name: kubeconform name: kubeconform
on: push on: push
jobs: jobs:
@ -209,7 +308,7 @@ jobs:
- name: login to Github Packages - name: login to Github Packages
run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: docker://ghcr.io/yannh/kubeconform:master - uses: docker://ghcr.io/yannh/kubeconform:latest
with: with:
entrypoint: '/kubeconform' entrypoint: '/kubeconform'
args: "-summary -output json kubeconfigs/" 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 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. 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: The Kubeconform Docker image can be used in Gitlab-CI. Here is an example of a Gitlab-CI job:
```
$ 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
```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. **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) 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) 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 * @garethr for the [Kubeval](https://github.com/instrumenta/kubeval) and
[kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) projects ❤️ [kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) projects ❤️

View file

@ -19,3 +19,8 @@
run bin/kubeconform -schema-location 'fixtures/{{ .ResourceKind }}.json' -schema-location './fixtures/registry/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/test_crd.yaml run bin/kubeconform -schema-location 'fixtures/{{ .ResourceKind }}.json' -schema-location './fixtures/registry/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/test_crd.yaml
[ "$status" -eq 0 ] [ "$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 ]
}

View file

@ -11,6 +11,12 @@ resetCacheFolder() {
[ "${lines[0]}" == 'Usage: bin/kubeconform [OPTION]... [FILE OR FOLDER]...' ] [ "${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" { @test "Pass when parsing a valid Kubernetes config YAML file" {
run bin/kubeconform -summary fixtures/valid.yaml run bin/kubeconform -summary fixtures/valid.yaml
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
@ -30,7 +36,7 @@ resetCacheFolder() {
} }
@test "Pass when parsing a valid Kubernetes config JSON file" { @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 ] [ "$status" -eq 0 ]
[ "$output" = "Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 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" { @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 ] [ "$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" { @test "Pass when using a valid, preset -schema-location" {
run bin/kubeconform -schema-location default fixtures/valid.yaml run bin/kubeconform -schema-location default fixtures/valid.yaml
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
} }
@test "Pass when using a valid HTTP -schema-location" { @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 ] [ "$status" -eq 0 ]
} }
@ -159,6 +180,13 @@ resetCacheFolder() {
[ "$status" -eq 1 ] [ "$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" { @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 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 ] [ "$status" -eq 0 ]
@ -202,6 +230,18 @@ resetCacheFolder() {
[ "$output" = "fixtures/valid.yaml - bob ReplicationController skipped" ] [ "$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" { @test "Fail when parsing a resource from a kind to reject" {
run bin/kubeconform -verbose -reject ReplicationController fixtures/valid.yaml run bin/kubeconform -verbose -reject ReplicationController fixtures/valid.yaml
[ "$status" -eq 1 ] [ "$status" -eq 1 ]
@ -224,7 +264,7 @@ resetCacheFolder() {
@test "Fail when no schema found, ensure 404 is not cached on disk" { @test "Fail when no schema found, ensure 404 is not cached on disk" {
resetCacheFolder 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 ] [ "$status" -eq 1 ]
[ "$output" == 'fixtures/valid.yaml - ReplicationController bob failed validation: could not find schema for ReplicationController' ] [ "$output" == 'fixtures/valid.yaml - ReplicationController bob failed validation: could not find schema for ReplicationController' ]
[ "`ls cache/ | wc -l`" -eq 0 ] [ "`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" ] [ "$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" { @test "Produces correct TAP output" {
run bin/kubeconform -output tap fixtures/valid.yaml run bin/kubeconform -output tap fixtures/valid.yaml
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
[ "${lines[0]}" == 'TAP version 13' ] [ "${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' ] [ "${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 ]
}

View file

@ -15,6 +15,8 @@ import (
"github.com/yannh/kubeconform/pkg/validator" "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 { func processResults(cancel context.CancelFunc, o output.Output, validationResults <-chan validator.Result, exitOnError bool) <-chan bool {
success := true success := true
result := make(chan bool) result := make(chan bool)
@ -44,20 +46,11 @@ func processResults(cancel context.CancelFunc, o output.Output, validationResult
return result return result
} }
func realMain() int { func kubeconform(cfg config.Config) int {
cfg, out, err := config.FromFlags(os.Args[0], os.Args[1:]) var err error
if cfg.Help { cpuProfileFile := os.Getenv("KUBECONFORM_CPUPROFILE_FILE")
return 0 if cpuProfileFile != "" {
} else if out != "" { f, err := os.Create(cpuProfileFile)
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)
if err != nil { if err != nil {
log.Fatal("could not create CPU profile: ", err) log.Fatal("could not create CPU profile: ", err)
} }
@ -80,17 +73,18 @@ func realMain() int {
} }
var o output.Output 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) fmt.Fprintln(os.Stderr, err)
return 1 return 1
} }
var v validator.Validator
v, err := validator.New(cfg.SchemaLocations, validator.Opts{ v, err = validator.New(cfg.SchemaLocations, validator.Opts{
Cache: cfg.Cache, Cache: cfg.Cache,
Debug: cfg.Debug,
SkipTLS: cfg.SkipTLS, SkipTLS: cfg.SkipTLS,
SkipKinds: cfg.SkipKinds, SkipKinds: cfg.SkipKinds,
RejectKinds: cfg.RejectKinds, RejectKinds: cfg.RejectKinds,
KubernetesVersion: cfg.KubernetesVersion, KubernetesVersion: cfg.KubernetesVersion.String(),
Strict: cfg.Strict, Strict: cfg.Strict,
IgnoreMissingSchemas: cfg.IgnoreMissingSchemas, IgnoreMissingSchemas: cfg.IgnoreMissingSchemas,
}) })
@ -163,5 +157,27 @@ func realMain() int {
} }
func main() { 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))
} }

View 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#"
}

File diff suppressed because it is too large Load diff

View 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

View 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

View 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
View 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
View 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>

View file

@ -0,0 +1,4 @@
---
apiVersion: v1
kind: List
items: []

View 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

View file

@ -1,46 +1,34 @@
{ {
"apiVersion": "apps/v1beta1", "apiVersion": "v1",
"kind": "Deployment", "kind": "ReplicationController",
"metadata": { "metadata": {
"name": "nginx-deployment", "name": "bob"
"namespace": "default" },
}, "spec": {
"spec": { "replicas": 2,
"replicas": 2, "selector": {
"template": { "app": "nginx"
"spec": { },
"affinity": { }, "template": {
"containers": [ "metadata": {
{ "name": "nginx",
"args": [ ], "labels": {
"command": [ ], "app": "nginx"
"env": [ ], }
"envFrom": [ ], },
"image": "nginx:1.7.9", "spec": {
"lifecycle": { }, "containers": [
"livenessProbe": { }, {
"name": "nginx", "name": "nginx",
"ports": [ "image": "nginx",
{ "ports": [
"containerPort": 80, {
"name": "http" "containerPort": 80
} }
], ]
"readinessProbe": { }, }
"resources": { }, ]
"securityContext": { },
"volumeMounts": [ ]
}
],
"hostMappings": [ ],
"imagePullSecrets": [ ],
"initContainers": [ ],
"nodeSelector": { },
"securityContext": { },
"tolerations": [ ],
"volumes": [ ]
}
} }
}, }
"status": { } }
} }

600003
fixtures/valid_large.yaml Normal file

File diff suppressed because it is too large Load diff

12
go.mod
View file

@ -1,10 +1,12 @@
module github.com/yannh/kubeconform module github.com/yannh/kubeconform
go 1.14 go 1.24
require ( require (
github.com/beevik/etree v1.1.0 github.com/hashicorp/go-retryablehttp v0.7.7
github.com/xeipuuv/gojsonschema v1.2.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
gopkg.in/yaml.v2 v2.3.0 // indirect golang.org/x/text v0.25.0
sigs.k8s.io/yaml v1.2.0 sigs.k8s.io/yaml v1.4.0
) )
require github.com/hashicorp/go-cleanhttp v0.5.2 // indirect

76
go.sum
View file

@ -1,52 +1,26 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357 h1:Rem2+U35z1QtPQc6r+WolF7yXiefXqDKyk+lN2pE164= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0 h1:j30noezaCfvNLcdMYSvHLv81DxYRSt1grlpseG67vhU= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/instrumenta/kubeval v0.0.0-20200515185822-7721cbec724c h1:tF3B96upB2wECZMXZxrAMLiVUgT22sNNxhuOhrcg28s= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/instrumenta/kubeval v0.0.0-20200515185822-7721cbec724c/go.mod h1:cD+P/oZrBwOnaIHXrqvKPuN353KPxGomnsXSXf8pFJs= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
github.com/pelletier/go-toml v0.0.0-20180724185102-c2dbbc24a979/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
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=

4
pkg/cache/cache.go vendored
View file

@ -1,6 +1,6 @@
package cache package cache
type Cache interface { type Cache interface {
Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error) Get(key string) (any, error)
Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error Set(key string, schema any) error
} }

18
pkg/cache/inmemory.go vendored
View file

@ -10,26 +10,21 @@ import (
// - This cache caches the parsed Schemas // - This cache caches the parsed Schemas
type inMemory struct { type inMemory struct {
sync.RWMutex sync.RWMutex
schemas map[string]interface{} schemas map[string]any
} }
// New creates a new cache for downloaded schemas // New creates a new cache for downloaded schemas
func NewInMemoryCache() Cache { func NewInMemoryCache() Cache {
return &inMemory{ 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 // Get retrieves the JSON schema given a resource signature
func (c *inMemory) Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error) { func (c *inMemory) Get(key string) (any, error) {
k := key(resourceKind, resourceAPIVersion, k8sVersion)
c.RLock() c.RLock()
defer c.RUnlock() defer c.RUnlock()
schema, ok := c.schemas[k] schema, ok := c.schemas[key]
if !ok { if !ok {
return nil, fmt.Errorf("schema not found in in-memory cache") 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 // Set adds a JSON schema to the schema cache
func (c *inMemory) Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error { func (c *inMemory) Set(key string, schema any) error {
k := key(resourceKind, resourceAPIVersion, k8sVersion)
c.Lock() c.Lock()
defer c.Unlock() defer c.Unlock()
c.schemas[k] = schema c.schemas[key] = schema
return nil return nil
} }

24
pkg/cache/ondisk.go vendored
View file

@ -1,10 +1,9 @@
package cache package cache
import ( import (
"crypto/md5" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "io"
"io/ioutil"
"os" "os"
"path" "path"
"sync" "sync"
@ -22,27 +21,32 @@ func NewOnDiskCache(cache string) Cache {
} }
} }
func cachePath(folder, resourceKind, resourceAPIVersion, k8sVersion string) string { func cachePath(folder, key string) string {
hash := md5.Sum([]byte(fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion))) hash := sha256.Sum256([]byte(key))
return path.Join(folder, hex.EncodeToString(hash[:])) return path.Join(folder, hex.EncodeToString(hash[:]))
} }
// Get retrieves the JSON schema given a resource signature // 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() c.RLock()
defer c.RUnlock() defer c.RUnlock()
f, err := os.Open(cachePath(c.folder, resourceKind, resourceAPIVersion, k8sVersion)) f, err := os.Open(cachePath(c.folder, key))
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close()
return ioutil.ReadAll(f) return io.ReadAll(f)
} }
// Set adds a JSON schema to the schema cache // 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() c.Lock()
defer c.Unlock() 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
} }

View file

@ -4,28 +4,29 @@ import (
"bytes" "bytes"
"flag" "flag"
"fmt" "fmt"
"os" "regexp"
"strings" "strings"
) )
type Config struct { type Config struct {
Cache string Cache string `yaml:"cache" json:"cache"`
CPUProfileFile string Debug bool `yaml:"debug" json:"debug"`
ExitOnError bool ExitOnError bool `yaml:"exitOnError" json:"exitOnError"`
Files []string Files []string `yaml:"files" json:"files"`
SchemaLocations []string Help bool `yaml:"help" json:"help"`
SkipTLS bool IgnoreFilenamePatterns []string `yaml:"ignoreFilenamePatterns" json:"ignoreFilenamePatterns"`
SkipKinds map[string]struct{} IgnoreMissingSchemas bool `yaml:"ignoreMissingSchemas" json:"ignoreMissingSchemas"`
RejectKinds map[string]struct{} KubernetesVersion k8sVersionValue `yaml:"kubernetesVersion" json:"kubernetesVersion"`
OutputFormat string NumberOfWorkers int `yaml:"numberOfWorkers" json:"numberOfWorkers"`
KubernetesVersion string OutputFormat string `yaml:"output" json:"output"`
NumberOfWorkers int RejectKinds map[string]struct{} `yaml:"reject" json:"reject"`
Summary bool SchemaLocations []string `yaml:"schemaLocations" json:"schemaLocations"`
Strict bool SkipKinds map[string]struct{} `yaml:"skip" json:"skip"`
Verbose bool SkipTLS bool `yaml:"insecureSkipTLSVerify" json:"insecureSkipTLSVerify"`
IgnoreMissingSchemas bool Strict bool `yaml:"strict" json:"strict"`
IgnoreFilenamePatterns []string Summary bool `yaml:"summary" json:"summary"`
Help bool Verbose bool `yaml:"verbose" json:"verbose"`
Version bool `yaml:"version" json:"version"`
} }
type arrayParam []string type arrayParam []string
@ -39,11 +40,30 @@ func (ap *arrayParam) Set(value string) error {
return nil 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{} { func splitCSV(csvStr string) map[string]struct{} {
splitValues := strings.Split(csvStr, ",") splitValues := strings.Split(csvStr, ",")
valuesMap := map[string]struct{}{} valuesMap := map[string]struct{}{}
for _, kind := range splitValues { for _, kind := range splitValues {
kind = strings.TrimSpace(kind)
if len(kind) > 0 { if len(kind) > 0 {
valuesMap[kind] = struct{}{} valuesMap[kind] = struct{}{}
} }
@ -56,33 +76,32 @@ func splitCSV(csvStr string) map[string]struct{} {
func FromFlags(progName string, args []string) (Config, string, error) { func FromFlags(progName string, args []string) (Config, string, error) {
var schemaLocationsParam, ignoreFilenamePatterns arrayParam var schemaLocationsParam, ignoreFilenamePatterns arrayParam
var skipKindsCSV, rejectKindsCSV string var skipKindsCSV, rejectKindsCSV string
flags := flag.NewFlagSet(progName, flag.ExitOnError) flags := flag.NewFlagSet(progName, flag.ContinueOnError)
var buf bytes.Buffer var buf bytes.Buffer
flags.SetOutput(&buf) flags.SetOutput(&buf)
c := Config{} c := Config{}
c.Files = []string{} 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.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(&skipKindsCSV, "skip", "", "comma-separated list of kinds or GVKs to ignore")
flags.StringVar(&rejectKindsCSV, "reject", "", "comma-separated list of kinds to reject") 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.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.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.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.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.IntVar(&c.NumberOfWorkers, "n", 4, "number of goroutines to run concurrently")
flags.BoolVar(&c.Strict, "strict", false, "disallow additional properties not in schema") 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, tap, text") 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.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.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.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.Help, "h", false, "show help information")
flags.BoolVar(&c.Version, "v", false, "show version information")
flags.Usage = func() { flags.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [OPTION]... [FILE OR FOLDER]...\n", progName) fmt.Fprintf(&buf, "Usage: %s [OPTION]... [FILE OR FOLDER]...\n", progName)
flags.SetOutput(os.Stderr)
flags.PrintDefaults() flags.PrintDefaults()
} }

View file

@ -49,7 +49,7 @@ func TestFromFlags(t *testing.T) {
[]string{}, []string{},
Config{ Config{
Files: []string{}, Files: []string{},
KubernetesVersion: "1.18.0", KubernetesVersion: "master",
NumberOfWorkers: 4, NumberOfWorkers: 4,
OutputFormat: "text", OutputFormat: "text",
SchemaLocations: nil, SchemaLocations: nil,
@ -62,7 +62,20 @@ func TestFromFlags(t *testing.T) {
Config{ Config{
Files: []string{}, Files: []string{},
Help: true, 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, NumberOfWorkers: 4,
OutputFormat: "text", OutputFormat: "text",
SchemaLocations: nil, SchemaLocations: nil,
@ -74,7 +87,31 @@ func TestFromFlags(t *testing.T) {
[]string{"-skip", "a,b,c"}, []string{"-skip", "a,b,c"},
Config{ Config{
Files: []string{}, 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, NumberOfWorkers: 4,
OutputFormat: "text", OutputFormat: "text",
SchemaLocations: nil, SchemaLocations: nil,
@ -86,7 +123,7 @@ func TestFromFlags(t *testing.T) {
[]string{"-summary", "-verbose", "file1", "file2"}, []string{"-summary", "-verbose", "file1", "file2"},
Config{ Config{
Files: []string{"file1", "file2"}, Files: []string{"file1", "file2"},
KubernetesVersion: "1.18.0", KubernetesVersion: "master",
NumberOfWorkers: 4, NumberOfWorkers: 4,
OutputFormat: "text", OutputFormat: "text",
SchemaLocations: nil, 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", []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", "-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{ Config{
Cache: "cache", Cache: "cache",
Debug: true,
Files: []string{"file1", "file2"}, Files: []string{"file1", "file2"},
IgnoreMissingSchemas: true, IgnoreMissingSchemas: true,
KubernetesVersion: "1.16.0", KubernetesVersion: "1.16.0",

65
pkg/loader/file.go Normal file
View 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
View 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
View 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
View 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 }

View file

@ -9,12 +9,13 @@ import (
) )
type oresult struct { type oresult struct {
Filename string `json:"filename"` Filename string `json:"filename"`
Kind string `json:"kind"` Kind string `json:"kind"`
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
Status string `json:"status"` Status string `json:"status"`
Msg string `json:"msg"` Msg string `json:"msg"`
ValidationErrors []validator.ValidationError `json:"validationErrors,omitempty"`
} }
type jsono struct { type jsono struct {
@ -49,11 +50,15 @@ func (o *jsono) Write(result validator.Result) error {
o.nValid++ o.nValid++
case validator.Invalid: case validator.Invalid:
st = "statusInvalid" st = "statusInvalid"
msg = result.Err.Error() if result.Err != nil {
msg = result.Err.Error()
}
o.nInvalid++ o.nInvalid++
case validator.Error: case validator.Error:
st = "statusError" st = "statusError"
msg = result.Err.Error() if result.Err != nil {
msg = result.Err.Error()
}
o.nErrors++ o.nErrors++
case validator.Skipped: case validator.Skipped:
st = "statusSkipped" 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) { if o.verbose || (result.Status != validator.Valid && result.Status != validator.Skipped && result.Status != validator.Empty) {
sig, _ := result.Resource.Signature() 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 return nil

View file

@ -93,6 +93,60 @@ metadata:
"skipped": 0 "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
}
}
`, `,
}, },
} { } {

View file

@ -10,9 +10,10 @@ import (
"bufio" "bufio"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"github.com/yannh/kubeconform/pkg/validator"
"io" "io"
"time" "time"
"github.com/yannh/kubeconform/pkg/validator"
) )
type TestSuiteCollection struct { type TestSuiteCollection struct {
@ -32,22 +33,22 @@ type Property struct {
} }
type TestSuite struct { type TestSuite struct {
XMLName xml.Name `xml:"testsuite"` XMLName xml.Name `xml:"testsuite"`
Properties []*Property `xml:"properties>property,omitempty"` Cases []TestCase `xml:"testcase"`
Cases []TestCase `xml:"testcase"` Name string `xml:"name,attr"`
Name string `xml:"name,attr"` Id int `xml:"id,attr"`
Id int `xml:"id,attr"` Tests int `xml:"tests,attr"`
Tests int `xml:"tests,attr"` Failures int `xml:"failures,attr"`
Failures int `xml:"failures,attr"` Errors int `xml:"errors,attr"`
Errors int `xml:"errors,attr"` Disabled int `xml:"disabled,attr"`
Disabled int `xml:"disabled,attr"` Skipped int `xml:"skipped,attr"`
Skipped int `xml:"skipped,attr"`
} }
type TestCase struct { type TestCase struct {
XMLName xml.Name `xml:"testcase"` XMLName xml.Name `xml:"testcase"`
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
ClassName string `xml:"classname,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"` Skipped *TestCaseSkipped `xml:"skipped,omitempty"`
Error *TestCaseError `xml:"error,omitempty"` Error *TestCaseError `xml:"error,omitempty"`
Failure []TestCaseError `xml:"failure,omitempty"` Failure []TestCaseError `xml:"failure,omitempty"`
@ -64,13 +65,13 @@ type TestCaseError struct {
} }
type junito struct { type junito struct {
id int id int
w io.Writer w io.Writer
withSummary bool withSummary bool
verbose bool verbose bool
suites map[string]*TestSuite // map filename to corresponding suite suitesIndex map[string]int // map filename to index in suites
nValid, nInvalid, nErrors, nSkipped int suites []TestSuite
startTime time.Time startTime time.Time
} }
func junitOutput(w io.Writer, withSummary bool, isStdin, verbose bool) Output { 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, w: w,
withSummary: withSummary, withSummary: withSummary,
verbose: verbose, verbose: verbose,
suites: make(map[string]*TestSuite), suites: []TestSuite{},
nValid: 0, suitesIndex: make(map[string]int),
nInvalid: 0,
nErrors: 0,
nSkipped: 0,
startTime: time.Now(), startTime: time.Now(),
} }
} }
// Write adds a result to the report. // Write adds a result to the report.
func (o *junito) Write(result validator.Result) error { func (o *junito) Write(result validator.Result) error {
var suite *TestSuite var suite TestSuite
suite, found := o.suites[result.Resource.Path] i, found := o.suitesIndex[result.Resource.Path]
if !found { if !found {
o.id++ o.id++
suite = &TestSuite{ suite = TestSuite{
Name: result.Resource.Path, Name: result.Resource.Path,
Id: o.id, Id: o.id,
Tests: 0, Failures: 0, Errors: 0, Disabled: 0, Skipped: 0, Tests: 0, Failures: 0, Errors: 0, Disabled: 0, Skipped: 0,
Cases: make([]TestCase, 0), Cases: make([]TestCase, 0),
Properties: make([]*Property, 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() sig, _ := result.Resource.Signature()
var objectName string var objectName string
if len(sig.Namespace) > 0 { if len(sig.Namespace) > 0 {
@ -119,24 +116,22 @@ func (o *junito) Write(result validator.Result) error {
switch result.Status { switch result.Status {
case validator.Valid: case validator.Valid:
o.nValid++
case validator.Invalid: case validator.Invalid:
suite.Failures++ o.suites[i].Failures++
o.nInvalid++
failure := TestCaseError{Message: result.Err.Error()} failure := TestCaseError{Message: result.Err.Error()}
testCase.Failure = append(testCase.Failure, failure) testCase.Failure = append(testCase.Failure, failure)
case validator.Error: case validator.Error:
suite.Errors++ o.suites[i].Errors++
o.nErrors++
testCase.Error = &TestCaseError{Message: result.Err.Error()} testCase.Error = &TestCaseError{Message: result.Err.Error()}
case validator.Skipped: case validator.Skipped:
suite.Skipped++
testCase.Skipped = &TestCaseSkipped{} testCase.Skipped = &TestCaseSkipped{}
o.nSkipped++ o.suites[i].Skipped++
case validator.Empty: 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 return nil
} }
@ -145,19 +140,33 @@ func (o *junito) Write(result validator.Result) error {
func (o *junito) Flush() error { func (o *junito) Flush() error {
runtime := time.Now().Sub(o.startTime) runtime := time.Now().Sub(o.startTime)
var suites = make([]TestSuite, 0) totalValid := 0
totalInvalid := 0
totalErrors := 0
totalSkipped := 0
for _, suite := range o.suites { 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{ root := TestSuiteCollection{
Name: "kubeconform", Name: "kubeconform",
Time: runtime.Seconds(), Time: runtime.Seconds(),
Tests: o.nValid + o.nInvalid + o.nErrors + o.nSkipped, Tests: totalValid + totalInvalid + totalErrors + totalSkipped,
Failures: o.nInvalid, Failures: totalInvalid,
Errors: o.nErrors, Errors: totalErrors,
Disabled: o.nSkipped, Disabled: totalSkipped,
Suites: suites, Suites: o.suites,
} }
// 2-space indentation // 2-space indentation

View file

@ -2,73 +2,37 @@ package output
import ( import (
"bytes" "bytes"
"github.com/yannh/kubeconform/pkg/resource" "fmt"
"regexp" "regexp"
"testing" "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 { func TestJunitWrite(t *testing.T) {
matched, _ := regexp.MatchString("^\\d+(\\.\\d+)?$", s)
return matched
}
func TestJUnitWrite(t *testing.T) {
for _, testCase := range []struct { for _, testCase := range []struct {
name string name string
withSummary bool withSummary bool
isStdin bool isStdin bool
verbose bool verbose bool
results []validator.Result results []validator.Result
evaluate func(d *etree.Document) expect string
}{ }{
{ {
"empty document", "an empty result",
false, true,
false, false,
false, false,
[]validator.Result{}, []validator.Result{},
func(d *etree.Document) { "<testsuites name=\"kubeconform\" time=\"\" tests=\"0\" failures=\"0\" disabled=\"0\" errors=\"0\"></testsuites>\n",
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")
}
},
}, },
{ {
"a single deployment, verbose, with summary", "a single deployment, summary, no verbose",
true, true,
false, false,
true, false,
[]validator.Result{ []validator.Result{
{ {
Resource: resource.Resource{ Resource: resource.Resource{
@ -77,84 +41,111 @@ func TestJUnitWrite(t *testing.T) {
kind: Deployment kind: Deployment
metadata: metadata:
name: "my-app" name: "my-app"
namespace: "my-namespace"
`), `),
}, },
Status: validator.Valid, Status: validator.Valid,
Err: nil, Err: nil,
}, },
}, },
func(d *etree.Document) { "<testsuites name=\"kubeconform\" time=\"\" tests=\"1\" failures=\"0\" disabled=\"0\" errors=\"0\">\n" +
suites := d.FindElements("//testsuites/testsuite") " <testsuite name=\"deployment.yml\" id=\"1\" tests=\"1\" failures=\"0\" errors=\"0\" disabled=\"0\" skipped=\"0\">\n" +
if len(suites) != 1 { " <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\"></testcase>\n" +
t.Errorf("Expected exactly 1 testsuite element, got %d", len(suites)) " </testsuite>\n" +
return "</testsuites>\n",
} },
suite := suites[0] {
for _, attr := range suite.Attr { "a deployment, an empty resource, summary, no verbose",
switch attr.Key { true,
case "name": false,
if attr.Value != "deployment.yml" { false,
t.Errorf("Test suite name should be the resource path") []validator.Result{
} {
continue Resource: resource.Resource{
case "id": Path: "deployment.yml",
if attr.Value != "1" { Bytes: []byte(`apiVersion: apps/v1
t.Errorf("testsuite/@id should be 1") kind: Deployment
} metadata:
continue name: "my-app"
case "tests": `),
if attr.Value != "1" { },
t.Errorf("testsuite/@tests should be 1") Status: validator.Valid,
} Err: nil,
continue },
case "failures": {
if attr.Value != "0" { Resource: resource.Resource{
t.Errorf("testsuite/@failures should be 0") Path: "deployment.yml",
} Bytes: []byte(`#A single comment`),
continue },
case "errors": Status: validator.Empty,
if attr.Value != "0" { Err: nil,
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",
},
{
"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) w := new(bytes.Buffer)
@ -165,9 +156,13 @@ metadata:
} }
o.Flush() o.Flush()
doc := etree.NewDocument() // We remove the time, which will be different every time, before the comparison
doc.ReadFromString(w.String()) 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)
}
} }
} }

View file

@ -2,7 +2,7 @@ package output
import ( import (
"fmt" "fmt"
"os" "io"
"github.com/yannh/kubeconform/pkg/validator" "github.com/yannh/kubeconform/pkg/validator"
) )
@ -12,19 +12,19 @@ type Output interface {
Flush() error Flush() error
} }
func New(outputFormat string, printSummary, isStdin, verbose bool) (Output, error) { func New(w io.Writer, outputFormat string, printSummary, isStdin, verbose bool) (Output, error) {
w := os.Stdout
switch { switch {
case outputFormat == "json": case outputFormat == "json":
return jsonOutput(w, printSummary, isStdin, verbose), nil return jsonOutput(w, printSummary, isStdin, verbose), nil
case outputFormat == "junit": case outputFormat == "junit":
return junitOutput(w, printSummary, isStdin, verbose), nil return junitOutput(w, printSummary, isStdin, verbose), nil
case outputFormat == "pretty":
return prettyOutput(w, printSummary, isStdin, verbose), nil
case outputFormat == "tap": case outputFormat == "tap":
return tapOutput(w, printSummary, isStdin, verbose), nil return tapOutput(w, printSummary, isStdin, verbose), nil
case outputFormat == "text": case outputFormat == "text":
return textOutput(w, printSummary, isStdin, verbose), nil return textOutput(w, printSummary, isStdin, verbose), nil
default: 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
View 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
View 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)
}
}
}

View file

@ -40,11 +40,11 @@ func (o *tapo) Write(res validator.Result) error {
switch res.Status { switch res.Status {
case validator.Valid: case validator.Valid:
sig, _ := res.Resource.Signature() 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: case validator.Invalid:
sig, _ := res.Resource.Signature() 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: case validator.Empty:
fmt.Fprintf(o.w, "ok %d - %s (empty)\n", o.index, res.Resource.Path) 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()) fmt.Fprintf(o.w, "not ok %d - %s: %s\n", o.index, res.Resource.Path, res.Err.Error())
case validator.Skipped: 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 return nil

View file

@ -8,7 +8,7 @@ import (
"github.com/yannh/kubeconform/pkg/validator" "github.com/yannh/kubeconform/pkg/validator"
) )
func TestTextWrite(t *testing.T) { func TestTapWrite(t *testing.T) {
for _, testCase := range []struct { for _, testCase := range []struct {
name string name string
withSummary bool withSummary bool
@ -36,7 +36,7 @@ metadata:
Err: nil, 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", "a single deployment, verbose, with summary",
@ -57,7 +57,7 @@ metadata:
Err: nil, 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) w := new(bytes.Buffer)

View file

@ -8,7 +8,7 @@ import (
"github.com/yannh/kubeconform/pkg/validator" "github.com/yannh/kubeconform/pkg/validator"
) )
func TestTapWrite(t *testing.T) { func TestTextWrite(t *testing.T) {
for _, testCase := range []struct { for _, testCase := range []struct {
name string name string
withSummary bool withSummary bool

View file

@ -1,97 +1,34 @@
package registry package registry
import ( import (
"crypto/tls" "github.com/santhosh-tekuri/jsonschema/v6"
"fmt"
"io/ioutil"
"net/http"
"os"
"time"
"github.com/yannh/kubeconform/pkg/cache"
) )
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 // SchemaRegistry is a file repository (local or remote) that contains JSON schemas for Kubernetes resources
type SchemaRegistry struct { type SchemaRegistry struct {
c httpGetter
schemaPathTemplate string schemaPathTemplate string
cache cache.Cache
strict bool strict bool
debug bool
loader jsonschema.URLLoader
} }
func newHTTPRegistry(schemaPathTemplate string, cacheFolder string, strict bool, skipTLS bool) (*SchemaRegistry, error) { func newHTTPRegistry(schemaPathTemplate string, loader jsonschema.URLLoader, strict bool, debug 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)
}
return &SchemaRegistry{ return &SchemaRegistry{
c: &http.Client{Transport: reghttp},
schemaPathTemplate: schemaPathTemplate, schemaPathTemplate: schemaPathTemplate,
cache: filecache,
strict: strict, strict: strict,
loader: loader,
debug: debug,
}, nil }, nil
} }
// DownloadSchema downloads the schema for a particular resource from an HTTP server // 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) url, err := schemaPath(r.schemaPathTemplate, resourceKind, resourceAPIVersion, k8sVersion, r.strict)
if err != nil { if err != nil {
return nil, err return "", nil, err
} }
if r.cache != nil { resp, err := r.loader.Load(url)
if b, err := r.cache.Get(resourceKind, resourceAPIVersion, k8sVersion); err == nil {
return b.([]byte), nil
}
}
resp, err := r.c.Get(url) return url, resp, err
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
} }

View file

@ -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)
}
}
}

View file

@ -1,43 +1,33 @@
package registry package registry
import ( import (
"fmt" "github.com/santhosh-tekuri/jsonschema/v6"
"io/ioutil"
"os"
) )
type LocalRegistry struct { type LocalRegistry struct {
pathTemplate string pathTemplate string
strict bool strict bool
debug bool
loader jsonschema.URLLoader
} }
// NewLocalSchemas creates a new "registry", that will serve schemas from files, given a list of schema filenames // 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{ return &LocalRegistry{
pathTemplate, pathTemplate,
strict, strict,
debug,
loader,
}, nil }, nil
} }
// DownloadSchema retrieves the schema from a file for the resource // 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) schemaFile, err := schemaPath(r.pathTemplate, resourceKind, resourceAPIVersion, k8sVersion, r.strict)
if err != nil { if err != nil {
return []byte{}, nil return schemaFile, []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)
} }
defer f.Close() s, err := r.loader.Load(schemaFile)
content, err := ioutil.ReadAll(f) return schemaFile, s, err
if err != nil {
return nil, err
}
return content, nil
} }

View file

@ -3,6 +3,9 @@ package registry
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/yannh/kubeconform/pkg/cache"
"github.com/yannh/kubeconform/pkg/loader"
"os"
"strings" "strings"
"text/template" "text/template"
) )
@ -13,25 +16,9 @@ type Manifest struct {
// Registry is an interface that should be implemented by any source of Kubernetes schemas // Registry is an interface that should be implemented by any source of Kubernetes schemas
type Registry interface { 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) { func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict bool) (string, error) {
normalisedVersion := k8sVersion normalisedVersion := k8sVersion
if normalisedVersion != "master" { if normalisedVersion != "master" {
@ -61,12 +48,14 @@ func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict
StrictSuffix string StrictSuffix string
ResourceKind string ResourceKind string
ResourceAPIVersion string ResourceAPIVersion string
Group string
KindSuffix string KindSuffix string
}{ }{
normalisedVersion, normalisedVersion,
strictSuffix, strictSuffix,
strings.ToLower(resourceKind), strings.ToLower(resourceKind),
groupParts[len(groupParts)-1], groupParts[len(groupParts)-1],
groupParts[0],
kindSuffix, kindSuffix,
} }
@ -79,7 +68,7 @@ func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict
return buf.String(), nil 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" { if schemaLocation == "default" {
schemaLocation = "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json" 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 } 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 // 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) return nil, fmt.Errorf("failed initialising schema location registry: %s", err)
} }
if strings.HasPrefix(schemaLocation, "http") { var c cache.Cache = nil
return newHTTPRegistry(schemaLocation, cache, strict, skipTLS) 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)
} }

View file

@ -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) { func findResourcesInReader(p string, f io.Reader, resources chan<- Resource, errors chan<- error, buf []byte) {
maxBufSize := 256 * 1024 * 1024
scanner := bufio.NewScanner(f) 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) scanner.Split(SplitYAMLDocument)
nRes := 0 nRes := 0
for scanner.Scan() { for scanner.Scan() {
if len(scanner.Text()) > 0 { if len(scanner.Text()) > 0 {
resources <- Resource{Path: p, Bytes: []byte(scanner.Text())} res := Resource{Path: p, Bytes: []byte(scanner.Text())}
nRes++ for _, subres := range res.Resources() {
resources <- subres
nRes++
}
} }
} }
if err := scanner.Err(); err != nil { 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) files, errors := findFilesInFolders(ctx, paths, ignoreFilePatterns)
go func() { go func() {
maxResourceSize := 4 * 1024 * 1024 // 4MB ought to be enough for everybody initialBufSize := 4 * 1024 * 1024 // This is the initial size - scanner will resize if needed
buf := make([]byte, maxResourceSize) // We reuse this to avoid multiple large memory allocations buf := make([]byte, initialBufSize) // We reuse the same buffer to avoid multiple large memory allocations
for p := range files { for p := range files {
findResourcesInFile(p, resources, errors, buf) findResourcesInFile(p, resources, errors, buf)

View file

@ -2,15 +2,17 @@ package resource
import ( import (
"fmt" "fmt"
"strings"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
// Resource represents a Kubernetes resource within a file // Resource represents a Kubernetes resource within a file
type Resource struct { type Resource struct {
Path string Path string
Bytes []byte Bytes []byte
sig *Signature sig *Signature // Cache signature parsing
sigErr error // Cache potential signature parsing error
} }
// Signature is a key representing a Kubernetes resource // Signature is a key representing a Kubernetes resource
@ -18,10 +20,22 @@ type Signature struct {
Kind, Version, Namespace, Name string 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 // Signature computes a signature for a resource, based on its Kind, Version, Namespace & Name
func (res *Resource) Signature() (*Signature, error) { func (res *Resource) Signature() (*Signature, error) {
if res.sig != nil { if res.sig != nil {
return res.sig, nil return res.sig, res.sigErr
} }
resource := struct { 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} 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 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 == "" { if res.sig.Kind == "" {
return res.sig, fmt.Errorf("missing 'kind' key") res.sigErr = fmt.Errorf("missing 'kind' key")
return res.sig, res.sigErr
} }
if resource.APIVersion == "" { if res.sig.Version == "" {
return res.sig, fmt.Errorf("missing 'apiVersion' key") 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) { func (res *Resource) SignatureFromMap(m map[string]interface{}) (*Signature, error) {
if res.sig != nil { if res.sig != nil {
return res.sig, nil return res.sig, res.sigErr
} }
Kind, ok := m["kind"].(string) Kind, ok := m["kind"].(string)
if !ok { 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) APIVersion, ok := m["apiVersion"].(string)
if !ok { 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 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} res.sig = &Signature{Kind: Kind, Version: APIVersion, Namespace: ns, Name: name}
return res.sig, nil 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}
}

View file

@ -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))
}
}
}

View file

@ -47,10 +47,12 @@ func FromStream(ctx context.Context, path string, r io.Reader) (<-chan Resource,
errors := make(chan error) errors := make(chan error)
go func() { go func() {
const initialBufSize = 4 * 1024 * 1024 // Start with 4MB
const maxBufSize = 256 * 1024 * 1024 // Start with 4MB
scanner := bufio.NewScanner(r) scanner := bufio.NewScanner(r)
const maxResourceSize = 4 * 1024 * 1024 // 4MB ought to be enough for everybody buf := make([]byte, initialBufSize)
buf := make([]byte, maxResourceSize) scanner.Buffer(buf, maxBufSize) // Resize up to 256MB
scanner.Buffer(buf, maxResourceSize)
scanner.Split(SplitYAMLDocument) scanner.Split(SplitYAMLDocument)
SCAN: SCAN:
@ -60,7 +62,10 @@ func FromStream(ctx context.Context, path string, r io.Reader) (<-chan Resource,
break SCAN break SCAN
default: default:
} }
resources <- Resource{Path: path, Bytes: scanner.Bytes()} res := Resource{Path: path, Bytes: scanner.Bytes()}
for _, subres := range res.Resources() {
resources <- subres
}
} }
close(resources) close(resources)

View file

@ -3,15 +3,20 @@ package validator
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" jsonschema "github.com/santhosh-tekuri/jsonschema/v6"
"github.com/yannh/kubeconform/pkg/cache" "github.com/yannh/kubeconform/pkg/cache"
"github.com/yannh/kubeconform/pkg/loader"
"github.com/yannh/kubeconform/pkg/registry" "github.com/yannh/kubeconform/pkg/registry"
"github.com/yannh/kubeconform/pkg/resource" "github.com/yannh/kubeconform/pkg/resource"
"golang.org/x/text/language"
"github.com/xeipuuv/gojsonschema" "golang.org/x/text/message"
"io"
"os"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"strings"
"time"
) )
// Different types of validation results // Different types of validation results
@ -26,11 +31,21 @@ const (
Empty // resource is empty. Note: is triggered for files starting with a --- separator. 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 // Result contains the details of the result of a resource validation
type Result struct { type Result struct {
Resource resource.Resource Resource resource.Resource
Err error Err error
Status Status Status Status
ValidationErrors []ValidationError
} }
// Validator exposes multiple methods to validate your Kubernetes resources. // 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. // Opts contains a set of options for the validator.
type Opts struct { type Opts struct {
Cache string // Cache schemas downloaded via HTTP to this folder 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 SkipTLS bool // skip TLS validation when downloading from an HTTP Schema Registry
SkipKinds map[string]struct{} // List of resource Kinds to ignore SkipKinds map[string]struct{} // List of resource Kinds to ignore
RejectKinds map[string]struct{} // List of resource Kinds to reject 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 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 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{} registries := []registry.Registry{}
for _, schemaLocation := range schemaLocations { 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 { if err != nil {
return nil, err return nil, err
} }
@ -69,7 +85,7 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
} }
if opts.KubernetesVersion == "" { if opts.KubernetesVersion == "" {
opts.KubernetesVersion = "1.18.0" opts.KubernetesVersion = "master"
} }
if opts.SkipKinds == nil { if opts.SkipKinds == nil {
@ -79,30 +95,70 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
opts.RejectKinds = map[string]struct{}{} 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{ return &v{
opts: opts, opts: opts,
schemaDownload: downloadSchema, schemaDownload: downloadSchema,
schemaCache: cache.NewInMemoryCache(), schemaMemoryCache: cache.NewInMemoryCache(),
regs: registries, regs: registries,
loader: jsonschema.SchemeURLLoader{
"file": jsonschema.FileLoader{},
"http": httpLoader,
"https": httpLoader,
},
}, nil }, nil
} }
type v struct { type v struct {
opts Opts opts Opts
schemaCache cache.Cache schemaDiskCache cache.Cache
schemaDownload func(registries []registry.Registry, kind, version, k8sVersion string) (*gojsonschema.Schema, error) schemaMemoryCache cache.Cache
regs []registry.Registry 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 // ValidateResource validates a single resource. This allows to validate
// large resource streams using multiple Go Routines. // large resource streams using multiple Go Routines.
func (val *v) ValidateResource(res resource.Resource) Result { 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 { skip := func(signature resource.Signature) bool {
if _, ok := val.opts.SkipKinds[signature.GroupVersionKind()]; ok {
return ok
}
_, ok := val.opts.SkipKinds[signature.Kind] _, ok := val.opts.SkipKinds[signature.Kind]
return ok return ok
} }
reject := func(signature resource.Signature) bool { reject := func(signature resource.Signature) bool {
if _, ok := val.opts.RejectKinds[signature.GroupVersionKind()]; ok {
return ok
}
_, ok := val.opts.RejectKinds[signature.Kind] _, ok := val.opts.RejectKinds[signature.Kind]
return ok return ok
} }
@ -112,7 +168,12 @@ func (val *v) ValidateResource(res resource.Resource) Result {
} }
var r map[string]interface{} 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)} 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 cached := false
var schema *gojsonschema.Schema var schema *jsonschema.Schema
if val.schemaCache != nil { if val.schemaMemoryCache != nil {
s, err := val.schemaCache.Get(sig.Kind, sig.Version, val.opts.KubernetesVersion) s, err := val.schemaMemoryCache.Get(key(sig.Kind, sig.Version, val.opts.KubernetesVersion))
if err == nil { if err == nil {
cached = true cached = true
schema = s.(*gojsonschema.Schema) schema = s.(*jsonschema.Schema)
} }
} }
if !cached { 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} return Result{Resource: res, Err: err, Status: Error}
} }
if val.schemaCache != nil { if val.schemaMemoryCache != nil {
val.schemaCache.Set(sig.Kind, sig.Version, val.opts.KubernetesVersion, schema) 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} return Result{Resource: res, Err: fmt.Errorf("could not find schema for %s", sig.Kind), Status: Error}
} }
resourceLoader := gojsonschema.NewGoLoader(r) err = schema.Validate(r)
results, err := schema.Validate(resourceLoader)
if err != nil { if err != nil {
// This error can only happen if the Object to validate is poorly formed. There's no hope of saving this one validationErrors := []ValidationError{}
return Result{Resource: res, Status: Error, Err: fmt.Errorf("problem validating schema. Check JSON formatting: %s", err)} 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 // ValidateWithContext validates resources found in r
@ -194,8 +260,9 @@ func (val *v) ValidateWithContext(ctx context.Context, filename string, r io.Rea
for { for {
select { select {
case res, ok := <-resourcesChan: case res, ok := <-resourcesChan:
validationResults = append(validationResults, val.ValidateResource(res)) if ok {
if !ok { validationResults = append(validationResults, val.ValidateResource(res))
} else {
resourcesChan = nil resourcesChan = nil
} }
@ -218,18 +285,112 @@ func (val *v) Validate(filename string, r io.ReadCloser) []Result {
return val.ValidateWithContext(context.Background(), filename, r) 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 err error
var schemaBytes []byte var path string
var s any
for _, reg := range registries { for _, reg := range registries {
schemaBytes, err = reg.DownloadSchema(kind, version, k8sVersion) path, s, err = reg.DownloadSchema(kind, version, k8sVersion)
if err == nil { 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.(*loader.NotFoundError); notfound {
if _, notfound := err.(*registry.NotFoundError); notfound { continue
}
if _, nonJSONError := err.(*loader.NonJSONResponseError); nonJSONError {
continue 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 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{})
// }

View file

@ -1,19 +1,41 @@
package validator package validator
import ( import (
"bytes"
"github.com/santhosh-tekuri/jsonschema/v6"
"github.com/yannh/kubeconform/pkg/loader"
"io"
"reflect"
"testing" "testing"
"github.com/yannh/kubeconform/pkg/registry" "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) { func TestValidate(t *testing.T) {
for i, testCase := range []struct { for i, testCase := range []struct {
name string name string
rawResource, schema []byte rawResource, schemaRegistry1 []byte
expect Status schemaRegistry2 []byte
ignoreMissingSchema bool
strict bool
expectStatus Status
expectErrors []ValidationError
}{ }{
{ {
"valid resource", "valid resource",
@ -44,7 +66,11 @@ lastName: bar
}, },
"required": ["firstName", "lastName"] "required": ["firstName", "lastName"]
}`), }`),
nil,
false,
false,
Valid, Valid,
[]ValidationError{},
}, },
{ {
"invalid resource", "invalid resource",
@ -75,7 +101,16 @@ lastName: bar
}, },
"required": ["firstName", "lastName"] "required": ["firstName", "lastName"]
}`), }`),
nil,
false,
false,
Invalid, Invalid,
[]ValidationError{
{
Path: "/firstName",
Msg: "got string, want number",
},
},
}, },
{ {
"missing required field", "missing required field",
@ -105,7 +140,70 @@ firstName: foo
}, },
"required": ["firstName", "lastName"] "required": ["firstName", "lastName"]
}`), }`),
nil,
false,
false,
Invalid, 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", "resource has invalid yaml",
@ -139,26 +237,397 @@ lastName: bar
}, },
"required": ["firstName", "lastName"] "required": ["firstName", "lastName"]
}`), }`),
nil,
false,
false,
Error, 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{ val := v{
opts: Opts{ opts: Opts{
SkipKinds: map[string]struct{}{}, SkipKinds: map[string]struct{}{},
RejectKinds: map[string]struct{}{}, RejectKinds: map[string]struct{}{},
IgnoreMissingSchemas: testCase.ignoreMissingSchema,
Strict: testCase.strict,
}, },
schemaCache: nil, schemaDownload: downloadSchema,
schemaDownload: func(_ []registry.Registry, _, _, _ string) (*gojsonschema.Schema, error) { regs: []registry.Registry{
schema, err := gojsonschema.NewSchema(gojsonschema.NewBytesLoader(testCase.schema)) newMockRegistry(func() (string, any, error) {
if err != nil { if testCase.schemaRegistry1 == nil {
t.Errorf("failed parsing test schema") return "", nil, loader.NewNotFoundError(nil)
} }
return schema, 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 { got := val.ValidateResource(resource.Resource{Bytes: testCase.rawResource})
t.Errorf("%d - expected %d, got %d: %s", i, testCase.expect, got.Status, got.Err.Error()) 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# Derived from https://github.com/instrumenta/openapi2jsonschema # Derived from https://github.com/instrumenta/openapi2jsonschema
import yaml import yaml
@ -6,37 +6,44 @@ import json
import sys import sys
import os import os
import urllib.request 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): def test_additional_properties():
if hasattr(dict, "iteritems"): for test in iter([{
return d.iteritems() "input": {"something": {"properties": {}}},
else: "expect": {'something': {'properties': {}, "additionalProperties": False}}
return iter(d.items()) },{
"input": {"something": {"somethingelse": {}}},
"expect": {'something': {'somethingelse': {}}}
}]):
assert additional_properties(test["input"]) == test["expect"]
def additional_properties(data, skip=False):
def additional_properties(data):
"This recreates the behaviour of kubectl at https://github.com/kubernetes/kubernetes/blob/225b9119d6a8f03fcbe3cc3d590c261965d928d0/pkg/kubectl/validation/schema.go#L312" "This recreates the behaviour of kubectl at https://github.com/kubernetes/kubernetes/blob/225b9119d6a8f03fcbe3cc3d590c261965d928d0/pkg/kubectl/validation/schema.go#L312"
new = {} if isinstance(data, dict):
try: if "properties" in data and not skip:
for k, v in iteritems(data): if "additionalProperties" not in data:
new_v = v data["additionalProperties"] = False
if isinstance(v, dict): for _, v in data.items():
if "properties" in v: additional_properties(v)
if "additionalProperties" not in v: return data
v["additionalProperties"] = False
new_v = additional_properties(v)
else:
new_v = v
new[k] = new_v
return new
except AttributeError:
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): def replace_int_or_string(data):
new = {} new = {}
try: try:
for k, v in iteritems(data): for k, v in iter(data.items()):
new_v = v new_v = v
if isinstance(v, dict): if isinstance(v, dict):
if "format" in v and v["format"] == "int-or-string": if "format" in v and v["format"] == "int-or-string":
@ -54,11 +61,10 @@ def replace_int_or_string(data):
except AttributeError: except AttributeError:
return data return data
def allow_null_optional_fields(data, parent=None, grand_parent=None, key=None): def allow_null_optional_fields(data, parent=None, grand_parent=None, key=None):
new = {} new = {}
try: try:
for k, v in iteritems(data): for k, v in iter(data.items()):
new_v = v new_v = v
if isinstance(v, dict): if isinstance(v, dict):
new_v = allow_null_optional_fields(v, data, parent, k) 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): elif isinstance(v, str):
is_non_null_type = k == "type" and v != "null" is_non_null_type = k == "type" and v != "null"
has_required_fields = grand_parent and "required" in grand_parent 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_v = [v, "null"]
new[k] = new_v new[k] = new_v
return new return new
@ -91,55 +97,89 @@ def append_no_duplicates(obj, key, value):
def write_schema_file(schema, filename): def write_schema_file(schema, filename):
schemaJSON = "" schemaJSON = ""
schema = additional_properties(schema) schema = additional_properties(schema, skip=not os.getenv("DENY_ROOT_ADDITIONAL_PROPERTIES"))
schema = replace_int_or_string(schema) schema = replace_int_or_string(schema)
schemaJSON = json.dumps(schema, indent=2) schemaJSON = json.dumps(schema, indent=2)
# Dealing with user input here.. # Dealing with user input here..
filename = os.path.basename(filename) filename = os.path.basename(filename)
f = open(filename, "w") f = open(filename, "w")
f.write(schemaJSON) print(schemaJSON, file=f)
f.close() f.close()
print("JSON schema written to {filename}".format(filename=filename)) print("JSON schema written to {filename}".format(filename=filename))
if len(sys.argv) == 0: def construct_value(load, node):
print("missing file") # Handle nodes that start with '='
exit(1) # 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}") if __name__ == "__main__":
filename = "" if len(sys.argv) < 2:
if "spec" in y and "validation" in y["spec"] and "openAPIV3Schema" in y["spec"]["validation"]: print('Missing FILE parameter.\nUsage: %s [FILE]' % sys.argv[0])
filename = filename_format.format( exit(1)
kind=y["spec"]["names"]["kind"],
group=y["spec"]["group"].split(".")[0],
version=y["spec"]["version"],
).lower() + ".json"
schema = y["spec"]["validation"]["openAPIV3Schema"] for crdFile in sys.argv[1:]:
write_schema_file(schema, filename) if crdFile.startswith("http"):
elif "spec" in y and "versions" in y["spec"]: f = urllib.request.urlopen(crdFile)
for version in y["spec"]["versions"]: else:
if "schema" in version and "openAPIV3Schema" in version["schema"]: f = open(crdFile)
filename = filename_format.format( with f:
kind=y["spec"]["names"]["kind"], defs = []
group=y["spec"]["group"].split(".")[0], yaml.SafeLoader.add_constructor(u'tag:yaml.org,2002:value', construct_value)
version=version["name"], for y in yaml.load_all(f, Loader=yaml.SafeLoader):
).lower() + ".json" 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"] for y in defs:
write_schema_file(schema, filename) 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
View file

@ -0,0 +1,2 @@
pyyaml
pytest

View file

@ -0,0 +1,6 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

6
site/config.toml Normal file
View 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
View 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**.

View 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

View 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).

View 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.

View 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.

View 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.

View 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 >}}

View 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)

View 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>

View 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
View 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
View 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;
}

View 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>&lt; 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 &ldquo;default&rdquo;, 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 &lsquo;.json&rsquo;, 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 &lsquo;.json&rsquo; - Kubeconform assumes the value is a Go templated
string that indicates how to search for JSON schemas.</li>
<li>the -schema-location value of &ldquo;default&rdquo; 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> - &ldquo;-strict&rdquo; 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 - &ldquo;v1&rdquo; in &ldquo;apiVersion: monitoring.coreos.com/v1&rdquo;</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>&lt; 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>

View 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;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 - &amp;&amp; \ 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 &amp;ldquo;default&amp;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 &amp;lsquo;.json&amp;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&amp;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>

View 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>&lt; 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>&lt; 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>

View 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>&lt; 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>&lt; 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>

View 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>&lt; 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&rsquo;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>&lt; 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>

View 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>&lt; 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>&lt; 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>

View 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>&lt; 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>&lt; 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
View 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
View 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;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 - &amp;&amp; \ 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 &amp;ldquo;default&amp;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 &amp;lsquo;.json&amp;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&amp;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;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>

View 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&#39;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&#39;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

File diff suppressed because one or more lines are too long

1
site/public/sitemap.xml Normal file
View 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>

View 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;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>

View 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&#39;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>

View 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>

View 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 - &amp;&amp; \ 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>

View 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;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 - &amp;&amp; \ 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 &amp;ldquo;default&amp;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 &amp;lsquo;.json&amp;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&amp;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;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>

View 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&#39;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>

View 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 &amp;ldquo;default&amp;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 &amp;lsquo;.json&amp;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&amp;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>

View 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.

View file

@ -0,0 +1,3 @@
+++
+++

View file

Some files were not shown because too many files have changed in this diff Show more