Merge pull request #13 from yannh/refactor

refactor
This commit is contained in:
Yann Hamon 2020-11-01 16:30:01 +01:00 committed by GitHub
commit 4a2aaa4c5a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 241 additions and 458 deletions

View file

@ -1,17 +1,13 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"github.com/xeipuuv/gojsonschema" "github.com/xeipuuv/gojsonschema"
"github.com/yannh/kubeconform/pkg/config"
"io"
"io/ioutil"
"os" "os"
"strings"
"sync" "sync"
"github.com/yannh/kubeconform/pkg/cache" "github.com/yannh/kubeconform/pkg/cache"
"github.com/yannh/kubeconform/pkg/config"
"github.com/yannh/kubeconform/pkg/fsutils" "github.com/yannh/kubeconform/pkg/fsutils"
"github.com/yannh/kubeconform/pkg/output" "github.com/yannh/kubeconform/pkg/output"
"github.com/yannh/kubeconform/pkg/registry" "github.com/yannh/kubeconform/pkg/registry"
@ -19,23 +15,6 @@ import (
"github.com/yannh/kubeconform/pkg/validator" "github.com/yannh/kubeconform/pkg/validator"
) )
type validationResult struct {
filename, kind, version, Name string
err error
skipped bool
}
func resourcesFromReader(r io.Reader) ([][]byte, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return [][]byte{}, err
}
resources := bytes.Split(data, []byte("---\n"))
return resources, nil
}
func downloadSchema(registries []registry.Registry, kind, version, k8sVersion string) (*gojsonschema.Schema, error) { func downloadSchema(registries []registry.Registry, kind, version, k8sVersion string) (*gojsonschema.Schema, error) {
var err error var err error
var schemaBytes []byte var schemaBytes []byte
@ -57,99 +36,82 @@ 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
} }
// filter returns true if the file should be skipped func ValidateResources(resources <-chan []resource.Resource, validationResults chan<- validator.Result, regs []registry.Registry, k8sVersion string, c *cache.SchemaCache, skip func(signature resource.Signature) bool, ignoreMissingSchemas bool) {
// Returning an array, this Reader might container multiple resources for resBatch := range resources {
func ValidateStream(r io.Reader, regs []registry.Registry, k8sVersion string, c *cache.SchemaCache, skip func(signature resource.Signature) bool, ignoreMissingSchemas bool) []validationResult { for _, res := range resBatch {
rawResources, err := resourcesFromReader(r) sig, err := res.Signature()
if err != nil {
return []validationResult{{err: fmt.Errorf("failed reading file: %s", err)}}
}
validationResults := []validationResult{}
if len(rawResources) == 0 {
// In case a file has no resources, we want to capture that the file was parsed - and therefore send a message with an empty resource and no error
validationResults = append(validationResults, validationResult{kind: "", version: "", Name: "", err: nil, skipped: false})
}
for _, rawResource := range rawResources {
var sig resource.Signature
if sig, err = resource.SignatureFromBytes(rawResource); err != nil {
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: fmt.Errorf("error while parsing: %s", err)})
continue
}
if sig.Kind == "" {
validationResults = append(validationResults, validationResult{kind: "", version: "", Name: "", err: nil, skipped: false})
continue // We skip resoures that don't have a Kind defined
}
if skip(sig) {
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: nil, skipped: true})
continue
}
ok := false
var schema *gojsonschema.Schema
cacheKey := ""
if c != nil {
cacheKey = cache.Key(sig.Kind, sig.Version, k8sVersion)
schema, ok = c.Get(cacheKey)
}
if !ok {
schema, err = downloadSchema(regs, sig.Kind, sig.Version, k8sVersion)
if err != nil { if err != nil {
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: err, skipped: false}) validationResults <- validator.Result{Resource: res, Err: fmt.Errorf("error while parsing: %s", err), Status: validator.Error}
continue continue
} }
if sig.Kind == "" {
validationResults <- validator.Result{Resource: res, Err: nil, Status: validator.Empty}
continue // We skip resoures that don't have a Kind defined
}
if skip(*sig) {
validationResults <- validator.Result{Resource: res, Err: nil, Status: validator.Skipped}
continue
}
ok := false
var schema *gojsonschema.Schema
cacheKey := ""
if c != nil { if c != nil {
c.Set(cacheKey, schema) cacheKey = cache.Key(sig.Kind, sig.Version, k8sVersion)
schema, ok = c.Get(cacheKey)
} }
}
if schema == nil { if !ok {
if ignoreMissingSchemas { schema, err = downloadSchema(regs, sig.Kind, sig.Version, k8sVersion)
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: nil, skipped: true}) if err != nil {
} else { validationResults <- validator.Result{Resource: res, Err: err, Status: validator.Error}
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: fmt.Errorf("could not find schema for %s", sig.Kind), skipped: false}) continue
}
if c != nil {
c.Set(cacheKey, schema)
}
} }
}
err = validator.Validate(rawResource, schema) if schema == nil {
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: err}) if ignoreMissingSchemas {
validationResults <- validator.Result{Resource: res, Err: nil, Status: validator.Skipped}
} else {
validationResults <- validator.Result{Resource: res, Err: fmt.Errorf("could not find schema for %s", sig.Kind), Status: validator.Error}
}
}
validationResults <- validator.Validate(res, schema)
}
} }
return validationResults
} }
func processResults(o output.Output, validationResults chan []validationResult, result chan<- bool) { func processResults(o output.Output, validationResults chan validator.Result, result chan<- bool) {
success := true success := true
for results := range validationResults { for res := range validationResults {
for _, result := range results { if res.Err != nil {
if result.err != nil { success = false
success = false }
} if err := o.Write(res); err != nil {
fmt.Fprint(os.Stderr, "failed writing log\n")
if err := o.Write(result.filename, result.kind, result.Name, result.version, result.err, result.skipped); err != nil {
fmt.Fprint(os.Stderr, "failed writing log\n")
}
} }
} }
result <- success result <- success
} }
func getFiles(files []string, fileBatches chan []string, validationResults chan []validationResult) { func getFiles(files []string, filesChan chan<- string, validationResults chan validator.Result) {
for _, filename := range files { for _, filename := range files {
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
validationResults <- []validationResult{{ validationResults <- validator.Result{
filename: filename, Resource: resource.Resource{Path: filename},
err: err, Err: err,
skipped: false, Status: validator.Error,
}} }
continue continue
} }
defer file.Close() defer file.Close()
@ -157,23 +119,23 @@ func getFiles(files []string, fileBatches chan []string, validationResults chan
fi, err := file.Stat() fi, err := file.Stat()
switch { switch {
case err != nil: case err != nil:
validationResults <- []validationResult{{ validationResults <- validator.Result{
filename: filename, Resource: resource.Resource{Path: filename},
err: err, Err: err,
skipped: false, Status: validator.Error,
}} }
case fi.IsDir(): case fi.IsDir():
if err := fsutils.FindYamlInDir(filename, fileBatches, 10); err != nil { if err := fsutils.FindYamlInDir(filename, filesChan); err != nil {
validationResults <- []validationResult{{ validationResults <- validator.Result{
filename: filename, Resource: resource.Resource{Path: filename},
err: err, Err: err,
skipped: false, Status: validator.Error,
}} }
} }
default: default:
fileBatches <- []string{filename} filesChan <- filename
} }
} }
} }
@ -189,7 +151,6 @@ func realMain() int {
// Detect whether we have data being piped through stdin // Detect whether we have data being piped through stdin
stat, _ := os.Stdin.Stat() stat, _ := os.Stdin.Stat()
isStdin := (stat.Mode() & os.ModeCharDevice) == 0 isStdin := (stat.Mode() & os.ModeCharDevice) == 0
if len(cfg.Files) == 1 && cfg.Files[0] == "-" { if len(cfg.Files) == 1 && cfg.Files[0] == "-" {
isStdin = true isStdin = true
} }
@ -201,75 +162,76 @@ func realMain() int {
registries := []registry.Registry{} registries := []registry.Registry{}
for _, schemaLocation := range cfg.SchemaLocations { for _, schemaLocation := range cfg.SchemaLocations {
if !strings.HasSuffix(schemaLocation, "json") { // If we dont specify a full templated path, we assume the paths of kubernetesjsonschema.dev registries = append(registries, registry.New(schemaLocation, cfg.Strict))
schemaLocation += "/{{ .NormalizedVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json"
}
if strings.HasPrefix(schemaLocation, "http") {
registries = append(registries, registry.NewHTTPRegistry(schemaLocation, cfg.Strict))
} else {
registries = append(registries, registry.NewLocalRegistry(schemaLocation, cfg.Strict))
}
} }
validationResults := make(chan []validationResult)
c := cache.New()
fileBatches := make(chan []string)
go func() {
getFiles(cfg.Files, fileBatches, validationResults)
close(fileBatches)
}()
var o output.Output var o output.Output
if o, err = output.New(cfg.OutputFormat, cfg.Summary, isStdin, cfg.Verbose); err != nil { if o, err = output.New(cfg.OutputFormat, cfg.Summary, isStdin, cfg.Verbose); err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
return 1 return 1
} }
validationResults := make(chan validator.Result)
res := make(chan bool) res := make(chan bool)
go processResults(o, validationResults, res) go processResults(o, validationResults, res)
if isStdin { files := make(chan string)
res := ValidateStream(os.Stdin, registries, cfg.KubernetesVersion, c, filter, cfg.IgnoreMissingSchemas) go func() {
for i := range res { getFiles(cfg.Files, files, validationResults)
res[i].filename = "stdin" close(files)
} }()
validationResults <- res
} else {
var wg sync.WaitGroup
for i := 0; i < cfg.NumberOfWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for fileBatch := range fileBatches { resourcesChan := make(chan []resource.Resource)
for _, filename := range fileBatch { c := cache.New()
f, err := os.Open(filename) wg := sync.WaitGroup{}
if err != nil { for i := 0; i < cfg.NumberOfWorkers; i++ {
validationResults <- []validationResult{{ wg.Add(1)
filename: filename, go func() {
err: err, ValidateResources(resourcesChan, validationResults, registries, cfg.KubernetesVersion, c, filter, cfg.IgnoreMissingSchemas)
skipped: true, wg.Done()
}} }()
continue
}
res := ValidateStream(f, registries, cfg.KubernetesVersion, c, filter, cfg.IgnoreMissingSchemas)
f.Close()
for i := range res {
res[i].filename = filename
}
validationResults <- res
}
}
}()
}
wg.Wait()
} }
if isStdin {
resources, err := resource.FromStream("stdin", os.Stdin)
if err != nil {
validationResults <- validator.Result{
Resource: resource.Resource{Path: "stdin"},
Err: err,
Status: validator.Error,
}
} else {
resourcesChan <- resources
}
} else {
for filename := range files {
f, err := os.Open(filename)
if err != nil {
validationResults <- validator.Result{
Resource: resource.Resource{Path: filename},
Err: err,
Status: validator.Error,
}
continue
}
resources, err := resource.FromStream(filename, f)
if err != nil {
validationResults <- validator.Result{
Resource: resource.Resource{Path: filename},
Err: err,
Status: validator.Error,
}
continue
}
resourcesChan <- resources
f.Close()
}
}
close(resourcesChan)
wg.Wait()
close(validationResults) close(validationResults)
success := <-res success := <-res
o.Flush() o.Flush()

