Files
Jose Diaz-Gonzalez 496ede44b7 feat: add scheduler-k3s:preview subcommand
Adds a `scheduler-k3s:preview <app>` command that renders a unified diff
between the manifests currently stored in the live Helm release for an app
and the manifests that the next deploy would roll out. The diff is produced
entirely via the Helm Go SDK that scheduler-k3s already uses, with no
external `helm` binary or `helm-diff` plugin install required. The
underlying diff and parse logic is vendored from `databus23/helm-diff`
(Apache 2.0) into a new internal package, trimmed of three-way-merge,
release-ownership tracking, and the non-default output formatters.

The chart-construction half of `TriggerSchedulerDeploy` has been extracted
into a reusable `BuildAppChart` helper so deploy and preview share the
same chart materialization without duplicating template logic. Secret
values are redacted by default in preview output; `--show-secrets` and
`--show-secrets-decoded` flags match the upstream `helm diff` UX. Only the
main app helm release is compared in this iteration; auxiliary releases
for config vars, image-pull secrets, and TLS certificates are out of scope
and documented as such.
2026-06-02 20:28:31 -04:00

185 lines
4.5 KiB
Go

// Derived from github.com/databus23/helm-diff (Apache License 2.0).
// See LICENSE and NOTICE.md in this directory.
package helmdiff
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"strings"
"gopkg.in/yaml.v2"
)
const (
hookAnnotation = "helm.sh/hook"
resourcePolicyAnnotation = "helm.sh/resource-policy"
)
var yamlSeparator = []byte("\n---\n")
// MappingResult stores a single Kubernetes object rendered from a Helm chart.
type MappingResult struct {
Name string
Kind string
Content string
ResourcePolicy string
}
type metadata struct {
APIVersion string `yaml:"apiVersion"`
Kind string
Metadata struct {
Namespace string
Name string
Annotations map[string]string
}
}
func (m metadata) String() string {
apiBase := m.APIVersion
sp := strings.Split(apiBase, "/")
if len(sp) > 1 {
apiBase = strings.Join(sp[:len(sp)-1], "/")
}
name := m.Metadata.Name
if a := m.Metadata.Annotations; a != nil {
if baseName, ok := a["helm-diff/base-name"]; ok {
name = baseName
}
}
return fmt.Sprintf("%s, %s, %s (%s)", m.Metadata.Namespace, name, m.Kind, apiBase)
}
func scanYamlSpecs(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.Index(data, yamlSeparator); i >= 0 {
return i + len(yamlSeparator), data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
// Parse parses manifest bytes into a map of MappingResults keyed by
// "<namespace>, <name>, <kind> (<apiVersion>)".
func Parse(manifest []byte, defaultNamespace string, normalizeManifests bool, excludedHooks ...string) map[string]*MappingResult {
scanner := bufio.NewScanner(io.MultiReader(strings.NewReader("\n"), bytes.NewReader(manifest)))
scanner.Split(scanYamlSpecs)
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), 10485760)
result := make(map[string]*MappingResult)
for scanner.Scan() {
content := bytes.TrimSpace(scanner.Bytes())
if len(content) == 0 {
continue
}
parsed, err := parseContent(content, defaultNamespace, normalizeManifests, excludedHooks...)
if err != nil {
log.Fatalf("%v", err)
}
for _, p := range parsed {
name := p.Name
if _, ok := result[name]; ok {
log.Printf("Error: Found duplicate key %#v in manifest", name)
} else {
result[name] = p
}
}
}
if err := scanner.Err(); err != nil {
log.Fatalf("Error reading input: %s", err)
}
return result
}
func parseContent(content []byte, defaultNamespace string, normalizeManifests bool, excludedHooks ...string) ([]*MappingResult, error) {
var parsedMetadata metadata
if err := yaml.Unmarshal(content, &parsedMetadata); err != nil {
log.Fatalf("YAML unmarshal error: %s\nCan't unmarshal %s", err, content)
}
if parsedMetadata.APIVersion == "" && parsedMetadata.Kind == "" {
return nil, nil
}
if strings.HasSuffix(parsedMetadata.Kind, "List") {
type ListV1 struct {
Items []yaml.MapSlice `yaml:"items"`
}
var list ListV1
if err := yaml.Unmarshal(content, &list); err != nil {
log.Fatalf("YAML unmarshal error: %s\nCan't unmarshal %s", err, content)
}
var result []*MappingResult
for _, item := range list.Items {
subcontent, err := yaml.Marshal(item)
if err != nil {
log.Printf("YAML marshal error: %s\nCan't marshal %v", err, item)
}
subs, err := parseContent(subcontent, defaultNamespace, normalizeManifests, excludedHooks...)
if err != nil {
return nil, fmt.Errorf("parsing YAML list item: %w", err)
}
result = append(result, subs...)
}
return result, nil
}
if normalizeManifests {
var object map[interface{}]interface{}
if err := yaml.Unmarshal(content, &object); err != nil {
log.Fatalf("YAML unmarshal error: %s\nCan't unmarshal %s", err, content)
}
normalizedContent, err := yaml.Marshal(object)
if err != nil {
log.Fatalf("YAML marshal error: %s\nCan't marshal %v", err, object)
}
content = normalizedContent
}
if isHook(parsedMetadata, excludedHooks...) {
return nil, nil
}
if parsedMetadata.Metadata.Namespace == "" {
parsedMetadata.Metadata.Namespace = defaultNamespace
}
name := parsedMetadata.String()
return []*MappingResult{
{
Name: name,
Kind: parsedMetadata.Kind,
Content: string(content),
ResourcePolicy: parsedMetadata.Metadata.Annotations[resourcePolicyAnnotation],
},
}, nil
}
func isHook(metadata metadata, hooks ...string) bool {
for _, hook := range hooks {
if metadata.Metadata.Annotations[hookAnnotation] == hook {
return true
}
}
return false
}