Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: expose Kubeconform as a module #189

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 17 additions & 34 deletions cmd/kubeconform/main.go → cmd/kubeconform/validate.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package kubeconform

import (
"context"
Expand All @@ -15,8 +15,6 @@ import (
"github.com/yannh/kubeconform/pkg/validator"
)

var version = "development"

func processResults(cancel context.CancelFunc, o output.Output, validationResults <-chan validator.Result, exitOnError bool) <-chan bool {
success := true
result := make(chan bool)
Expand All @@ -28,7 +26,7 @@ func processResults(cancel context.CancelFunc, o output.Output, validationResult
}
if o != nil {
if err := o.Write(res); err != nil {
fmt.Fprint(os.Stderr, "failed writing log\n")
log.Fatal("failed writing log: ", err)
}
}
if !success && exitOnError {
Expand All @@ -46,27 +44,19 @@ func processResults(cancel context.CancelFunc, o output.Output, validationResult
return result
}

func realMain() int {
cfg, out, err := config.FromFlags(os.Args[0], os.Args[1:])
func Validate(cfg config.Config, out string) error {

if out != "" {
o := os.Stderr
errCode := 1
if cfg.Help {
o = os.Stdout
errCode = 0
fmt.Fprintln(cfg.Stream.Output, out)
return nil
}
fmt.Fprintln(o, out)
return errCode
return fmt.Errorf("config out is not empty")
}

if cfg.Version {
fmt.Println(version)
return 0
}

if err != nil {
fmt.Fprintf(os.Stderr, "failed parsing command line: %s\n", err.Error())
return 1
fmt.Fprintln(cfg.Stream.Output, out)
return nil
}

cpuProfileFile := os.Getenv("KUBECONFORM_CPUPROFILE_FILE")
Expand All @@ -93,13 +83,11 @@ func realMain() int {
useStdin = true
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we still refer to os.Stdin here instead of cfg.stream.stdin, but here we need Stat()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I put it away to think about the best way to make it.
I will get back to it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hold on a bit - I'm not super happy with the signature of "Validate" yet, I would want it to enable the caller to process the results in a way they see fit. I need to think about this a bit more 🙇

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great 👌 👌

}

var o output.Output
if o, err = output.New(cfg.OutputFormat, cfg.Summary, useStdin, cfg.Verbose); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
o, err := output.New(cfg.Stream.Output, cfg.OutputFormat, cfg.Summary, useStdin, cfg.Verbose)
if err != nil {
return fmt.Errorf("failed to get output: %s", err.Error())
}
var v validator.Validator
v, err = validator.New(cfg.SchemaLocations, validator.Opts{
v, err := validator.New(cfg.SchemaLocations, validator.Opts{
Cache: cfg.Cache,
Debug: cfg.Debug,
SkipTLS: cfg.SkipTLS,
Expand All @@ -110,8 +98,7 @@ func realMain() int {
IgnoreMissingSchemas: cfg.IgnoreMissingSchemas,
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
return fmt.Errorf("failed to validate: %s", err.Error())
}

validationResults := make(chan validator.Result)
Expand All @@ -121,7 +108,7 @@ func realMain() int {
var resourcesChan <-chan resource.Resource
var errors <-chan error
if useStdin {
resourcesChan, errors = resource.FromStream(ctx, "stdin", os.Stdin)
resourcesChan, errors = resource.FromStream(ctx, "stdin", cfg.Stream.Input)
} else {
resourcesChan, errors = resource.FromFiles(ctx, cfg.Files, cfg.IgnoreFilenamePatterns)
}
Expand Down Expand Up @@ -171,12 +158,8 @@ func realMain() int {
o.Flush()

if !success {
return 1
return fmt.Errorf("failed to process results")
}

return 0
}

func main() {
os.Exit(realMain())
return nil
}
24 changes: 24 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda liked having the main function in cmd/kubeconform... I might move the validation to pkg/kubeconform.. still thinking about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw different ways to make it, but if that's your preference, maybe putting the main back in cmd/kubeconform and just moving validate to its own package under cmd/kubeconform/validate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another idea, we can just put Validate() under pkg/validator/validator.go.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not completely made up my mind on this, I'm likely to try out something that would allow you to work, but I might reserve myself the right to break that location/interface for a little while while I find a good place for it :)


import (
"fmt"
"os"

"github.com/yannh/kubeconform/cmd/kubeconform"
"github.com/yannh/kubeconform/pkg/config"
)

var version = "development"

func main() {
cfg, out, err := config.FromFlags(os.Args[0], os.Args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "failed parsing command line: %s\n", err.Error())
os.Exit(1)
}

if err = kubeconform.Validate(cfg, out); err != nil {
fmt.Fprintf(os.Stderr, "failed validating resources: %s - %s\n", err.Error(), out)
Copy link
Owner

@yannh yannh Apr 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is incorrect.. kubeconform currently returns 1 when some resources are not valid (but it didn't fail validating resources) - we need to differentiate errors processing from errors when validating..
It could return a bool saying if everything was valid or not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't change the logic here, I just used what's there but with an error instead of an init.
So any return 1 became a return error and any return 0 became a return nil.

os.Exit(1)
}
}
75 changes: 50 additions & 25 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,58 @@ import (
"bytes"
"flag"
"fmt"
"io"
"os"
"strings"
)

type Stream struct {
Input io.Reader
Output io.Writer
Error io.Writer
}

type Config struct {
Cache string
Debug bool
ExitOnError bool
Files []string
SchemaLocations []string
SkipTLS bool
SkipKinds map[string]struct{}
Cache string `yaml:"cache" json:"cache"`
Debug bool `yaml:"debug" json:"debug"`
ExitOnError bool `yaml:"exitOnError" json:"exitOnError"`
Files []string `yaml:"files" json:"files"`
Help bool `yaml:"help" json:"help"`
IgnoreFilenamePatterns []string `yaml:"ignoreFilenamePatterns" json:"ignoreFilenamePatterns"`
IgnoreMissingSchemas bool `yaml:"ignoreMissingSchemas" json:"ignoreMissingSchemas"`
KubernetesVersion string `yaml:"kubernetesVersion" json:"kubernetesVersion"`
NumberOfWorkers int `yaml:"numberOfWorkers" json:"numberOfWorkers"`
OutputFormat string `yaml:"output" json:"output"`
RejectKinds map[string]struct{}
OutputFormat string
KubernetesVersion string
NumberOfWorkers int
Summary bool
Strict bool
Verbose bool
IgnoreMissingSchemas bool
IgnoreFilenamePatterns []string
Help bool
Version bool
RejectKindsNG []string `yaml:"reject" json:"reject"`
SchemaLocations []string `yaml:"schemaLocations" json:"schemaLocations"`
SkipKinds map[string]struct{}
SkipKindsNG []string `yaml:"skip" json:"skip"`
SkipTLS bool `yaml:"insecureSkipTLSVerify" json:"insecureSkipTLSVerify"`
Strict bool `yaml:"strict" json:"strict"`
Summary bool `yaml:"summary" json:"summary"`
Verbose bool `yaml:"verbose" json:"verbose"`
Version bool `yaml:"version" json:"version"`
Stream *Stream
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if Stream fits better here, or as a separate argument to Validate().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in cmd/kubeconform if it is exposed (it should be a package, not main to be reusable).

I will think of a struct and it has something like:

type Kubeconform struct {
	Stream *Stream
	Config  *Config
}

which represents Kubeconform as an application.

}

// LoadNGConfig allows to introduce new config format/structure while keeping backward compatibility.
func (c *Config) LoadNGConfig() error {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest leaving this out for now, to make the PR easier to merge.

// SkipKindsNG.
loadKinds(c.SkipKinds, c.SkipKindsNG)

// RejectKindsNG.
loadKinds(c.RejectKinds, c.RejectKindsNG)

return nil
}

func loadKinds(dest map[string]struct{}, src []string) {
for _, kind := range src {
if len(kind) > 0 {
dest[kind] = struct{}{}
}
}
}

type arrayParam []string
Expand All @@ -40,15 +70,8 @@ func (ap *arrayParam) Set(value string) error {
}

func splitCSV(csvStr string) map[string]struct{} {
splitValues := strings.Split(csvStr, ",")
valuesMap := map[string]struct{}{}

for _, kind := range splitValues {
if len(kind) > 0 {
valuesMap[kind] = struct{}{}
}
}

loadKinds(valuesMap, strings.Split(csvStr, ","))
return valuesMap
}

Expand All @@ -62,7 +85,9 @@ func FromFlags(progName string, args []string) (Config, string, error) {

c := Config{}
c.Files = []string{}
c.Stream = &Stream{os.Stdin, os.Stdout, os.Stderr}

flags.SetOutput(c.Stream.Output)
flags.StringVar(&c.KubernetesVersion, "kubernetes-version", "master", "version of Kubernetes to validate against, e.g.: 1.18.0")
flags.Var(&schemaLocationsParam, "schema-location", "override schemas location search path (can be specified multiple times)")
flags.StringVar(&skipKindsCSV, "skip", "", "comma-separated list of kinds or GVKs to ignore")
Expand Down
6 changes: 2 additions & 4 deletions pkg/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package output

import (
"fmt"
"os"
"io"

"github.com/yannh/kubeconform/pkg/validator"
)
Expand All @@ -12,9 +12,7 @@ type Output interface {
Flush() error
}

func New(outputFormat string, printSummary, isStdin, verbose bool) (Output, error) {
w := os.Stdout

func New(w io.Writer, outputFormat string, printSummary, isStdin, verbose bool) (Output, error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I quite like the ability to override the streams here 👍

switch {
case outputFormat == "json":
return jsonOutput(w, printSummary, isStdin, verbose), nil
Expand Down