mirror of
https://github.com/yannh/kubeconform.git
synced 2026-02-18 09:27:02 +00:00
commit
4a2aaa4c5a
15 changed files with 241 additions and 458 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
22
pkg/resource/stream.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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)}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue