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

Edit strict mode to throw errors on additional properties, not found in the schema #234

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
1 change: 1 addition & 0 deletions cmd/kubeconform/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func kubeconform(cfg config.Config) int {
RejectKinds: cfg.RejectKinds,
KubernetesVersion: cfg.KubernetesVersion,
Strict: cfg.Strict,
StrictExceptions: cfg.StrictExceptions,
IgnoreMissingSchemas: cfg.IgnoreMissingSchemas,
})
if err != nil {
Expand Down
5 changes: 4 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Config struct {
SkipKinds map[string]struct{} `yaml:"skip" json:"skip"`
SkipTLS bool `yaml:"insecureSkipTLSVerify" json:"insecureSkipTLSVerify"`
Strict bool `yaml:"strict" json:"strict"`
StrictExceptions map[string]struct{} `yaml:"strictExceptions" json:"strictExceptions"`
Summary bool `yaml:"summary" json:"summary"`
Verbose bool `yaml:"verbose" json:"verbose"`
Version bool `yaml:"version" json:"version"`
Expand Down Expand Up @@ -55,7 +56,7 @@ func splitCSV(csvStr string) map[string]struct{} {
// FromFlags retrieves kubeconform's runtime configuration from the command-line parameters
func FromFlags(progName string, args []string) (Config, string, error) {
var schemaLocationsParam, ignoreFilenamePatterns arrayParam
var skipKindsCSV, rejectKindsCSV string
var skipKindsCSV, rejectKindsCSV, strictExceptionsCSV string
flags := flag.NewFlagSet(progName, flag.ContinueOnError)
var buf bytes.Buffer
flags.SetOutput(&buf)
Expand All @@ -74,6 +75,7 @@ func FromFlags(progName string, args []string) (Config, string, error) {
flags.BoolVar(&c.Summary, "summary", false, "print a summary at the end (ignored for junit output)")
flags.IntVar(&c.NumberOfWorkers, "n", 4, "number of goroutines to run concurrently")
flags.BoolVar(&c.Strict, "strict", false, "disallow additional properties not in schema or duplicated keys")
flags.StringVar(&strictExceptionsCSV, "strict-exception", "", "comma-separated list of yaml paths to ignore when strict is enabled")
flags.StringVar(&c.OutputFormat, "output", "text", "output format - json, junit, pretty, tap, text")
flags.BoolVar(&c.Verbose, "verbose", false, "print results for all resources (ignored for tap and junit output)")
flags.BoolVar(&c.SkipTLS, "insecure-skip-tls-verify", false, "disable verification of the server's SSL certificate. This will make your HTTPS connections insecure")
Expand All @@ -89,6 +91,7 @@ func FromFlags(progName string, args []string) (Config, string, error) {

c.SkipKinds = splitCSV(skipKindsCSV)
c.RejectKinds = splitCSV(rejectKindsCSV)
c.StrictExceptions = splitCSV(strictExceptionsCSV)
c.IgnoreFilenamePatterns = ignoreFilenamePatterns
c.SchemaLocations = schemaLocationsParam
c.Files = flags.Args()
Expand Down
66 changes: 58 additions & 8 deletions pkg/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,18 @@ type Opts struct {
RejectKinds map[string]struct{} // List of resource Kinds to reject
KubernetesVersion string // Kubernetes Version - has to match one in https://github.com/instrumenta/kubernetes-json-schema
Strict bool // thros an error if resources contain undocumented fields
StrictExceptions map[string]struct{} // list of field paths (e.g. spec.template.metadata) to ignore when strict is enabled
IgnoreMissingSchemas bool // skip a resource if no schema for that resource can be found
}

// New returns a new Validator
func New(schemaLocations []string, opts Opts) (Validator, error) {
// Default to our kubernetes-json-schema fork
// raw.githubusercontent.com is frontend by Fastly and very fast
defaultLocationsUsed := false
if len(schemaLocations) == 0 {
schemaLocations = []string{"https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json"}
defaultLocationsUsed = true
}

registries := []registry.Registry{}
Expand All @@ -91,20 +94,62 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
if opts.RejectKinds == nil {
opts.RejectKinds = map[string]struct{}{}
}
if opts.StrictExceptions == nil {
opts.StrictExceptions = map[string]struct{}{}
}
if len(opts.StrictExceptions) == 0 {
// If no strict exceptions are specified, we add the metadata field
// as it is mostly not autogenerated in the OpenAPI schemas generated from CRDs
opts.StrictExceptions["metadata"] = struct{}{}
}

return &v{
opts: opts,
schemaDownload: downloadSchema,
schemaCache: cache.NewInMemoryCache(),
regs: registries,
opts: opts,
schemaDownload: downloadSchema,
schemaCache: cache.NewInMemoryCache(),
regs: registries,
defaultLocationsUsed: defaultLocationsUsed,
}, nil
}

func (val *v) setStrictSchema(schema *jsonschema.Schema, prefixPath string) {
if _, ok := val.opts.StrictExceptions[prefixPath]; ok {
return
}
if schema.AdditionalProperties == nil {
schema.AdditionalProperties = false
}
if schema.Items != nil {
casted, ok := schema.Items.(*jsonschema.Schema)
if ok {
fullPath := prefixPath + ".0"
val.setStrictSchema(casted, fullPath)
} else {
casted, ok := schema.Items.([]*jsonschema.Schema)
if ok {
for i, s := range casted {
fullPath := prefixPath + "." + fmt.Sprintf("%d", i)
val.setStrictSchema(s, fullPath)
}
}
}
}

for name, s := range schema.Properties {
fullPath := prefixPath + "." + name
if fullPath[0] == '.' {
fullPath = fullPath[1:]
}
val.setStrictSchema(s, fullPath)
}
}

type v struct {
opts Opts
schemaCache cache.Cache
schemaDownload func(registries []registry.Registry, kind, version, k8sVersion string) (*jsonschema.Schema, error)
regs []registry.Registry
opts Opts
schemaCache cache.Cache
schemaDownload func(registries []registry.Registry, kind, version, k8sVersion string) (*jsonschema.Schema, error)
regs []registry.Registry
defaultLocationsUsed bool
}

// ValidateResource validates a single resource. This allows to validate
Expand Down Expand Up @@ -191,6 +236,11 @@ func (val *v) ValidateResource(res resource.Resource) Result {
return Result{Resource: res, Err: fmt.Errorf("could not find schema for %s", sig.Kind), Status: Error}
}

// If strict mode is enabled, we set additionalProperties to false
// if not explicitly set in the schema
if val.opts.Strict && !val.defaultLocationsUsed {
val.setStrictSchema(schema, "")
}
err = schema.Validate(r)
if err != nil {
validationErrors := []ValidationError{}
Expand Down