From 95185a88ad7fb4595330650d3097defbc16c7358 Mon Sep 17 00:00:00 2001 From: Edwin Smith Date: Tue, 8 Oct 2024 11:00:21 -0500 Subject: [PATCH] fix: inject defaults for missing properties when available --- cmd/kubeconform/main.go | 104 ++++++++++++++++++++++++++++++++++++++-- pkg/config/config.go | 5 ++ 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/cmd/kubeconform/main.go b/cmd/kubeconform/main.go index d1ae4b3..5bb8c33 100644 --- a/cmd/kubeconform/main.go +++ b/cmd/kubeconform/main.go @@ -1,7 +1,9 @@ package main import ( + "bytes" "context" + "encoding/json" "fmt" "log" "os" @@ -9,6 +11,8 @@ import ( "runtime/pprof" "sync" + "sigs.k8s.io/yaml" + "github.com/yannh/kubeconform/pkg/config" "github.com/yannh/kubeconform/pkg/output" "github.com/yannh/kubeconform/pkg/resource" @@ -17,6 +21,69 @@ import ( var version = "development" +// New function to load the manifest and schema for injecting defaults +func loadManifestAndSchema(manifestPath, schemaPath string) (map[string]interface{}, map[string]interface{}, error) { + manifestFile, err := os.ReadFile(manifestPath) + if err != nil { + return nil, nil, fmt.Errorf("error reading manifest file: %v", err) + } + var manifest map[string]interface{} + if err := yaml.Unmarshal(manifestFile, &manifest); err != nil { + return nil, nil, fmt.Errorf("error parsing manifest YAML: %v", err) + } + + schemaFile, err := os.ReadFile(schemaPath) + if err != nil { + return nil, nil, fmt.Errorf("error reading schema file: %v", err) + } + var schema map[string]interface{} + if err := json.Unmarshal(schemaFile, &schema); err != nil { + return nil, nil, fmt.Errorf("error parsing schema JSON: %v", err) + } + + return manifest, schema, nil +} + +// New function to inject defaults recursively +func injectDefaultsRecursively(schema map[string]interface{}, manifest map[string]interface{}) { + properties, propertiesExist := schema["properties"].(map[string]interface{}) + if !propertiesExist { + return + } + + for key, subschema := range properties { + if _, keyExists := manifest[key]; !keyExists { + subSchemaMap, ok := subschema.(map[string]interface{}) + if ok { + if defaultValue, hasDefault := subSchemaMap["default"]; hasDefault { + manifest[key] = defaultValue + fmt.Printf("Injected default for %s: %v\\n", key, defaultValue) + } + } + } else { + if subSchemaMap, ok := subschema.(map[string]interface{}); ok { + if subSchemaType, typeExists := subSchemaMap["type"].(string); typeExists { + if subSchemaType == "object" { + if nestedManifest, isMap := manifest[key].(map[string]interface{}); isMap { + injectDefaultsRecursively(subSchemaMap, nestedManifest) + } + } else if subSchemaType == "array" { + if arrayItems, hasItems := subSchemaMap["items"].(map[string]interface{}); hasItems { + if manifestArray, isArray := manifest[key].([]interface{}); isArray { + for _, item := range manifestArray { + if itemMap, isMap := item.(map[string]interface{}); isMap { + injectDefaultsRecursively(arrayItems, itemMap) + } + } + } + } + } + } + } + } + } +} + func processResults(cancel context.CancelFunc, o output.Output, validationResults <-chan validator.Result, exitOnError bool) <-chan bool { success := true result := make(chan bool) @@ -28,7 +95,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") + fmt.Fprint(os.Stderr, "failed writing log\\n") } } if !success && exitOnError { @@ -99,10 +166,36 @@ func kubeconform(cfg config.Config) int { var resourcesChan <-chan resource.Resource var errors <-chan error - if useStdin { - resourcesChan, errors = resource.FromStream(ctx, "stdin", os.Stdin) + + // Use the manifest with injected defaults for validation + if cfg.InjectMissingDefaults { + manifest, schema, err := loadManifestAndSchema(cfg.Files[0], cfg.SchemaLocations[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "error loading manifest or schema: %s\\n", err) + os.Exit(1) + } + + // Inject defaults into the manifest + injectDefaultsRecursively(schema, manifest) + + // Convert the modified manifest back to YAML for validation + updatedManifest, err := yaml.Marshal(manifest) + if err != nil { + fmt.Fprintf(os.Stderr, "error converting updated manifest to YAML: %s\\n", err) + os.Exit(1) + } + + // Use a buffer as io.Reader to pass updated manifest + manifestReader := bytes.NewReader(updatedManifest) + + // Use the updated manifest for validation + resourcesChan, errors = resource.FromStream(ctx, "updatedManifest", manifestReader) } else { - resourcesChan, errors = resource.FromFiles(ctx, cfg.Files, cfg.IgnoreFilenamePatterns) + if useStdin { + resourcesChan, errors = resource.FromStream(ctx, "stdin", os.Stdin) + } else { + resourcesChan, errors = resource.FromFiles(ctx, cfg.Files, cfg.IgnoreFilenamePatterns) + } } // Process discovered resources across multiple workers @@ -175,9 +268,10 @@ func main() { } if err != nil { - fmt.Fprintf(os.Stderr, "failed parsing command line: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "failed parsing command line: %s\\n", err.Error()) os.Exit(1) } + // Inject defaults if the flag is enabled and validate the updated manifest os.Exit(kubeconform(cfg)) } diff --git a/pkg/config/config.go b/pkg/config/config.go index b7df0f4..c48a27b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,6 +16,7 @@ type Config struct { Help bool `yaml:"help" json:"help"` IgnoreFilenamePatterns []string `yaml:"ignoreFilenamePatterns" json:"ignoreFilenamePatterns"` IgnoreMissingSchemas bool `yaml:"ignoreMissingSchemas" json:"ignoreMissingSchemas"` + InjectMissingDefaults bool `yaml:"injectMissingDefaults" json:"injectMissingDefaults"` // New field added KubernetesVersion k8sVersionValue `yaml:"kubernetesVersion" json:"kubernetesVersion"` NumberOfWorkers int `yaml:"numberOfWorkers" json:"numberOfWorkers"` OutputFormat string `yaml:"output" json:"output"` @@ -100,6 +101,10 @@ func FromFlags(progName string, args []string) (Config, string, error) { flags.StringVar(&c.Cache, "cache", "", "cache schemas downloaded via HTTP to this folder") flags.BoolVar(&c.Help, "h", false, "show help information") flags.BoolVar(&c.Version, "v", false, "show version information") + + // New flag added for injecting missing defaults + flags.BoolVar(&c.InjectMissingDefaults, "inject-missing-defaults", false, "Inject missing required fields with defaults from the schema") + flags.Usage = func() { fmt.Fprintf(&buf, "Usage: %s [OPTION]... [FILE OR FOLDER]...\n", progName) flags.PrintDefaults()