View file

@ -8,27 +8,17 @@ import (
// FindYamlInDir will find yaml files in folder dir, and send their filenames in batches // FindYamlInDir will find yaml files in folder dir, and send their filenames in batches
// of size batchSize to channel fileBatches // of size batchSize to channel fileBatches
func FindYamlInDir(dir string, fileBatches chan<- []string, batchSize int) error { func FindYamlInDir(dir string, fileBatches chan<- string) error {
files := []string{}
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
if !info.IsDir() && (strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml")) { if !info.IsDir() && (strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml")) {
files = append(files, path) fileBatches <- path
if len(files) > batchSize {
fileBatches <- files
files = []string{}
}
} }
return nil return nil
}) })
if len(files) > 0 {
fileBatches <- files
}
return err return err
} }

View file

@ -3,10 +3,11 @@ package output
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/yannh/kubeconform/pkg/validator"
"io" "io"
) )
type result struct { type oresult struct {
Filename string `json:"filename"` Filename string `json:"filename"`
Kind string `json:"kind"` Kind string `json:"kind"`
Name string `json:"name"` Name string `json:"name"`
@ -19,7 +20,7 @@ type jsono struct {
w io.Writer w io.Writer
withSummary bool withSummary bool
verbose bool verbose bool
results []result results []oresult
nValid, nInvalid, nErrors, nSkipped int nValid, nInvalid, nErrors, nSkipped int
} }
@ -29,7 +30,7 @@ func jsonOutput(w io.Writer, withSummary bool, isStdin, verbose bool) Output {
w: w, w: w,
withSummary: withSummary, withSummary: withSummary,
verbose: verbose, verbose: verbose,
results: []result{}, results: []oresult{},
nValid: 0, nValid: 0,
nInvalid: 0, nInvalid: 0,
nErrors: 0, nErrors: 0,
@ -38,31 +39,30 @@ func jsonOutput(w io.Writer, withSummary bool, isStdin, verbose bool) Output {
} }
// JSON.Write will only write when JSON.Flush has been called // JSON.Write will only write when JSON.Flush has been called
func (o *jsono) Write(filename, kind, name, version string, err error, skipped bool) error { func (o *jsono) Write(result validator.Result) error {
msg, st := "", "" msg, st := "", ""
s := status(kind, name, err, skipped) switch result.Status {
case validator.Valid:
switch s {
case statusValid:
st = "statusValid" st = "statusValid"
o.nValid++ o.nValid++
case statusInvalid: case validator.Invalid:
st = "statusInvalid" st = "statusInvalid"
msg = err.Error() msg = result.Err.Error()
o.nInvalid++ o.nInvalid++
case statusError: case validator.Error:
st = "statusError" st = "statusError"
msg = err.Error() msg = result.Err.Error()
o.nErrors++ o.nErrors++
case statusSkipped: case validator.Skipped:
st = "statusSkipped" st = "statusSkipped"
o.nSkipped++ o.nSkipped++
case statusEmpty: case validator.Empty:
} }
if o.verbose || (s != statusValid && s != statusSkipped && s != statusEmpty) { if o.verbose || (result.Status != validator.Valid && result.Status != validator.Skipped && result.Status != validator.Empty) {
o.results = append(o.results, result{Filename: filename, Kind: kind, Name: name, Version: version, Status: st, Msg: msg}) sig, _ := result.Resource.Signature()
o.results = append(o.results, oresult{Filename: result.Resource.Path, Kind: sig.Kind, Name: sig.Name, Version: sig.Version, Status: st, Msg: msg})
} }
return nil return nil
@ -75,7 +75,7 @@ func (o *jsono) Flush() error {
if o.withSummary { if o.withSummary {
jsonObj := struct { jsonObj := struct {
Resources []result `json:"resources"` Resources []oresult `json:"resources"`
Summary struct { Summary struct {
Valid int `json:"valid"` Valid int `json:"valid"`
Invalid int `json:"invalid"` Invalid int `json:"invalid"`
@ -100,7 +100,7 @@ func (o *jsono) Flush() error {
res, err = json.MarshalIndent(jsonObj, "", " ") res, err = json.MarshalIndent(jsonObj, "", " ")
} else { } else {
jsonObj := struct { jsonObj := struct {
Resources []result `json:"resources"` Resources []oresult `json:"resources"`
}{ }{
Resources: o.results, Resources: o.results,
} }

View file

@ -1,114 +0,0 @@
package output
import (
"bytes"
"testing"
)
func TestJSONWrite(t *testing.T) {
type result struct {
fileName, kind, name, version string
err error
skipped bool
}
for _, testCase := range []struct {
name string
withSummary bool
verbose bool
res []result
expect string
}{
{
"a single deployment, no summary, no verbose",
false,
false,
[]result{
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
},
},
`{
"resources": []
}
`,
},
{
"a single deployment, summary, no verbose",
true,
false,
[]result{
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
},
},
`{
"resources": [],
"summary": {
"valid": 1,
"invalid": 0,
"errors": 0,
"skipped": 0
}
}
`,
},
{
"a single deployment, verbose, with summary",
true,
true,
[]result{
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
},
},
`{
"resources": [
{
"filename": "deployment.yml",
"kind": "Deployment",
"name": "my-app",
"version": "apps/v1",
"status": "statusValid",
"msg": ""
}
],
"summary": {
"valid": 1,
"invalid": 0,
"errors": 0,
"skipped": 0
}
}
`,
},
} {
w := new(bytes.Buffer)
o := jsonOutput(w, testCase.withSummary, false, testCase.verbose)
for _, res := range testCase.res {
o.Write(res.fileName, res.kind, res.name, res.version, res.err, res.skipped)
}
o.Flush()
if w.String() != testCase.expect {
t.Fatalf("%s - expected %s, got %s", testCase.name, testCase.expect, w)
}
}
}

View file

@ -6,17 +6,8 @@ import (
"os" "os"
) )
const (
_ = iota
statusValid
statusInvalid
statusError
statusSkipped
statusEmpty
)
type Output interface { type Output interface {
Write(filename, kind, name, version string, err error, skipped bool) error Write(validator.Result) error
Flush() error Flush() error
} }
@ -32,22 +23,3 @@ func New(outputFormat string, printSummary, isStdin, verbose bool) (Output, erro
return nil, fmt.Errorf("`outputFormat` must be 'text' or 'json'") return nil, fmt.Errorf("`outputFormat` must be 'text' or 'json'")
} }
} }
func status(kind, name string, err error, skipped bool) int {
if name == "" && kind == "" && err == nil && skipped == false {
return statusEmpty
}
if skipped {
return statusSkipped
}
if err != nil {
if _, ok := err.(validator.InvalidResourceError); ok {
return statusInvalid
}
return statusError
}
return statusValid
}

View file

@ -2,6 +2,7 @@ package output
import ( import (
"fmt" "fmt"
"github.com/yannh/kubeconform/pkg/validator"
"io" "io"
"sync" "sync"
) )
@ -31,35 +32,37 @@ func textOutput(w io.Writer, withSummary, isStdin, verbose bool) Output {
} }
} }
func (o *texto) Write(filename, kind, name, version string, reserr error, skipped bool) error { func (o *texto) Write(result validator.Result) error {
o.Lock() o.Lock()
defer o.Unlock() defer o.Unlock()
var err error var err error
o.files[filename] = true sig, _ := result.Resource.Signature()
switch status(kind, name, reserr, skipped) {
case statusValid: o.files[result.Resource.Path] = true
switch result.Status {
case validator.Valid:
if o.verbose { if o.verbose {
_, err = fmt.Fprintf(o.w, "%s - %s %s is valid\n", filename, kind, name) _, err = fmt.Fprintf(o.w, "%s - %s %s is valid\n", result.Resource.Path, sig.Kind, sig.Name)
} }
o.nValid++ o.nValid++
case statusInvalid: case validator.Invalid:
_, err = fmt.Fprintf(o.w, "%s - %s %s is invalid: %s\n", filename, kind, name, reserr) _, err = fmt.Fprintf(o.w, "%s - %s %s is invalid: %s\n", result.Resource.Path, sig.Kind, sig.Name, result.Err)
o.nInvalid++ o.nInvalid++
case statusError: case validator.Error:
if kind != "" && name != "" { if sig.Kind != "" && sig.Name != "" {
_, err = fmt.Fprintf(o.w, "%s - %s %s failed validation: %s\n", filename, kind, name, reserr) _, err = fmt.Fprintf(o.w, "%s - %s %s failed validation: %s\n", result.Resource.Path, sig.Kind, sig.Name, result.Err)
} else { } else {
_, err = fmt.Fprintf(o.w, "%s - failed validation: %s\n", filename, reserr) _, err = fmt.Fprintf(o.w, "%s - failed validation: %s\n", result.Resource.Path, result.Err)
} }
o.nErrors++ o.nErrors++
case statusSkipped: case validator.Skipped:
if o.verbose { if o.verbose {
_, err = fmt.Fprintf(o.w, "%s - %s %s skipped\n", filename, name, kind) _, err = fmt.Fprintf(o.w, "%s - %s %s skipped\n", result.Resource.Path, sig.Name, sig.Kind)
} }
o.nSkipped++ o.nSkipped++
case statusEmpty: // sent to ensure we count the filename as parsed case validator.Empty: // sent to ensure we count the filename as parsed
} }
return err return err

View file

@ -1,86 +0,0 @@
package output
import (
"bytes"
"testing"
)
func TestTextWrite(t *testing.T) {
type result struct {
fileName, kind, name, version string
err error
skipped bool
}
for _, testCase := range []struct {
name string
withSummary bool
verbose bool
res []result
expect string
}{
{
"a single deployment, no summary, no verbose",
false,
false,
[]result{
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
},
},
"",
},
{
"a single deployment, summary, no verbose",
true,
false,
[]result{
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
},
},
"Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0\n",
},
{
"a single deployment, verbose, with summary",
true,
true,
[]result{
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
},
},
`deployment.yml - Deployment my-app is valid
Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
`,
},
} {
w := new(bytes.Buffer)
o := textOutput(w, testCase.withSummary, false, testCase.verbose)
for _, res := range testCase.res {
o.Write(res.fileName, res.kind, res.name, res.version, res.err, res.skipped)
}
o.Flush()
if w.String() != testCase.expect {
t.Errorf("%s - expected: %s, got: %s", testCase.name, testCase.expect, w)
}
}
}

