mirror of
https://github.com/yannh/kubeconform.git
synced 2026-02-22 03:07:01 +00:00
replace -schema with -local-registry
This commit is contained in:
parent
58363ddcfd
commit
59c23325d3
8 changed files with 110 additions and 130 deletions
16
Readme.md
16
Readme.md
|
|
@ -12,6 +12,22 @@ following improvements:
|
||||||
* **high performance**: will validate & download manifests over multiple routines
|
* **high performance**: will validate & download manifests over multiple routines
|
||||||
* support for **Kubernetes CRDs** (in progress)
|
* support for **Kubernetes CRDs** (in progress)
|
||||||
|
|
||||||
|
### 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 the same JSON schemas from kubernetesjsonschema.dev, and will download required
|
||||||
|
schemas at runtime as required.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,3 @@
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "Succeed parsing a CRD when additional schema passed" {
|
|
||||||
run bin/kubeconform -schema fixtures/crd_schema.yaml fixtures/test_crd.yaml
|
|
||||||
[ "$status" -eq 0 ]
|
|
||||||
}
|
|
||||||
|
|
|
||||||
16
main.go
16
main.go
|
|
@ -210,7 +210,7 @@ func getFiles(files []string, fileBatches chan []string, validationResults chan
|
||||||
}
|
}
|
||||||
|
|
||||||
func realMain() int {
|
func realMain() int {
|
||||||
var schemas arrayParam
|
var localRegistryFolders arrayParam
|
||||||
var skipKindsCSV, k8sVersion, outputFormat string
|
var skipKindsCSV, k8sVersion, outputFormat string
|
||||||
var summary, strict, verbose, ignoreMissingSchemas bool
|
var summary, strict, verbose, ignoreMissingSchemas bool
|
||||||
var nWorkers int
|
var nWorkers int
|
||||||
|
|
@ -218,7 +218,7 @@ func realMain() int {
|
||||||
var files []string
|
var files []string
|
||||||
|
|
||||||
flag.StringVar(&k8sVersion, "k8sversion", "1.18.0", "version of Kubernetes to test against")
|
flag.StringVar(&k8sVersion, "k8sversion", "1.18.0", "version of Kubernetes to test against")
|
||||||
flag.Var(&schemas, "schema", "file containing an additional Schema (can be specified multiple times)")
|
flag.Var(&localRegistryFolders, "local-registry", "folder containing additional schemas (can be specified multiple times)")
|
||||||
flag.BoolVar(&ignoreMissingSchemas, "ignore-missing-schemas", false, "skip files with missing schemas instead of failing")
|
flag.BoolVar(&ignoreMissingSchemas, "ignore-missing-schemas", false, "skip files with missing schemas instead of failing")
|
||||||
flag.BoolVar(&summary, "summary", false, "print a summary at the end")
|
flag.BoolVar(&summary, "summary", false, "print a summary at the end")
|
||||||
flag.IntVar(&nWorkers, "n", 4, "number of routines to run in parallel")
|
flag.IntVar(&nWorkers, "n", 4, "number of routines to run in parallel")
|
||||||
|
|
@ -241,12 +241,14 @@ func realMain() int {
|
||||||
|
|
||||||
registries := []registry.Registry{}
|
registries := []registry.Registry{}
|
||||||
registries = append(registries, registry.NewKubernetesRegistry(strict))
|
registries = append(registries, registry.NewKubernetesRegistry(strict))
|
||||||
if len(schemas) > 0 {
|
if len(localRegistryFolders) > 0 {
|
||||||
localRegistry, err := registry.NewLocalSchemas(schemas)
|
for _, localRegistryFolder := range localRegistryFolders {
|
||||||
if err != nil {
|
localRegistry, err := registry.NewLocalRegistry(localRegistryFolder, strict)
|
||||||
log.Fatalf("%s", err)
|
if err != nil {
|
||||||
|
log.Fatalf("%s", err)
|
||||||
|
}
|
||||||
|
registries = append(registries, localRegistry)
|
||||||
}
|
}
|
||||||
registries = append(registries, localRegistry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validationResults := make(chan []validationResult)
|
validationResults := make(chan []validationResult)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type KubernetesRegistry struct {
|
type KubernetesRegistry struct {
|
||||||
|
|
@ -30,30 +29,8 @@ func NewKubernetesRegistry(strict bool) *KubernetesRegistry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r KubernetesRegistry) schemaURL(resourceKind, resourceAPIVersion, k8sVersion string) string {
|
|
||||||
normalisedVersion := k8sVersion
|
|
||||||
if normalisedVersion != "master" {
|
|
||||||
normalisedVersion = "v" + normalisedVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
strictSuffix := ""
|
|
||||||
if r.strict {
|
|
||||||
strictSuffix = "-strict"
|
|
||||||
}
|
|
||||||
|
|
||||||
groupParts := strings.Split(resourceAPIVersion, "/")
|
|
||||||
versionParts := strings.Split(groupParts[0], ".")
|
|
||||||
|
|
||||||
kindSuffix := "-" + strings.ToLower(versionParts[0])
|
|
||||||
if len(groupParts) > 1 {
|
|
||||||
kindSuffix += "-" + strings.ToLower(groupParts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s/%s-standalone%s/%s%s.json", r.baseURL, normalisedVersion, strictSuffix, strings.ToLower(resourceKind), kindSuffix)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r KubernetesRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) ([]byte, error) {
|
func (r KubernetesRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) ([]byte, error) {
|
||||||
url := r.schemaURL(resourceKind, resourceAPIVersion, k8sVersion)
|
url := r.baseURL + "/" + schemaPath(resourceKind, resourceAPIVersion, k8sVersion, r.strict)
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSchemaURL(t *testing.T) {
|
|
||||||
for i, testCase := range []struct {
|
|
||||||
resourceKind, resourceAPIVersion, k8sVersion, expected string
|
|
||||||
strict bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"Deployment",
|
|
||||||
"apps/v1",
|
|
||||||
"1.16.0",
|
|
||||||
"https://kubernetesjsonschema.dev/v1.16.0-standalone-strict/deployment-apps-v1.json",
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Deployment",
|
|
||||||
"apps/v1",
|
|
||||||
"1.16.0",
|
|
||||||
"https://kubernetesjsonschema.dev/v1.16.0-standalone/deployment-apps-v1.json",
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Service",
|
|
||||||
"v1",
|
|
||||||
"1.18.0",
|
|
||||||
"https://kubernetesjsonschema.dev/v1.18.0-standalone/service-v1.json",
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Service",
|
|
||||||
"v1",
|
|
||||||
"master",
|
|
||||||
"https://kubernetesjsonschema.dev/master-standalone/service-v1.json",
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
reg := NewKubernetesRegistry(testCase.strict)
|
|
||||||
if got := reg.schemaURL(testCase.resourceKind, testCase.resourceAPIVersion, testCase.k8sVersion); got != testCase.expected {
|
|
||||||
t.Errorf("%d - got %s, expected %s", i+1, got, testCase.expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,55 +4,25 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"sigs.k8s.io/yaml"
|
"path"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LocalSchemas struct {
|
type LocalRegistry struct {
|
||||||
schemas map[string]string
|
folder string
|
||||||
|
strict 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 NewLocalSchemas(schemaFiles []string) (*LocalSchemas, error) {
|
func NewLocalRegistry(folder string, strict bool) (*LocalRegistry, error) {
|
||||||
schemas := &LocalSchemas{
|
return &LocalRegistry{
|
||||||
schemas: map[string]string{},
|
folder,
|
||||||
}
|
strict,
|
||||||
|
}, nil
|
||||||
for _, schemaFile := range schemaFiles {
|
|
||||||
f, err := os.Open(schemaFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to open schema %s", schemaFile)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
content, err := ioutil.ReadAll(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read schema %s", schemaFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsedSchema struct {
|
|
||||||
Spec struct {
|
|
||||||
Names struct {
|
|
||||||
Kind string `json:"Kind"`
|
|
||||||
} `json:"Names"`
|
|
||||||
} `json:"Spec"`
|
|
||||||
}
|
|
||||||
err = yaml.Unmarshal(content, &parsedSchema) // Index Schemas by kind
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed parsing schema %s", schemaFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
schemas.schemas[parsedSchema.Spec.Names.Kind] = schemaFile
|
|
||||||
}
|
|
||||||
|
|
||||||
return schemas, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadSchema retrieves the schema from a file for the resource
|
// DownloadSchema retrieves the schema from a file for the resource
|
||||||
func (r LocalSchemas) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) ([]byte, error) {
|
func (r LocalRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) ([]byte, error) {
|
||||||
schemaFile, ok := r.schemas[resourceKind]
|
schemaFile := path.Join(r.folder, schemaPath(resourceKind, resourceAPIVersion, k8sVersion, r.strict))
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("no local schema for Kind %s", resourceKind)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Open(schemaFile)
|
f, err := os.Open(schemaFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -64,12 +34,5 @@ func (r LocalSchemas) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersio
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
asJSON := content
|
return content, nil
|
||||||
if strings.HasSuffix(schemaFile, ".yml") || strings.HasSuffix(schemaFile, ".yaml") {
|
|
||||||
asJSON, err = yaml.YAMLToJSON(content)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error converting manifest %s to JSON: %s", schemaFile, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return asJSON, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type Manifest struct {
|
type Manifest struct {
|
||||||
Kind, Version string
|
Kind, Version string
|
||||||
}
|
}
|
||||||
|
|
@ -13,3 +18,25 @@ type Registry interface {
|
||||||
type Retryable interface {
|
type Retryable interface {
|
||||||
IsRetryable() bool
|
IsRetryable() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func schemaPath(resourceKind, resourceAPIVersion, k8sVersion string, strict bool) string {
|
||||||
|
normalisedVersion := k8sVersion
|
||||||
|
if normalisedVersion != "master" {
|
||||||
|
normalisedVersion = "v" + normalisedVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
strictSuffix := ""
|
||||||
|
if strict {
|
||||||
|
strictSuffix = "-strict"
|
||||||
|
}
|
||||||
|
|
||||||
|
groupParts := strings.Split(resourceAPIVersion, "/")
|
||||||
|
versionParts := strings.Split(groupParts[0], ".")
|
||||||
|
|
||||||
|
kindSuffix := "-" + strings.ToLower(versionParts[0])
|
||||||
|
if len(groupParts) > 1 {
|
||||||
|
kindSuffix += "-" + strings.ToLower(groupParts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s-standalone%s/%s%s.json", normalisedVersion, strictSuffix, strings.ToLower(resourceKind), kindSuffix)
|
||||||
|
}
|
||||||
|
|
|
||||||
45
pkg/registry/registry_test.go
Normal file
45
pkg/registry/registry_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSchemaPath(t *testing.T) {
|
||||||
|
for i, testCase := range []struct {
|
||||||
|
resourceKind, resourceAPIVersion, k8sVersion, expected string
|
||||||
|
strict bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"Deployment",
|
||||||
|
"apps/v1",
|
||||||
|
"1.16.0",
|
||||||
|
"v1.16.0-standalone-strict/deployment-apps-v1.json",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Deployment",
|
||||||
|
"apps/v1",
|
||||||
|
"1.16.0",
|
||||||
|
"v1.16.0-standalone/deployment-apps-v1.json",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Service",
|
||||||
|
"v1",
|
||||||
|
"1.18.0",
|
||||||
|
"v1.18.0-standalone/service-v1.json",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Service",
|
||||||
|
"v1",
|
||||||
|
"master",
|
||||||
|
"master-standalone/service-v1.json",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if got := schemaPath(testCase.resourceKind, testCase.resourceAPIVersion, testCase.k8sVersion, testCase.strict); got != testCase.expected {
|
||||||
|
t.Errorf("%d - got %s, expected %s", i+1, got, testCase.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue