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
import (
"bytes"
"fmt"
"github.com/xeipuuv/gojsonschema"
"github.com/yannh/kubeconform/pkg/config"
"io"
"io/ioutil"
"os"
"strings"
"sync"
"github.com/yannh/kubeconform/pkg/cache"
"github.com/yannh/kubeconform/pkg/config"
"github.com/yannh/kubeconform/pkg/fsutils"
"github.com/yannh/kubeconform/pkg/output"
"github.com/yannh/kubeconform/pkg/registry"
@ -19,23 +15,6 @@ import (
"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) {
var err error
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
}
// filter returns true if the file should be skipped
// Returning an array, this Reader might container multiple resources
func ValidateStream(r io.Reader, regs []registry.Registry, k8sVersion string, c *cache.SchemaCache, skip func(signature resource.Signature) bool, ignoreMissingSchemas bool) []validationResult {
rawResources, err := resourcesFromReader(r)
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)
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) {
for resBatch := range resources {
for _, res := range resBatch {
sig, err := res.Signature()
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
}
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 {
c.Set(cacheKey, schema)
cacheKey = cache.Key(sig.Kind, sig.Version, k8sVersion)
schema, ok = c.Get(cacheKey)
}
}
if schema == nil {
if ignoreMissingSchemas {
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: nil, skipped: true})
} else {
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})
if !ok {
schema, err = downloadSchema(regs, sig.Kind, sig.Version, k8sVersion)
if err != nil {
validationResults <- validator.Result{Resource: res, Err: err, Status: validator.Error}
continue
}
if c != nil {
c.Set(cacheKey, schema)
}
}
}
err = validator.Validate(rawResource, schema)
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: err})
if schema == nil {
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
for results := range validationResults {
for _, result := range results {
if result.err != nil {
success = false
}
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")
}
for res := range validationResults {
if res.Err != nil {
success = false
}
if err := o.Write(res); err != nil {
fmt.Fprint(os.Stderr, "failed writing log\n")
}
}
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 {
file, err := os.Open(filename)
if err != nil {
validationResults <- []validationResult{{
filename: filename,
err: err,
skipped: false,
}}
validationResults <- validator.Result{
Resource: resource.Resource{Path: filename},
Err: err,
Status: validator.Error,
}
continue
}
defer file.Close()
@ -157,23 +119,23 @@ func getFiles(files []string, fileBatches chan []string, validationResults chan
fi, err := file.Stat()
switch {
case err != nil:
validationResults <- []validationResult{{
filename: filename,
err: err,
skipped: false,
}}
validationResults <- validator.Result{
Resource: resource.Resource{Path: filename},
Err: err,
Status: validator.Error,
}
case fi.IsDir():
if err := fsutils.FindYamlInDir(filename, fileBatches, 10); err != nil {
validationResults <- []validationResult{{
filename: filename,
err: err,
skipped: false,
}}
if err := fsutils.FindYamlInDir(filename, filesChan); err != nil {
validationResults <- validator.Result{
Resource: resource.Resource{Path: filename},
Err: err,
Status: validator.Error,
}
}
default:
fileBatches <- []string{filename}
filesChan <- filename
}
}
}
@ -189,7 +151,6 @@ func realMain() int {
// Detect whether we have data being piped through stdin
stat, _ := os.Stdin.Stat()
isStdin := (stat.Mode() & os.ModeCharDevice) == 0
if len(cfg.Files) == 1 && cfg.Files[0] == "-" {
isStdin = true
}
@ -201,75 +162,76 @@ func realMain() int {
registries := []registry.Registry{}
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
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))
}
registries = append(registries, registry.New(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
if o, err = output.New(cfg.OutputFormat, cfg.Summary, isStdin, cfg.Verbose); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
validationResults := make(chan validator.Result)
res := make(chan bool)
go processResults(o, validationResults, res)
if isStdin {
res := ValidateStream(os.Stdin, registries, cfg.KubernetesVersion, c, filter, cfg.IgnoreMissingSchemas)
for i := range res {
res[i].filename = "stdin"
}
validationResults <- res
} else {
var wg sync.WaitGroup
for i := 0; i < cfg.NumberOfWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
files := make(chan string)
go func() {
getFiles(cfg.Files, files, validationResults)
close(files)
}()
for fileBatch := range fileBatches {
for _, filename := range fileBatch {
f, err := os.Open(filename)
if err != nil {
validationResults <- []validationResult{{
filename: filename,
err: err,
skipped: true,
}}
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()
resourcesChan := make(chan []resource.Resource)
c := cache.New()
wg := sync.WaitGroup{}
for i := 0; i < cfg.NumberOfWorkers; i++ {
wg.Add(1)
go func() {
ValidateResources(resourcesChan, validationResults, registries, cfg.KubernetesVersion, c, filter, cfg.IgnoreMissingSchemas)
wg.Done()
}()
}
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)
success := <-res
o.Flush()

View file

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

View file

@ -3,10 +3,11 @@ package output
import (
"encoding/json"
"fmt"
"github.com/yannh/kubeconform/pkg/validator"
"io"
)
type result struct {
type oresult struct {
Filename string `json:"filename"`
Kind string `json:"kind"`
Name string `json:"name"`
@ -19,7 +20,7 @@ type jsono struct {
w io.Writer
withSummary bool
verbose bool
results []result
results []oresult
nValid, nInvalid, nErrors, nSkipped int
}
@ -29,7 +30,7 @@ func jsonOutput(w io.Writer, withSummary bool, isStdin, verbose bool) Output {
w: w,
withSummary: withSummary,
verbose: verbose,
results: []result{},
results: []oresult{},
nValid: 0,
nInvalid: 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
func (o *jsono) Write(filename, kind, name, version string, err error, skipped bool) error {
func (o *jsono) Write(result validator.Result) error {
msg, st := "", ""
s := status(kind, name, err, skipped)
switch s {
case statusValid:
switch result.Status {
case validator.Valid:
st = "statusValid"
o.nValid++
case statusInvalid:
case validator.Invalid:
st = "statusInvalid"
msg = err.Error()
msg = result.Err.Error()
o.nInvalid++
case statusError:
case validator.Error:
st = "statusError"
msg = err.Error()
msg = result.Err.Error()
o.nErrors++
case statusSkipped:
case validator.Skipped:
st = "statusSkipped"
o.nSkipped++
case statusEmpty:
case validator.Empty:
}
if o.verbose || (s != statusValid && s != statusSkipped && s != statusEmpty) {
o.results = append(o.results, result{Filename: filename, Kind: kind, Name: name, Version: version, Status: st, Msg: msg})
if o.verbose || (result.Status != validator.Valid && result.Status != validator.Skipped && result.Status != validator.Empty) {
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
@ -75,7 +75,7 @@ func (o *jsono) Flush() error {
if o.withSummary {
jsonObj := struct {
Resources []result `json:"resources"`
Resources []oresult `json:"resources"`
Summary struct {
Valid int `json:"valid"`
Invalid int `json:"invalid"`
@ -100,7 +100,7 @@ func (o *jsono) Flush() error {
res, err = json.MarshalIndent(jsonObj, "", " ")
} else {
jsonObj := struct {
Resources []result `json:"resources"`
Resources []oresult `json:"resources"`
}{
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"
)
const (
_ = iota
statusValid
statusInvalid
statusError
statusSkipped
statusEmpty
)
type Output interface {
Write(filename, kind, name, version string, err error, skipped bool) error
Write(validator.Result) 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'")
}
}
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 (
"fmt"
"github.com/yannh/kubeconform/pkg/validator"
"io"
"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()
defer o.Unlock()
var err error
o.files[filename] = true
switch status(kind, name, reserr, skipped) {
case statusValid:
sig, _ := result.Resource.Signature()
o.files[result.Resource.Path] = true
switch result.Status {
case validator.Valid:
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++
case statusInvalid:
_, err = fmt.Fprintf(o.w, "%s - %s %s is invalid: %s\n", filename, kind, name, reserr)
case validator.Invalid:
_, err = fmt.Fprintf(o.w, "%s - %s %s is invalid: %s\n", result.Resource.Path, sig.Kind, sig.Name, result.Err)
o.nInvalid++
case statusError:
if kind != "" && name != "" {
_, err = fmt.Fprintf(o.w, "%s - %s %s failed validation: %s\n", filename, kind, name, reserr)
case validator.Error:
if sig.Kind != "" && sig.Name != "" {
_, err = fmt.Fprintf(o.w, "%s - %s %s failed validation: %s\n", result.Resource.Path, sig.Kind, sig.Name, result.Err)
} 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++
case statusSkipped:
case validator.Skipped:
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++
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

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) Error() string { return e.err.Error() }
func NewHTTPRegistry(schemaPathTemplate string, strict bool) *KubernetesRegistry {
func newHTTPRegistry(schemaPathTemplate string, strict bool) *KubernetesRegistry {
return &KubernetesRegistry{
schemaPathTemplate: schemaPathTemplate,
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() }
// 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{
pathTemplate,
strict,

View file

@ -64,3 +64,15 @@ func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict
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"
)
type Resource struct {
Path string
Bytes []byte
sig *Signature
}
type Signature struct {
Kind, Version, Namespace, Name string
}
// SignatureFromBytes returns key identifying elements from a []byte representing the resource
func SignatureFromBytes(res []byte) (Signature, error) {
func (res *Resource) Signature() (*Signature, error) {
if res.sig != nil {
return res.sig, nil
}
resource := struct {
APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
@ -19,12 +28,14 @@ func SignatureFromBytes(res []byte) (Signature, error) {
GenerateName string `yaml:"generateName"`
} `yaml:"Metadata"`
}{}
err := yaml.Unmarshal(res, &resource)
err := yaml.Unmarshal(res.Bytes, &resource)
name := resource.Metadata.Name
if resource.Metadata.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 {
sig, err := resource.SignatureFromBytes(testCase.have)
res := resource.Resource{Bytes: testCase.have}
sig, err := res.Signature()
if err != nil && err.Error() != testCase.err.Error() {
t.Errorf("test \"%s\" - received error: %s", testCase.name, err)
}
@ -44,6 +45,5 @@ spec:
sig.Namespace != testCase.want.Namespace {
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 (
"fmt"
"github.com/yannh/kubeconform/pkg/resource"
"github.com/xeipuuv/gojsonschema"
"sigs.k8s.io/yaml"
)
// InvalidResourceError is returned when a resource does not conform to
// the associated schema
type InvalidResourceError struct{ err string }
type Status int
func (r InvalidResourceError) Error() string {
return r.err
}
const (
_ Status = iota
Error
Skipped
Valid
Invalid
Empty
)
// ValidFormat is a type for quickly forcing
// new formats on the gojsonschema loader
@ -33,26 +37,32 @@ func (f ValidFormat) IsFormat(input interface{}) bool {
// 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
func Validate(rawResource []byte, schema *gojsonschema.Schema) error {
func Validate(res resource.Resource, schema *gojsonschema.Schema) Result {
if schema == nil {
return nil
return Result{Resource: res, Status: Skipped, Err: nil}
}
var resource map[string]interface{}
if err := yaml.Unmarshal(rawResource, &resource); err != nil {
return fmt.Errorf("error unmarshalling resource: %s", err)
if err := yaml.Unmarshal(res.Bytes, &resource); err != nil {
return Result{Resource: res, Status: Error, Err: fmt.Errorf("error unmarshalling resource: %s", err)}
}
resourceLoader := gojsonschema.NewGoLoader(resource)
results, err := schema.Validate(resourceLoader)
if err != nil {
// 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() {
return nil
return Result{Resource: res, Status: Valid}
}
msg := ""
@ -62,5 +72,6 @@ func Validate(rawResource []byte, schema *gojsonschema.Schema) error {
}
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 (
"fmt"
"github.com/yannh/kubeconform/pkg/resource"
"testing"
"github.com/xeipuuv/gojsonschema"
)
func TestValidate(t *testing.T) {
for i, testCase := range []struct {
name string
rawResource, schema []byte
@ -122,8 +122,8 @@ lastName: bar
if err != nil {
t.Errorf("failed parsing test schema")
}
if got := Validate(testCase.rawResource, schema); ((got == nil) != (testCase.expect == nil)) || (got != nil && (got.Error() != testCase.expect.Error())) {
t.Errorf("%d - expected %s, got %s", i, testCase.expect, got)
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.Err)
}
}
}