mirror of
https://github.com/yannh/kubeconform.git
synced 2026-02-19 01:47:02 +00:00
Merge branch 'yannh:master' into master
This commit is contained in:
commit
fff7023b7f
24 changed files with 948 additions and 339 deletions
|
|
@ -2,6 +2,13 @@ FROM alpine:3.14 as certs
|
||||||
RUN apk add ca-certificates
|
RUN apk add ca-certificates
|
||||||
|
|
||||||
FROM scratch AS kubeconform
|
FROM scratch AS kubeconform
|
||||||
|
LABEL org.opencontainers.image.authors="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/"
|
||||||
MAINTAINER Yann HAMON <yann@mandragor.org>
|
MAINTAINER Yann HAMON <yann@mandragor.org>
|
||||||
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 kubeconform /
|
COPY kubeconform /
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
FROM alpine:3.14 as certs
|
FROM alpine:3.14 as certs
|
||||||
|
LABEL org.opencontainers.image.authors="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/"
|
||||||
MAINTAINER Yann HAMON <yann@mandragor.org>
|
MAINTAINER Yann HAMON <yann@mandragor.org>
|
||||||
RUN apk add ca-certificates
|
RUN apk add ca-certificates
|
||||||
COPY kubeconform /
|
COPY kubeconform /
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
FROM bats/bats:v1.2.1
|
FROM bats/bats:v1.2.1
|
||||||
RUN apk --no-cache add ca-certificates parallel
|
RUN apk --no-cache add ca-certificates parallel libxml2-utils
|
||||||
COPY dist/kubeconform_linux_amd64/kubeconform /code/bin/
|
COPY dist/kubeconform_linux_amd64_v1/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
|
||||||
|
|
|
||||||
9
Makefile
9
Makefile
|
|
@ -31,12 +31,15 @@ docker-acceptance: build-bats
|
||||||
docker run --network none -t bats -p acceptance-nonetwork.bats
|
docker run --network none -t bats -p acceptance-nonetwork.bats
|
||||||
|
|
||||||
goreleaser-build-static:
|
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:v0.176.0 build --single-target --skip-post-hooks --rm-dist --snapshot
|
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:v1.11.5 build --single-target --skip-post-hooks --rm-dist --snapshot
|
||||||
cp dist/kubeconform_linux_amd64/kubeconform bin/
|
cp dist/kubeconform_linux_amd64_v1/kubeconform bin/
|
||||||
|
|
||||||
release:
|
release:
|
||||||
docker run -e GITHUB_TOKEN -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:v0.176.0 release --rm-dist
|
docker run -e GITHUB_TOKEN -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:v1.11.5 release --rm-dist
|
||||||
|
|
||||||
update-deps:
|
update-deps:
|
||||||
go get -u ./...
|
go get -u ./...
|
||||||
go mod tidy
|
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
|
||||||
|
|
|
||||||
54
Readme.md
54
Readme.md
|
|
@ -16,7 +16,7 @@ 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**.
|
||||||
|
|
||||||
### A small overview of Kubernetes manifest validation
|
### A small overview of Kubernetes manifest validation
|
||||||
|
|
@ -57,6 +57,16 @@ $ brew install kubeconform
|
||||||
|
|
||||||
You can also download the latest version from the [release page](https://github.com/yannh/kubeconform/releases).
|
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
|
### Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -64,8 +74,8 @@ $ ./bin/kubeconform -h
|
||||||
Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
Usage: ./bin/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
|
||||||
|
|
@ -82,16 +92,16 @@ Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
||||||
-output string
|
-output string
|
||||||
output format - json, junit, tap, text (default "text")
|
output format - json, junit, 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
|
-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)
|
||||||
```
|
```
|
||||||
|
|
@ -135,6 +145,17 @@ 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
|
||||||
|
./bin/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
|
||||||
|
$ ./bin/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
|
$ ./bin/kubeconform -summary -n 16 fixtures
|
||||||
|
|
@ -184,6 +205,7 @@ Here are the variables you can use in -schema-location:
|
||||||
* *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"
|
||||||
|
* *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
|
* *KindSuffix* - suffix computed from apiVersion - for compatibility with Kubeval schema registries
|
||||||
|
|
||||||
### Converting an OpenAPI file to a JSON Schema
|
### Converting an OpenAPI file to a JSON Schema
|
||||||
|
|
@ -209,7 +231,7 @@ Some CRD schemas do not have explicit validation for fields implicitly validated
|
||||||
|
|
||||||
### Usage as a Github Action
|
### Usage as a Github Action
|
||||||
|
|
||||||
Kubeconform is publishes Docker Images to Github's new Container Registry, ghcr.io. These images
|
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/).
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
@ -235,6 +257,22 @@ 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.
|
||||||
|
|
||||||
|
### Usage in Gitlab-CI
|
||||||
|
|
||||||
|
The Kubeconform Docker image can be used in Gitlab-CI. Here is an example of a Gitlab-CI job:
|
||||||
|
|
||||||
|
```
|
||||||
|
lint-kubeconform:
|
||||||
|
stage: validate
|
||||||
|
image:
|
||||||
|
name: ghcr.io/yannh/kubeconform:latest-alpine
|
||||||
|
entrypoint: [""]
|
||||||
|
script:
|
||||||
|
- kubeconform
|
||||||
|
```
|
||||||
|
|
||||||
|
See [issue 106](https://github.com/yannh/kubeconform/issues/106) for more details.
|
||||||
|
|
||||||
### Proxy support
|
### Proxy support
|
||||||
|
|
||||||
Kubeconform will respect the HTTPS_PROXY variable when downloading schema files.
|
Kubeconform will respect the HTTPS_PROXY variable when downloading schema files.
|
||||||
|
|
|
||||||
|
|
@ -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 ]
|
||||||
|
|
@ -132,6 +138,16 @@ resetCacheFolder() {
|
||||||
[ "$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.16.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.16.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 ]
|
||||||
|
|
@ -202,6 +218,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 ]
|
||||||
|
|
@ -300,3 +328,10 @@ resetCacheFolder() {
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[ "$output" = 'Summary: 100000 resources found parsing stdin - Valid: 100000, Invalid: 0, Errors: 0, Skipped: 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 ]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,9 @@ func realMain() int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.CPUProfileFile != "" {
|
cpuProfileFile := os.Getenv("KUBECONFORM_CPUPROFILE_FILE")
|
||||||
f, err := os.Create(cfg.CPUProfileFile)
|
if cpuProfileFile != "" {
|
||||||
|
f, err := os.Create(cpuProfileFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("could not create CPU profile: ", err)
|
log.Fatal("could not create CPU profile: ", err)
|
||||||
}
|
}
|
||||||
|
|
@ -97,9 +98,10 @@ func realMain() int {
|
||||||
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,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
18
fixtures/duplicate_property.yaml
Normal file
18
fixtures/duplicate_property.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: DaemonSet
|
||||||
|
metadata:
|
||||||
|
name: nginx-ds
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
k8s-app: nginx-ds
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: envoy
|
||||||
|
name: envoy
|
||||||
|
containers:
|
||||||
|
- image: nginx
|
||||||
|
name: nginx
|
||||||
118
fixtures/junit.xsd
Normal file
118
fixtures/junit.xsd
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!--
|
||||||
|
Source: https://svn.jenkins-ci.org/trunk/hudson/dtkit/dtkit-format/dtkit-junit-model/src/main/resources/com/thalesgroup/dtkit/junit/model/xsd/junit-4.xsd
|
||||||
|
|
||||||
|
This file available under the terms of the MIT License as follows:
|
||||||
|
|
||||||
|
*******************************************************************************
|
||||||
|
* Copyright (c) 2010 Thales Corporate Services SAS *
|
||||||
|
* *
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy *
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal*
|
||||||
|
* in the Software without restriction, including without limitation the rights *
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell *
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is *
|
||||||
|
* furnished to do so, subject to the following conditions: *
|
||||||
|
* *
|
||||||
|
* The above copyright notice and this permission notice shall be included in *
|
||||||
|
* all copies or substantial portions of the Software. *
|
||||||
|
* *
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,*
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN *
|
||||||
|
* THE SOFTWARE. *
|
||||||
|
********************************************************************************
|
||||||
|
-->
|
||||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||||
|
|
||||||
|
<xs:element name="failure">
|
||||||
|
<xs:complexType mixed="true">
|
||||||
|
<xs:attribute name="type" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="message" type="xs:string" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
|
||||||
|
<xs:element name="error">
|
||||||
|
<xs:complexType mixed="true">
|
||||||
|
<xs:attribute name="type" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="message" type="xs:string" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
|
||||||
|
<xs:element name="properties">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element ref="property" maxOccurs="unbounded"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
|
||||||
|
<xs:element name="property">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||||
|
<xs:attribute name="value" type="xs:string" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
|
||||||
|
<xs:element name="skipped" type="xs:string"/>
|
||||||
|
<xs:element name="system-err" type="xs:string"/>
|
||||||
|
<xs:element name="system-out" type="xs:string"/>
|
||||||
|
|
||||||
|
<xs:element name="testcase">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
<xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
<xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
<xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||||
|
<xs:attribute name="assertions" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="time" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="classname" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="status" type="xs:string" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
|
||||||
|
<xs:element name="testsuite">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element ref="properties" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element ref="testcase" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
<xs:element ref="system-out" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element ref="system-err" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||||
|
<xs:attribute name="tests" type="xs:string" use="required"/>
|
||||||
|
<xs:attribute name="failures" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="errors" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="time" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="disabled" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="skipped" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="timestamp" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="hostname" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="id" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="package" type="xs:string" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
|
||||||
|
<xs:element name="testsuites">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element ref="testsuite" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="name" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="time" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="tests" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="failures" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="disabled" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="errors" type="xs:string" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
|
||||||
|
|
||||||
|
</xs:schema>
|
||||||
4
pkg/cache/ondisk.go
vendored
4
pkg/cache/ondisk.go
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
@ -23,7 +23,7 @@ func NewOnDiskCache(cache string) Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
func cachePath(folder, resourceKind, resourceAPIVersion, k8sVersion string) string {
|
func cachePath(folder, resourceKind, resourceAPIVersion, k8sVersion string) string {
|
||||||
hash := md5.Sum([]byte(fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)))
|
hash := sha256.Sum256([]byte(fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)))
|
||||||
return path.Join(folder, hex.EncodeToString(hash[:]))
|
return path.Join(folder, hex.EncodeToString(hash[:]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Cache string
|
Cache string
|
||||||
CPUProfileFile string
|
Debug bool
|
||||||
ExitOnError bool
|
ExitOnError bool
|
||||||
Files []string
|
Files []string
|
||||||
SchemaLocations []string
|
SchemaLocations []string
|
||||||
|
|
@ -56,7 +56,7 @@ 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)
|
||||||
|
|
||||||
|
|
@ -65,19 +65,19 @@ func FromFlags(progName string, args []string) (Config, string, error) {
|
||||||
|
|
||||||
flags.StringVar(&c.KubernetesVersion, "kubernetes-version", "master", "version of Kubernetes to validate against, e.g.: 1.18.0")
|
flags.StringVar(&c.KubernetesVersion, "kubernetes-version", "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, 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.BoolVar(&c.Version, "v", false, "show version information")
|
||||||
flags.Usage = func() {
|
flags.Usage = func() {
|
||||||
|
|
|
||||||
|
|
@ -112,9 +112,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",
|
||||||
|
|
|
||||||
|
|
@ -33,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"`
|
||||||
|
|
@ -100,8 +100,7 @@ func (o *junito) Write(result validator.Result) error {
|
||||||
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[result.Resource.Path] = suite
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,7 @@ metadata:
|
||||||
},
|
},
|
||||||
"<testsuites name=\"kubeconform\" time=\"\" tests=\"1\" failures=\"0\" disabled=\"0\" errors=\"0\">\n" +
|
"<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" +
|
" <testsuite name=\"deployment.yml\" id=\"1\" tests=\"1\" failures=\"0\" errors=\"0\" disabled=\"0\" skipped=\"0\">\n" +
|
||||||
" <properties></properties>\n" +
|
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\"></testcase>\n" +
|
||||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\"></testcase>\n" +
|
|
||||||
" </testsuite>\n" +
|
" </testsuite>\n" +
|
||||||
"</testsuites>\n",
|
"</testsuites>\n",
|
||||||
},
|
},
|
||||||
|
|
@ -82,8 +81,7 @@ metadata:
|
||||||
},
|
},
|
||||||
"<testsuites name=\"kubeconform\" time=\"\" tests=\"1\" failures=\"0\" disabled=\"0\" errors=\"0\">\n" +
|
"<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" +
|
" <testsuite name=\"deployment.yml\" id=\"1\" tests=\"1\" failures=\"0\" errors=\"0\" disabled=\"0\" skipped=\"0\">\n" +
|
||||||
" <properties></properties>\n" +
|
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\"></testcase>\n" +
|
||||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\"></testcase>\n" +
|
|
||||||
" </testsuite>\n" +
|
" </testsuite>\n" +
|
||||||
"</testsuites>\n",
|
"</testsuites>\n",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -21,9 +23,10 @@ type SchemaRegistry struct {
|
||||||
schemaPathTemplate string
|
schemaPathTemplate string
|
||||||
cache cache.Cache
|
cache cache.Cache
|
||||||
strict bool
|
strict bool
|
||||||
|
debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHTTPRegistry(schemaPathTemplate string, cacheFolder string, strict bool, skipTLS bool) (*SchemaRegistry, error) {
|
func newHTTPRegistry(schemaPathTemplate string, cacheFolder string, strict bool, skipTLS bool, debug bool) (*SchemaRegistry, error) {
|
||||||
reghttp := &http.Transport{
|
reghttp := &http.Transport{
|
||||||
MaxIdleConns: 100,
|
MaxIdleConns: 100,
|
||||||
IdleConnTimeout: 3 * time.Second,
|
IdleConnTimeout: 3 * time.Second,
|
||||||
|
|
@ -53,6 +56,7 @@ func newHTTPRegistry(schemaPathTemplate string, cacheFolder string, strict bool,
|
||||||
schemaPathTemplate: schemaPathTemplate,
|
schemaPathTemplate: schemaPathTemplate,
|
||||||
cache: filecache,
|
cache: filecache,
|
||||||
strict: strict,
|
strict: strict,
|
||||||
|
debug: debug,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,21 +75,41 @@ func (r SchemaRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVers
|
||||||
|
|
||||||
resp, err := r.c.Get(url)
|
resp, err := r.c.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
|
msg := fmt.Sprintf("failed downloading schema at %s: %s", url, err)
|
||||||
|
if r.debug {
|
||||||
|
log.Println(msg)
|
||||||
|
}
|
||||||
|
return nil, errors.New(msg)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return nil, newNotFoundError(fmt.Errorf("no schema found"))
|
msg := fmt.Sprintf("could not find schema at %s", url)
|
||||||
|
if r.debug {
|
||||||
|
log.Print(msg)
|
||||||
|
}
|
||||||
|
return nil, newNotFoundError(errors.New(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("error while downloading schema at %s - received HTTP status %d", url, resp.StatusCode)
|
msg := fmt.Sprintf("error while downloading schema at %s - received HTTP status %d", url, resp.StatusCode)
|
||||||
|
if r.debug {
|
||||||
|
log.Print(msg)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
|
msg := fmt.Sprintf("failed parsing schema from %s: %s", url, err)
|
||||||
|
if r.debug {
|
||||||
|
log.Print(msg)
|
||||||
|
}
|
||||||
|
return nil, errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.debug {
|
||||||
|
log.Printf("using schema found at %s", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.cache != nil {
|
if r.cache != nil {
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ func TestDownloadSchema(t *testing.T) {
|
||||||
"v1",
|
"v1",
|
||||||
"1.18.0",
|
"1.18.0",
|
||||||
nil,
|
nil,
|
||||||
fmt.Errorf("no schema found"),
|
fmt.Errorf("could not find schema at http://kubernetesjson.dev"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"getting 503",
|
"getting 503",
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,25 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LocalRegistry struct {
|
type LocalRegistry struct {
|
||||||
pathTemplate string
|
pathTemplate string
|
||||||
strict bool
|
strict bool
|
||||||
|
debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, strict bool, debug bool) (*LocalRegistry, error) {
|
||||||
return &LocalRegistry{
|
return &LocalRegistry{
|
||||||
pathTemplate,
|
pathTemplate,
|
||||||
strict,
|
strict,
|
||||||
|
debug,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,16 +32,32 @@ func (r LocalRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersi
|
||||||
f, err := os.Open(schemaFile)
|
f, err := os.Open(schemaFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil, newNotFoundError(fmt.Errorf("no schema found"))
|
msg := fmt.Sprintf("could not open file %s", schemaFile)
|
||||||
|
if r.debug {
|
||||||
|
log.Print(msg)
|
||||||
|
}
|
||||||
|
return nil, newNotFoundError(errors.New(msg))
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("failed to open schema %s", schemaFile)
|
|
||||||
|
msg := fmt.Sprintf("failed to open schema at %s: %s", schemaFile, err)
|
||||||
|
if r.debug {
|
||||||
|
log.Print(msg)
|
||||||
|
}
|
||||||
|
return nil, errors.New(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
content, err := ioutil.ReadAll(f)
|
content, err := ioutil.ReadAll(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("failed to read schema at %s: %s", schemaFile, err)
|
||||||
|
if r.debug {
|
||||||
|
log.Print(msg)
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.debug {
|
||||||
|
log.Printf("using schema found at %s", schemaFile)
|
||||||
|
}
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,12 +61,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 +81,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, cache 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
|
||||||
|
|
@ -92,8 +94,8 @@ func New(schemaLocation string, cache string, strict bool, skipTLS bool) (Regist
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(schemaLocation, "http") {
|
if strings.HasPrefix(schemaLocation, "http") {
|
||||||
return newHTTPRegistry(schemaLocation, cache, strict, skipTLS)
|
return newHTTPRegistry(schemaLocation, cache, strict, skipTLS, debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newLocalRegistry(schemaLocation, strict)
|
return newLocalRegistry(schemaLocation, strict, debug)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,18 @@ 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 {
|
||||||
|
|
@ -119,8 +131,3 @@ func (res *Resource) Resources() []Resource {
|
||||||
|
|
||||||
return []Resource{*res}
|
return []Resource{*res}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ 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
|
||||||
|
|
@ -61,7 +62,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
|
||||||
}
|
}
|
||||||
|
|
@ -97,12 +98,23 @@ type v struct {
|
||||||
// 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 +124,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)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,11 +261,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{})
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package validator
|
package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/yannh/kubeconform/pkg/registry"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yannh/kubeconform/pkg/registry"
|
||||||
|
|
||||||
"github.com/yannh/kubeconform/pkg/resource"
|
"github.com/yannh/kubeconform/pkg/resource"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -27,6 +28,7 @@ func TestValidate(t *testing.T) {
|
||||||
rawResource, schemaRegistry1 []byte
|
rawResource, schemaRegistry1 []byte
|
||||||
schemaRegistry2 []byte
|
schemaRegistry2 []byte
|
||||||
ignoreMissingSchema bool
|
ignoreMissingSchema bool
|
||||||
|
strict bool
|
||||||
expect Status
|
expect Status
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
|
@ -60,6 +62,7 @@ lastName: bar
|
||||||
}`),
|
}`),
|
||||||
nil,
|
nil,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
Valid,
|
Valid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -93,6 +96,7 @@ lastName: bar
|
||||||
}`),
|
}`),
|
||||||
nil,
|
nil,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
Invalid,
|
Invalid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -125,8 +129,61 @@ firstName: foo
|
||||||
}`),
|
}`),
|
||||||
nil,
|
nil,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
Invalid,
|
Invalid,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resource has invalid yaml",
|
"resource has invalid yaml",
|
||||||
[]byte(`
|
[]byte(`
|
||||||
|
|
@ -161,6 +218,7 @@ lastName: bar
|
||||||
}`),
|
}`),
|
||||||
nil,
|
nil,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
Error,
|
Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -196,6 +254,7 @@ lastName: bar
|
||||||
},
|
},
|
||||||
"required": ["firstName", "lastName"]
|
"required": ["firstName", "lastName"]
|
||||||
}`),
|
}`),
|
||||||
|
false,
|
||||||
false,
|
false,
|
||||||
Valid,
|
Valid,
|
||||||
},
|
},
|
||||||
|
|
@ -232,6 +291,7 @@ lastName: bar
|
||||||
},
|
},
|
||||||
"required": ["firstName", "lastName"]
|
"required": ["firstName", "lastName"]
|
||||||
}`),
|
}`),
|
||||||
|
false,
|
||||||
false,
|
false,
|
||||||
Valid,
|
Valid,
|
||||||
},
|
},
|
||||||
|
|
@ -246,6 +306,7 @@ lastName: bar
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
Skipped,
|
Skipped,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -259,6 +320,7 @@ lastName: bar
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
Error,
|
Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -272,6 +334,7 @@ lastName: bar
|
||||||
[]byte(`<html>error page</html>`),
|
[]byte(`<html>error page</html>`),
|
||||||
[]byte(`<html>error page</html>`),
|
[]byte(`<html>error page</html>`),
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
Skipped,
|
Skipped,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -285,6 +348,7 @@ lastName: bar
|
||||||
[]byte(`<html>error page</html>`),
|
[]byte(`<html>error page</html>`),
|
||||||
[]byte(`<html>error page</html>`),
|
[]byte(`<html>error page</html>`),
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
Error,
|
Error,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
|
@ -293,6 +357,7 @@ lastName: bar
|
||||||
SkipKinds: map[string]struct{}{},
|
SkipKinds: map[string]struct{}{},
|
||||||
RejectKinds: map[string]struct{}{},
|
RejectKinds: map[string]struct{}{},
|
||||||
IgnoreMissingSchemas: testCase.ignoreMissingSchema,
|
IgnoreMissingSchemas: testCase.ignoreMissingSchema,
|
||||||
|
Strict: testCase.strict,
|
||||||
},
|
},
|
||||||
schemaCache: nil,
|
schemaCache: nil,
|
||||||
schemaDownload: downloadSchema,
|
schemaDownload: downloadSchema,
|
||||||
|
|
@ -306,7 +371,11 @@ lastName: bar
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if got := val.ValidateResource(resource.Resource{Bytes: testCase.rawResource}); got.Status != testCase.expect {
|
if got := val.ValidateResource(resource.Resource{Bytes: testCase.rawResource}); got.Status != testCase.expect {
|
||||||
t.Errorf("%d - expected %d, got %d: %s", i, testCase.expect, got.Status, got.Err.Error())
|
if got.Err != nil {
|
||||||
|
t.Errorf("%d - expected %d, got %d: %s", i, testCase.expect, got.Status, got.Err.Error())
|
||||||
|
} else {
|
||||||
|
t.Errorf("%d - expected %d, got %d", i, testCase.expect, got.Status)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
||||||
-skip string
|
-skip string
|
||||||
comma-separated list of kinds to ignore
|
comma-separated list of kinds 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
|
-v show version information
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
||||||
-skip string
|
-skip string
|
||||||
comma-separated list of kinds to ignore
|
comma-separated list of kinds 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
|
-v show version information
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue