From 4e96b44a8bffac1d5ebdcf16dfa38b0634ec71fb Mon Sep 17 00:00:00 2001 From: Yann Hamon Date: Sun, 15 Nov 2020 10:01:36 +0100 Subject: [PATCH] Add example how to use kubeconform as a library --- Readme.md | 7 ++++++ cmd/kubeconform/main.go | 17 +++++++++---- examples/main.go | 26 ++++++++++++++++++++ pkg/config/config.go | 4 --- pkg/config/config_test.go | 12 ++++----- pkg/validator/validator.go | 43 +++++++++++++++++++++++++++++++-- pkg/validator/validator_test.go | 2 +- 7 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 examples/main.go diff --git a/Readme.md b/Readme.md index 86333d7..b429f9a 100644 --- a/Readme.md +++ b/Readme.md @@ -191,6 +191,13 @@ sys 0m1,069s ``` +### Using kubeconform as a Go Module + +**Warning**: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged. + +Kubeconform contains a package that can be used as a library. +An example of usage can be found in [examples/main.go](examples/main.go) + ### Credits * @garethr for the [Kubeval](https://github.com/instrumenta/kubeval) and diff --git a/cmd/kubeconform/main.go b/cmd/kubeconform/main.go index 0efa36d..9769b15 100644 --- a/cmd/kubeconform/main.go +++ b/cmd/kubeconform/main.go @@ -74,25 +74,25 @@ func realMain() int { IgnoreMissingSchemas: cfg.IgnoreMissingSchemas, }) - var resourcesChan <-chan resource.Resource - var errors <-chan error validationResults := make(chan validator.Result) - ctx := context.Background() successChan := processResults(ctx, o, validationResults, cfg.ExitOnError) + var resourcesChan <-chan resource.Resource + var errors <-chan error if isStdin { resourcesChan, errors = resource.FromStream(ctx, "stdin", os.Stdin) } else { resourcesChan, errors = resource.FromFiles(ctx, cfg.IgnoreFilenamePatterns, cfg.Files...) } + // Process discovered resources across multiple workers wg := sync.WaitGroup{} for i := 0; i < cfg.NumberOfWorkers; i++ { wg.Add(1) go func(resources <-chan resource.Resource, validationResults chan<- validator.Result, v validator.Validator) { for res := range resources { - validationResults <- v.Validate(res) + validationResults <- v.ValidateResource(res) } wg.Done() }(resourcesChan, validationResults, v) @@ -100,6 +100,7 @@ func realMain() int { wg.Add(1) go func() { + // Process errors while discovering resources for err := range errors { if err == nil { continue @@ -111,8 +112,14 @@ func realMain() int { Err: err.Err, Status: validator.Error, } - ctx.Done() + } else { + validationResults <- validator.Result{ + Resource: resource.Resource{}, + Err: err, + Status: validator.Error, + } } + ctx.Done() } wg.Done() }() diff --git a/examples/main.go b/examples/main.go new file mode 100644 index 0000000..b7613cb --- /dev/null +++ b/examples/main.go @@ -0,0 +1,26 @@ +package main + +// WARNING: API of Kubeconform is still under development and not yet +// considered stable + +import ( + "github.com/yannh/kubeconform/pkg/validator" + "log" + "os" +) + +func main() { + filepath := "../fixtures/valid.yaml" + f, err := os.Open(filepath) + if err != nil { + log.Fatalf("failed opening %s: %s", filepath, err) + } + + v := validator.New(nil, validator.Opts{Strict: true}) + for i, res := range v.Validate(filepath, f) { // A file might contain multiple resources + // File starts with ---, the parser assumes a first empty resource + if res.Status != validator.Valid && res.Status != validator.Empty { + log.Fatalf("resource %d in file %s is not valid: %d, %s", i, filepath, res.Status, res.Err) + } + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index c0db57e..95ce14f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -88,10 +88,6 @@ func FromFlags(progName string, args []string) (Config, string, error) { c.RejectKinds = splitCSV(rejectKindsCSV) c.IgnoreFilenamePatterns = ignoreFilenamePatterns c.SchemaLocations = schemaLocationsParam - if len(c.SchemaLocations) == 0 { - c.SchemaLocations = append(c.SchemaLocations, "https://kubernetesjsonschema.dev") // if not specified, default behaviour is to use kubernetesjson-schema.dev as registry - } - c.Files = flags.Args() if c.Help { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 99c2665..e0a326e 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -52,7 +52,7 @@ func TestFromFlags(t *testing.T) { KubernetesVersion: "1.18.0", NumberOfWorkers: 4, OutputFormat: "text", - SchemaLocations: []string{"https://kubernetesjsonschema.dev"}, + SchemaLocations: nil, SkipKinds: map[string]bool{}, RejectKinds: map[string]bool{}, }, @@ -65,7 +65,7 @@ func TestFromFlags(t *testing.T) { KubernetesVersion: "1.18.0", NumberOfWorkers: 4, OutputFormat: "text", - SchemaLocations: []string{"https://kubernetesjsonschema.dev"}, + SchemaLocations: nil, SkipKinds: map[string]bool{}, RejectKinds: map[string]bool{}, }, @@ -77,7 +77,7 @@ func TestFromFlags(t *testing.T) { KubernetesVersion: "1.18.0", NumberOfWorkers: 4, OutputFormat: "text", - SchemaLocations: []string{"https://kubernetesjsonschema.dev"}, + SchemaLocations: nil, SkipKinds: map[string]bool{"a": true, "b": true, "c": true}, RejectKinds: map[string]bool{}, }, @@ -89,7 +89,7 @@ func TestFromFlags(t *testing.T) { KubernetesVersion: "1.18.0", NumberOfWorkers: 4, OutputFormat: "text", - SchemaLocations: []string{"https://kubernetesjsonschema.dev"}, + SchemaLocations: nil, SkipKinds: map[string]bool{}, RejectKinds: map[string]bool{}, Summary: true, @@ -116,10 +116,10 @@ func TestFromFlags(t *testing.T) { }, } - for _, testCase := range testCases { + for i, testCase := range testCases { cfg, _, _ := FromFlags("kubeconform", testCase.args) if reflect.DeepEqual(cfg, testCase.conf) != true { - t.Errorf("failed parsing config - expected , got: \n%+v\n%+v", testCase.conf, cfg) + t.Errorf("test %d: failed parsing config - expected , got: \n%+v\n%+v", i, testCase.conf, cfg) } } } diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go index de84fba..e6d1618 100644 --- a/pkg/validator/validator.go +++ b/pkg/validator/validator.go @@ -1,10 +1,12 @@ package validator import ( + "context" "fmt" "github.com/yannh/kubeconform/pkg/cache" "github.com/yannh/kubeconform/pkg/registry" "github.com/yannh/kubeconform/pkg/resource" + "io" "github.com/xeipuuv/gojsonschema" "sigs.k8s.io/yaml" @@ -29,7 +31,8 @@ type Result struct { } type Validator interface { - Validate(res resource.Resource) Result + ValidateResource(res resource.Resource) Result + Validate(filename string, r io.Reader) []Result } type Opts struct { @@ -42,11 +45,20 @@ type Opts struct { } func New(schemaLocations []string, opts Opts) Validator { + // Default to kubernetesjsonschema.dev + if schemaLocations == nil || len(schemaLocations) == 0 { + schemaLocations = []string{"https://kubernetesjsonschema.dev"} + } + registries := []registry.Registry{} for _, schemaLocation := range schemaLocations { registries = append(registries, registry.New(schemaLocation, opts.Strict, opts.SkipTLS)) } + if opts.KubernetesVersion == "" { + opts.KubernetesVersion = "1.18.0" + } + if opts.SkipKinds == nil { opts.SkipKinds = map[string]bool{} } @@ -69,7 +81,7 @@ type v struct { regs []registry.Registry } -func (val *v) Validate(res resource.Resource) Result { +func (val *v) ValidateResource(res resource.Resource) Result { skip := func(signature resource.Signature) bool { isSkipKind, ok := val.opts.SkipKinds[signature.Kind] return ok && isSkipKind @@ -151,6 +163,33 @@ func (val *v) Validate(res resource.Resource) Result { return Result{Resource: res, Status: Invalid, Err: fmt.Errorf("%s", msg)} } +func (val *v) ValidateWithContext(ctx context.Context, filename string, r io.Reader) []Result { + validationResults := []Result{} + resourcesChan, _ := resource.FromStream(ctx, filename, r) + for { + select { + case res, ok := <-resourcesChan: + validationResults = append(validationResults, val.ValidateResource(res)) + if !ok { + resourcesChan = nil + } + + case <-ctx.Done(): + break + } + + if resourcesChan == nil { + break + } + } + + return validationResults +} + +func (val *v) Validate(filename string, r io.Reader) []Result { + return val.ValidateWithContext(context.Background(), filename, r) +} + func downloadSchema(registries []registry.Registry, kind, version, k8sVersion string) (*gojsonschema.Schema, error) { var err error var schemaBytes []byte diff --git a/pkg/validator/validator_test.go b/pkg/validator/validator_test.go index c70a1af..8e7e053 100644 --- a/pkg/validator/validator_test.go +++ b/pkg/validator/validator_test.go @@ -149,7 +149,7 @@ lastName: bar }, regs: nil, } - if got := val.Validate(resource.Resource{Bytes: testCase.rawResource}); got.Status != testCase.expect { + if got := val.ValidateResource(resource.Resource{Bytes: testCase.rawResource}); got.Status != testCase.expect { t.Errorf("%d - expected %d, got %d", i, testCase.expect, got.Status) } }