From 4a2690a199ced8dc92508859872bee2072af5aa7 Mon Sep 17 00:00:00 2001 From: Caibin Chen Date: Thu, 9 Mar 2023 09:30:12 -0800 Subject: [PATCH] Adding kustomize mage utility (#38) * Adding kustomize mage utility * fix lint * fix linting --- kustomize/kustomize.go | 141 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 kustomize/kustomize.go diff --git a/kustomize/kustomize.go b/kustomize/kustomize.go new file mode 100644 index 0000000..24a9336 --- /dev/null +++ b/kustomize/kustomize.go @@ -0,0 +1,141 @@ +package kustomize + +import ( + "bytes" + "context" + "fmt" + "os/exec" + "path/filepath" + "regexp" + + "github.com/cresta/magehelper/files" + "gopkg.in/yaml.v3" +) + +var YAMLSeparator = regexp.MustCompile("---\n") +var namespacelessKinds = map[string]bool{ + "ClusterRole": true, + "ClusterRoleBinding": true, + "StorageClass": true, + "PersistentVolume": true, +} + +// Manifest represents a Kubernetes manifest YAML file +type Manifest struct { + // YAML is the original YAML string + YAML string + // Parsed is the YAML string parsed as a map + Parsed map[string]any + // Name is the name of the manifest + Name string + // Namespace is the Kubernetes namespace this manifest is defined in. + Namespace string + // Kind is the kind of the manifest + Kind string +} + +// ShortName returns a short display name of the Manifest, including on ly the kind and name +func (m *Manifest) ShortName() string { + return fmt.Sprintf("[%s] %s", m.Kind, m.Name) +} + +// LongName returns a long display name for the Manifest, including namespace, kind and name +func (m *Manifest) LongName() string { + return m.Namespace + "-" + m.Kind + "-" + m.Name +} + +// Init runs the `kuztomize init` command on a specified directory, returning the kustomize generated manifest. +// +// The returned map maps the Manifest long name to the manifest. +func Init(ctx context.Context, path string) (map[string]Manifest, error) { + if !files.FileExists(filepath.Join(path, "kustomization.yaml")) { + _, err := shell(path, "kustomize", "init", ".", "--recursive", "--autodetect") + if err != nil { + return nil, fmt.Errorf("cannot init kustomize %s: %w", path, err) + } + } + output, err := shell(path, "kustomize", "build", "--load-restrictor=LoadRestrictionsNone") + if err != nil { + return nil, fmt.Errorf("failed to run kustomize at %s: %w", path, err) + } + yamls := YAMLSeparator.Split(output, -1) + documents := make(map[string]Manifest, len(yamls)) + for _, content := range yamls { + var parsed map[string]any + err := yaml.Unmarshal([]byte(content), &parsed) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal output:\n%s\n%w", content, err) + } + kind, ok := getString(parsed, true, "kind") + if !ok { + return nil, fmt.Errorf("missing kind in yaml document:\n%s", content) + } + name, ok := getString(parsed, true, "metadata", "name") + if !ok { + return nil, fmt.Errorf("missing metadata.name in yaml document:\n%s", content) + } + namespace := "" + if kind == "Namespace" { + namespace = name + } else { + namespace, ok = getString(parsed, true, "metadata", "namespace") + if !ok && !namespacelessKinds[kind] { + return nil, fmt.Errorf("missing metadata.namespace in yaml document:\n%s", content) + } + } + if namespace == "" { + namespace = "[no-namespace]" + } + m := Manifest{ + YAML: content, + Parsed: parsed, + Name: name, + Namespace: namespace, + Kind: kind, + } + documents[m.LongName()] = m + } + return documents, nil +} + +func shell(pwd string, cmdName string, args ...string) (stdout string, err error) { + cmd := exec.Command(cmdName, args...) + cmd.Dir = pwd + stderr := bytes.NewBuffer(nil) + cmd.Stderr = stderr + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("%w. Stderr: %s", err, stderr.String()) + } + return string(output), nil +} + +func getString(m map[string]any, verbose bool, keys ...string) (string, bool) { + valueAny, ok := getValue(m, verbose, keys...) + if !ok { + return "", ok + } + value, ok := valueAny.(string) + return value, ok +} + +func getValue(m map[string]any, verbose bool, keys ...string) (any, bool) { + var v any = m + for _, key := range keys { + m, ok := v.(map[string]any) + if !ok { + if verbose { + fmt.Printf("not a map: %v\n", v) + } + return "", false + } + v, ok = m[key] + if !ok { + if verbose { + fmt.Printf("%s not in %v\n", key, m) + } + return "", false + } + } + return v, true +}