Merge branch 'yannh:master' into master

This commit is contained in:
Eyar Zilberman 2022-11-08 17:40:53 +02:00 committed by GitHub
commit fff7023b7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 948 additions and 339 deletions

View file

@ -2,6 +2,13 @@ FROM alpine:3.14 as certs
RUN apk add ca-certificates
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>
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY kubeconform /

View file

@ -1,4 +1,11 @@
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>
RUN apk add ca-certificates
COPY kubeconform /

View file

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

View file

@ -31,12 +31,15 @@ docker-acceptance: build-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:v0.176.0 build --single-target --skip-post-hooks --rm-dist --snapshot
cp dist/kubeconform_linux_amd64/kubeconform bin/
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_v1/kubeconform bin/
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:
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

View file

@ -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
custom resources (CRDs) and offline validation capabilities
* uses by default a [self-updating fork](https://github.com/yannh/kubernetes-json-schema) of the schemas registry maintained
by the [kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) project - which guarantees
by the kubernetes-json-schema project - which guarantees
up-to-date **schemas for all recent versions of Kubernetes**.
### 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).
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
```
@ -64,8 +74,8 @@ $ ./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
-debug
print debug information
-exit-on-error
immediately stop execution when the first error is encountered
-h show help information
@ -82,16 +92,16 @@ Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
-output string
output format - json, junit, tap, text (default "text")
-reject string
comma-separated list of kinds to reject
comma-separated list of kinds or GVKs to reject
-schema-location value
override schemas location search path (can be specified multiple times)
-skip string
comma-separated list of kinds to ignore
comma-separated list of kinds or GVKs to ignore
-strict
disallow additional properties not in schema
disallow additional properties not in schema or duplicated keys
-summary
print a summary at the end (ignored for junit output)
-v show version information
-v show version information
-verbose
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
```
* 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
```
$ ./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
* *ResourceKind* - Kind of the Kubernetes Resource
* *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
### 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
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/).
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
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
Kubeconform will respect the HTTPS_PROXY variable when downloading schema files.

View file

@ -11,6 +11,12 @@ resetCacheFolder() {
[ "${lines[0]}" == 'Usage: bin/kubeconform [OPTION]... [FILE OR FOLDER]...' ]
}
@test "Fail and display help when using an incorrect flag" {
run bin/kubeconform -xyz
[ "$status" -eq 1 ]
[ "${lines[0]}" == 'flag provided but not defined: -xyz' ]
}
@test "Pass when parsing a valid Kubernetes config YAML file" {
run bin/kubeconform -summary fixtures/valid.yaml
[ "$status" -eq 0 ]
@ -132,6 +138,16 @@ resetCacheFolder() {
[ "$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" {
run bin/kubeconform -schema-location default fixtures/valid.yaml
[ "$status" -eq 0 ]
@ -202,6 +218,18 @@ resetCacheFolder() {
[ "$output" = "fixtures/valid.yaml - bob ReplicationController skipped" ]
}
@test "Skip when parsing a resource with a GVK to skip" {
run bin/kubeconform -verbose -skip v1/ReplicationController fixtures/valid.yaml
[ "$status" -eq 0 ]
[ "$output" = "fixtures/valid.yaml - bob ReplicationController skipped" ]
}
@test "Do not skip when parsing a resource with a GVK to skip, where the Kind matches but not the version" {
run bin/kubeconform -verbose -skip v2/ReplicationController fixtures/valid.yaml
[ "$status" -eq 0 ]
[ "$output" = "fixtures/valid.yaml - ReplicationController bob is valid" ]
}
@test "Fail when parsing a resource from a kind to reject" {
run bin/kubeconform -verbose -reject ReplicationController fixtures/valid.yaml
[ "$status" -eq 1 ]
@ -300,3 +328,10 @@ resetCacheFolder() {
[ "$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 ]
}

View file

@ -69,8 +69,9 @@ func realMain() int {
return 1
}
if cfg.CPUProfileFile != "" {
f, err := os.Create(cfg.CPUProfileFile)
cpuProfileFile := os.Getenv("KUBECONFORM_CPUPROFILE_FILE")
if cpuProfileFile != "" {
f, err := os.Create(cpuProfileFile)
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
@ -97,9 +98,10 @@ func realMain() int {
fmt.Fprintln(os.Stderr, err)
return 1
}
v, err := validator.New(cfg.SchemaLocations, validator.Opts{
var v validator.Validator
v, err = validator.New(cfg.SchemaLocations, validator.Opts{
Cache: cfg.Cache,
Debug: cfg.Debug,
SkipTLS: cfg.SkipTLS,
SkipKinds: cfg.SkipKinds,
RejectKinds: cfg.RejectKinds,

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

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>

4
pkg/cache/ondisk.go vendored
View file

@ -1,7 +1,7 @@
package cache
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
@ -23,7 +23,7 @@ func NewOnDiskCache(cache string) Cache {
}
func cachePath(folder, resourceKind, resourceAPIVersion, k8sVersion string) string {
hash := md5.Sum([]byte(fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)))
hash := sha256.Sum256([]byte(fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)))
return path.Join(folder, hex.EncodeToString(hash[:]))
}

View file

@ -9,7 +9,7 @@ import (
type Config struct {
Cache string
CPUProfileFile string
Debug bool
ExitOnError bool
Files []string
SchemaLocations []string
@ -56,7 +56,7 @@ func splitCSV(csvStr string) map[string]struct{} {
func FromFlags(progName string, args []string) (Config, string, error) {
var schemaLocationsParam, ignoreFilenamePatterns arrayParam
var skipKindsCSV, rejectKindsCSV string
flags := flag.NewFlagSet(progName, flag.ExitOnError)
flags := flag.NewFlagSet(progName, flag.ContinueOnError)
var buf bytes.Buffer
flags.SetOutput(&buf)
@ -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.Var(&schemaLocationsParam, "schema-location", "override schemas location search path (can be specified multiple times)")
flags.StringVar(&skipKindsCSV, "skip", "", "comma-separated list of kinds to ignore")
flags.StringVar(&rejectKindsCSV, "reject", "", "comma-separated list of kinds to reject")
flags.StringVar(&skipKindsCSV, "skip", "", "comma-separated list of kinds or GVKs to ignore")
flags.StringVar(&rejectKindsCSV, "reject", "", "comma-separated list of kinds or GVKs to reject")
flags.BoolVar(&c.Debug, "debug", false, "print debug information")
flags.BoolVar(&c.ExitOnError, "exit-on-error", false, "immediately stop execution when the first error is encountered")
flags.BoolVar(&c.IgnoreMissingSchemas, "ignore-missing-schemas", false, "skip files with missing schemas instead of failing")
flags.Var(&ignoreFilenamePatterns, "ignore-filename-pattern", "regular expression specifying paths to ignore (can be specified multiple times)")
flags.BoolVar(&c.Summary, "summary", false, "print a summary at the end (ignored for junit output)")
flags.IntVar(&c.NumberOfWorkers, "n", 4, "number of goroutines to run concurrently")
flags.BoolVar(&c.Strict, "strict", false, "disallow additional properties not in schema")
flags.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.BoolVar(&c.Verbose, "verbose", false, "print results for all resources (ignored for tap and junit output)")
flags.BoolVar(&c.SkipTLS, "insecure-skip-tls-verify", false, "disable verification of the server's SSL certificate. This will make your HTTPS connections insecure")
flags.StringVar(&c.Cache, "cache", "", "cache schemas downloaded via HTTP to this folder")
flags.StringVar(&c.CPUProfileFile, "cpu-prof", "", "debug - log CPU profiling to file")
flags.BoolVar(&c.Help, "h", false, "show help information")
flags.BoolVar(&c.Version, "v", false, "show version information")
flags.Usage = func() {

View file

@ -112,9 +112,10 @@ func TestFromFlags(t *testing.T) {
{
[]string{"-cache", "cache", "-ignore-missing-schemas", "-kubernetes-version", "1.16.0", "-n", "2", "-output", "json",
"-schema-location", "folder", "-schema-location", "anotherfolder", "-skip", "kinda,kindb", "-strict",
"-reject", "kindc,kindd", "-summary", "-verbose", "file1", "file2"},
"-reject", "kindc,kindd", "-summary", "-debug", "-verbose", "file1", "file2"},
Config{
Cache: "cache",
Debug: true,
Files: []string{"file1", "file2"},
IgnoreMissingSchemas: true,
KubernetesVersion: "1.16.0",

View file

@ -33,22 +33,22 @@ type Property struct {
}
type TestSuite struct {
XMLName xml.Name `xml:"testsuite"`
Properties []*Property `xml:"properties>property,omitempty"`
Cases []TestCase `xml:"testcase"`
Name string `xml:"name,attr"`
Id int `xml:"id,attr"`
Tests int `xml:"tests,attr"`
Failures int `xml:"failures,attr"`
Errors int `xml:"errors,attr"`
Disabled int `xml:"disabled,attr"`
Skipped int `xml:"skipped,attr"`
XMLName xml.Name `xml:"testsuite"`
Cases []TestCase `xml:"testcase"`
Name string `xml:"name,attr"`
Id int `xml:"id,attr"`
Tests int `xml:"tests,attr"`
Failures int `xml:"failures,attr"`
Errors int `xml:"errors,attr"`
Disabled int `xml:"disabled,attr"`
Skipped int `xml:"skipped,attr"`
}
type TestCase struct {
XMLName xml.Name `xml:"testcase"`
Name string `xml:"name,attr"`
ClassName string `xml:"classname,attr"`
Time int `xml:"time,attr"` // Optional, but for Buildkite support https://github.com/yannh/kubeconform/issues/127
Skipped *TestCaseSkipped `xml:"skipped,omitempty"`
Error *TestCaseError `xml:"error,omitempty"`
Failure []TestCaseError `xml:"failure,omitempty"`
@ -100,8 +100,7 @@ func (o *junito) Write(result validator.Result) error {
Name: result.Resource.Path,
Id: o.id,
Tests: 0, Failures: 0, Errors: 0, Disabled: 0, Skipped: 0,
Cases: make([]TestCase, 0),
Properties: make([]*Property, 0),
Cases: make([]TestCase, 0),
}
o.suites[result.Resource.Path] = suite
}

View file

@ -48,8 +48,7 @@ metadata:
},
"<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" +
" <properties></properties>\n" +
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\"></testcase>\n" +
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\"></testcase>\n" +
" </testsuite>\n" +
"</testsuites>\n",
},
@ -82,8 +81,7 @@ metadata:
},
"<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" +
" <properties></properties>\n" +
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\"></testcase>\n" +
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\"></testcase>\n" +
" </testsuite>\n" +
"</testsuites>\n",
},

View file

@ -2,8 +2,10 @@ package registry
import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"time"
@ -21,9 +23,10 @@ type SchemaRegistry struct {
schemaPathTemplate string
cache cache.Cache
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{
MaxIdleConns: 100,
IdleConnTimeout: 3 * time.Second,
@ -53,6 +56,7 @@ func newHTTPRegistry(schemaPathTemplate string, cacheFolder string, strict bool,
schemaPathTemplate: schemaPathTemplate,
cache: filecache,
strict: strict,
debug: debug,
}, nil
}
@ -71,21 +75,41 @@ func (r SchemaRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVers
resp, err := r.c.Get(url)
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()
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 {
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)
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 {

View file

@ -59,7 +59,7 @@ func TestDownloadSchema(t *testing.T) {
"v1",
"1.18.0",
nil,
fmt.Errorf("no schema found"),
fmt.Errorf("could not find schema at http://kubernetesjson.dev"),
},
{
"getting 503",

View file

@ -1,21 +1,25 @@
package registry
import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
)
type LocalRegistry struct {
pathTemplate string
strict bool
debug bool
}
// 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{
pathTemplate,
strict,
debug,
}, nil
}
@ -28,16 +32,32 @@ func (r LocalRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersi
f, err := os.Open(schemaFile)
if err != nil {
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()
content, err := ioutil.ReadAll(f)
if err != nil {
msg := fmt.Sprintf("failed to read schema at %s: %s", schemaFile, err)
if r.debug {
log.Print(msg)
}
return nil, err
}
if r.debug {
log.Printf("using schema found at %s", schemaFile)
}
return content, nil
}

View file

@ -61,12 +61,14 @@ func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict
StrictSuffix string
ResourceKind string
ResourceAPIVersion string
Group string
KindSuffix string
}{
normalisedVersion,
strictSuffix,
strings.ToLower(resourceKind),
groupParts[len(groupParts)-1],
groupParts[0],
kindSuffix,
}
@ -79,7 +81,7 @@ func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict
return buf.String(), nil
}
func New(schemaLocation string, cache string, strict bool, skipTLS bool) (Registry, error) {
func New(schemaLocation string, cache string, strict bool, skipTLS bool, debug bool) (Registry, error) {
if schemaLocation == "default" {
schemaLocation = "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json"
} else if !strings.HasSuffix(schemaLocation, "json") { // If we dont specify a full templated path, we assume the paths of our fork of kubernetes-json-schema
@ -92,8 +94,8 @@ func New(schemaLocation string, cache string, strict bool, skipTLS bool) (Regist
}
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)
}

View file

@ -20,6 +20,18 @@ type Signature struct {
Kind, Version, Namespace, Name string
}
// GroupVersionKind returns a string with the GVK encoding of a resource signature.
// This encoding slightly differs from the Kubernetes upstream implementation
// in order to be suitable for being used in the kubeconform command-line arguments.
func (sig *Signature) GroupVersionKind() string {
return fmt.Sprintf("%s/%s", sig.Version, sig.Kind)
}
// QualifiedName returns a string for a signature in the format version/kind/namespace/name
func (sig *Signature) QualifiedName() string {
return fmt.Sprintf("%s/%s/%s/%s", sig.Version, sig.Kind, sig.Namespace, sig.Name)
}
// Signature computes a signature for a resource, based on its Kind, Version, Namespace & Name
func (res *Resource) Signature() (*Signature, error) {
if res.sig != nil {
@ -119,8 +131,3 @@ func (res *Resource) Resources() []Resource {
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)
}

View file

@ -43,6 +43,7 @@ type Validator interface {
// Opts contains a set of options for the validator.
type Opts struct {
Cache string // Cache schemas downloaded via HTTP to this folder
Debug bool // Debug infos will be print here
SkipTLS bool // skip TLS validation when downloading from an HTTP Schema Registry
SkipKinds map[string]struct{} // List of resource Kinds to ignore
RejectKinds map[string]struct{} // List of resource Kinds to reject
@ -61,7 +62,7 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
registries := []registry.Registry{}
for _, schemaLocation := range schemaLocations {
reg, err := registry.New(schemaLocation, opts.Cache, opts.Strict, opts.SkipTLS)
reg, err := registry.New(schemaLocation, opts.Cache, opts.Strict, opts.SkipTLS, opts.Debug)
if err != nil {
return nil, err
}
@ -97,12 +98,23 @@ type v struct {
// ValidateResource validates a single resource. This allows to validate
// large resource streams using multiple Go Routines.
func (val *v) ValidateResource(res resource.Resource) Result {
// For backward compatibility reasons when determining whether
// a resource should be skipped or rejected we use both
// the GVK encoding of the resource signatures (the recommended method
// for skipping/rejecting resources) and the raw Kind.
skip := func(signature resource.Signature) bool {
if _, ok := val.opts.SkipKinds[signature.GroupVersionKind()]; ok {
return ok
}
_, ok := val.opts.SkipKinds[signature.Kind]
return ok
}
reject := func(signature resource.Signature) bool {
if _, ok := val.opts.RejectKinds[signature.GroupVersionKind()]; ok {
return ok
}
_, ok := val.opts.RejectKinds[signature.Kind]
return ok
}
@ -112,7 +124,12 @@ func (val *v) ValidateResource(res resource.Resource) Result {
}
var r map[string]interface{}
if err := yaml.Unmarshal(res.Bytes, &r); err != nil {
unmarshaller := yaml.Unmarshal
if val.opts.Strict {
unmarshaller = yaml.UnmarshalStrict
}
if err := unmarshaller(res.Bytes, &r); err != nil {
return Result{Resource: res, Status: Error, Err: fmt.Errorf("error unmarshalling resource: %s", err)}
}
@ -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
}
// 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,9 +1,10 @@
package validator
import (
"github.com/yannh/kubeconform/pkg/registry"
"testing"
"github.com/yannh/kubeconform/pkg/registry"
"github.com/yannh/kubeconform/pkg/resource"
)
@ -27,6 +28,7 @@ func TestValidate(t *testing.T) {
rawResource, schemaRegistry1 []byte
schemaRegistry2 []byte
ignoreMissingSchema bool
strict bool
expect Status
}{
{
@ -60,6 +62,7 @@ lastName: bar
}`),
nil,
false,
false,
Valid,
},
{
@ -93,6 +96,7 @@ lastName: bar
}`),
nil,
false,
false,
Invalid,
},
{
@ -125,8 +129,61 @@ firstName: foo
}`),
nil,
false,
false,
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",
[]byte(`
@ -161,6 +218,7 @@ lastName: bar
}`),
nil,
false,
false,
Error,
},
{
@ -196,6 +254,7 @@ lastName: bar
},
"required": ["firstName", "lastName"]
}`),
false,
false,
Valid,
},
@ -232,6 +291,7 @@ lastName: bar
},
"required": ["firstName", "lastName"]
}`),
false,
false,
Valid,
},
@ -246,6 +306,7 @@ lastName: bar
nil,
nil,
true,
false,
Skipped,
},
{
@ -259,6 +320,7 @@ lastName: bar
nil,
nil,
false,
false,
Error,
},
{
@ -272,6 +334,7 @@ lastName: bar
[]byte(`<html>error page</html>`),
[]byte(`<html>error page</html>`),
true,
false,
Skipped,
},
{
@ -285,6 +348,7 @@ lastName: bar
[]byte(`<html>error page</html>`),
[]byte(`<html>error page</html>`),
false,
false,
Error,
},
} {
@ -293,6 +357,7 @@ lastName: bar
SkipKinds: map[string]struct{}{},
RejectKinds: map[string]struct{}{},
IgnoreMissingSchemas: testCase.ignoreMissingSchema,
Strict: testCase.strict,
},
schemaCache: nil,
schemaDownload: downloadSchema,
@ -306,7 +371,11 @@ lastName: bar
},
}
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)
}
}
}
}

View file

@ -34,7 +34,7 @@ Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
-skip string
comma-separated list of kinds to ignore
-strict
disallow additional properties not in schema
disallow additional properties not in schema or duplicated keys
-summary
print a summary at the end (ignored for junit output)
-v show version information
@ -83,4 +83,4 @@ fixtures/crd_schema.yaml - CustomResourceDefinition trainingjobs.sagemaker.aws.a
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 >}}
{{< /prism >}}

View file

@ -59,7 +59,7 @@ Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
-skip string
comma-separated list of kinds to ignore
-strict
disallow additional properties not in schema
disallow additional properties not in schema or duplicated keys
-summary
print a summary at the end (ignored for junit output)
-v show version information
@ -117,4 +117,4 @@ Website powered by <a href=https://gohugo.io/>Hugo</a>
</div>
<script defer src=/js/prism.js></script>
</body>
</html>
</html>