View file

@ -22,7 +22,7 @@ func newDownloadError(err error, isRetryable bool) *downloadError {
func (e *downloadError) IsRetryable() bool { return e.isRetryable } func (e *downloadError) IsRetryable() bool { return e.isRetryable }
func (e *downloadError) Error() string { return e.err.Error() } func (e *downloadError) Error() string { return e.err.Error() }
func NewHTTPRegistry(schemaPathTemplate string, strict bool) *KubernetesRegistry { func newHTTPRegistry(schemaPathTemplate string, strict bool) *KubernetesRegistry {
return &KubernetesRegistry{ return &KubernetesRegistry{
schemaPathTemplate: schemaPathTemplate, schemaPathTemplate: schemaPathTemplate,
strict: strict, strict: strict,

View file

@ -23,7 +23,7 @@ func (e *fileNotFoundError) IsRetryable() bool { return e.isRetryable }
func (e *fileNotFoundError) Error() string { return e.err.Error() } func (e *fileNotFoundError) Error() string { return e.err.Error() }
// 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 { func newLocalRegistry(pathTemplate string, strict bool) *LocalRegistry {
return &LocalRegistry{ return &LocalRegistry{
pathTemplate, pathTemplate,
strict, strict,

View file

@ -64,3 +64,15 @@ func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict
return buf.String(), nil return buf.String(), nil
} }
func New(schemaLocation string, strict bool) Registry {
if !strings.HasSuffix(schemaLocation, "json") { // If we dont specify a full templated path, we assume the paths of kubernetesjsonschema.dev
schemaLocation += "/{{ .NormalizedVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json"
}
if strings.HasPrefix(schemaLocation, "http") {
return newHTTPRegistry(schemaLocation, strict)
} else {
return newLocalRegistry(schemaLocation, strict)
}
}

View file

@ -4,12 +4,21 @@ import (
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
type Resource struct {
Path string
Bytes []byte
sig *Signature
}
type Signature struct { type Signature struct {
Kind, Version, Namespace, Name string Kind, Version, Namespace, Name string
} }
// SignatureFromBytes returns key identifying elements from a []byte representing the resource func (res *Resource) Signature() (*Signature, error) {
func SignatureFromBytes(res []byte) (Signature, error) { if res.sig != nil {
return res.sig, nil
}
resource := struct { resource := struct {
APIVersion string `yaml:"apiVersion"` APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"` Kind string `yaml:"kind"`
@ -19,12 +28,14 @@ func SignatureFromBytes(res []byte) (Signature, error) {
GenerateName string `yaml:"generateName"` GenerateName string `yaml:"generateName"`
} `yaml:"Metadata"` } `yaml:"Metadata"`
}{} }{}
err := yaml.Unmarshal(res, &resource) err := yaml.Unmarshal(res.Bytes, &resource)
name := resource.Metadata.Name name := resource.Metadata.Name
if resource.Metadata.GenerateName != "" { if resource.Metadata.GenerateName != "" {
name = resource.Metadata.GenerateName + "{{ generateName }}" name = resource.Metadata.GenerateName + "{{ generateName }}"
} }
return Signature{Kind: resource.Kind, Version: resource.APIVersion, Namespace: resource.Metadata.Namespace, Name: name}, err // We cache the result to not unmarshall every time we want to access the signature
res.sig = &Signature{Kind: resource.Kind, Version: resource.APIVersion, Namespace: resource.Metadata.Namespace, Name: name}
return res.sig, err
} }

View file

@ -35,7 +35,8 @@ spec:
} }
for _, testCase := range testCases { for _, testCase := range testCases {
sig, err := resource.SignatureFromBytes(testCase.have) res := resource.Resource{Bytes: testCase.have}
sig, err := res.Signature()
if err != nil && err.Error() != testCase.err.Error() { if err != nil && err.Error() != testCase.err.Error() {
t.Errorf("test \"%s\" - received error: %s", testCase.name, err) t.Errorf("test \"%s\" - received error: %s", testCase.name, err)
} }
@ -44,6 +45,5 @@ spec:
sig.Namespace != testCase.want.Namespace { sig.Namespace != testCase.want.Namespace {
t.Errorf("test \"%s\": received %+v, expected %+v", testCase.name, sig, testCase.want) t.Errorf("test \"%s\": received %+v, expected %+v", testCase.name, sig, testCase.want)
} }
} }
} }

22
pkg/resource/stream.go Normal file
View file

@ -0,0 +1,22 @@
package resource
import (
"bytes"
"io"
"io/ioutil"
)
func FromStream(path string, r io.Reader) ([]Resource, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return []Resource{}, err
}
resources := []Resource{}
rawResources := bytes.Split(data, []byte("---\n"))
for _, rawResource := range rawResources {
resources = append(resources, Resource{Path: path, Bytes: rawResource})
}
return resources, nil
}

View file

@ -2,18 +2,22 @@ package validator
import ( import (
"fmt" "fmt"
"github.com/yannh/kubeconform/pkg/resource"
"github.com/xeipuuv/gojsonschema" "github.com/xeipuuv/gojsonschema"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
// InvalidResourceError is returned when a resource does not conform to type Status int
// the associated schema
type InvalidResourceError struct{ err string }
func (r InvalidResourceError) Error() string { const (
return r.err _ Status = iota
} Error
Skipped
Valid
Invalid
Empty
)
// ValidFormat is a type for quickly forcing // ValidFormat is a type for quickly forcing
// new formats on the gojsonschema loader // new formats on the gojsonschema loader
@ -33,26 +37,32 @@ func (f ValidFormat) IsFormat(input interface{}) bool {
// gojsonschema.FormatCheckers.Add("int-or-string", ValidFormat{}) // gojsonschema.FormatCheckers.Add("int-or-string", ValidFormat{})
// } // }
type Result struct {
Resource resource.Resource
Err error
Status Status
}
// Validate validates a single Kubernetes resource against a Json Schema // Validate validates a single Kubernetes resource against a Json Schema
func Validate(rawResource []byte, schema *gojsonschema.Schema) error { func Validate(res resource.Resource, schema *gojsonschema.Schema) Result {
if schema == nil { if schema == nil {
return nil return Result{Resource: res, Status: Skipped, Err: nil}
} }
var resource map[string]interface{} var resource map[string]interface{}
if err := yaml.Unmarshal(rawResource, &resource); err != nil { if err := yaml.Unmarshal(res.Bytes, &resource); err != nil {
return fmt.Errorf("error unmarshalling resource: %s", err) return Result{Resource: res, Status: Error, Err: fmt.Errorf("error unmarshalling resource: %s", err)}
} }
resourceLoader := gojsonschema.NewGoLoader(resource) resourceLoader := gojsonschema.NewGoLoader(resource)
results, err := schema.Validate(resourceLoader) results, err := schema.Validate(resourceLoader)
if err != nil { if err != nil {
// This error can only happen if the Object to validate is poorly formed. There's no hope of saving this one // This error can only happen if the Object to validate is poorly formed. There's no hope of saving this one
return fmt.Errorf("problem validating schema. Check JSON formatting: %s", err) return Result{Resource: res, Status: Error, Err: fmt.Errorf("problem validating schema. Check JSON formatting: %s", err)}
} }
if results.Valid() { if results.Valid() {
return nil return Result{Resource: res, Status: Valid}
} }
msg := "" msg := ""
@ -62,5 +72,6 @@ func Validate(rawResource []byte, schema *gojsonschema.Schema) error {
} }
msg += errMsg.Description() msg += errMsg.Description()
} }
return InvalidResourceError{err: msg}
return Result{Resource: res, Status: Invalid, Err: fmt.Errorf("%s", msg)}
} }

View file

@ -2,13 +2,13 @@ package validator
import ( import (
"fmt" "fmt"
"github.com/yannh/kubeconform/pkg/resource"
"testing" "testing"
"github.com/xeipuuv/gojsonschema" "github.com/xeipuuv/gojsonschema"
) )
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
for i, testCase := range []struct { for i, testCase := range []struct {
name string name string
rawResource, schema []byte rawResource, schema []byte
@ -122,8 +122,8 @@ lastName: bar
if err != nil { if err != nil {
t.Errorf("failed parsing test schema") t.Errorf("failed parsing test schema")
} }
if got := Validate(testCase.rawResource, schema); ((got == nil) != (testCase.expect == nil)) || (got != nil && (got.Error() != testCase.expect.Error())) { if got := Validate(resource.Resource{Bytes: testCase.rawResource}, schema); ((got.Err == nil) != (testCase.expect == nil)) || (got.Err != nil && (got.Err.Error() != testCase.expect.Error())) {
t.Errorf("%d - expected %s, got %s", i, testCase.expect, got) t.Errorf("%d - expected %s, got %s", i, testCase.expect, got.Err)
} }
} }
} }