Skip to content

Commit

Permalink
Support auto-detecting pairs
Browse files Browse the repository at this point in the history
  • Loading branch information
tiulpin committed Feb 4, 2025
1 parent 6e394ad commit 247751c
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 22 deletions.
154 changes: 132 additions & 22 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"reflect"
"sort"
"strings"

"helm.sh/helm/v3/pkg/action"
Expand Down Expand Up @@ -60,8 +61,8 @@ func validateChartValues(defaultValues, providedValues map[string]interface{}, p

defaultValue, exists := defaultValues[key]
if !exists {
fmt.Printf("❌ Unexpected key: '%s' is not defined in chart defaults\n", fullKey)
*issuesFound = true
//fmt.Printf("❌ Unexpected key: '%s' is not defined in chart defaults\n", fullKey)
//*issuesFound = true
continue
}

Expand Down Expand Up @@ -118,6 +119,62 @@ func findChart(chartPath string) (string, error) {
return "", fmt.Errorf("chart not found: %s", chartPath)
}

// valuePair represents a candidate pair of values files:
// one overrides file and one service file (e.g. web_service.yaml).
type valuePair struct {
override string
service string
}

// detectPairs searches starting at baseDir (for example, the current working directory)
// for every file named "<chartName>.yaml". For each such service file, it traverses upward
// (but not past baseDir) to locate the nearest overrides.yaml. If found, the pair is recorded.
func detectPairs(baseDir, chartName string) ([]valuePair, error) {
var pairs []valuePair
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Look for files named "<chartName>.yaml" (e.g. "web_service.yaml")
if !info.IsDir() && filepath.Base(path) == chartName+".yaml" {
currentDir := filepath.Dir(path)
var overridePath string
// Traverse upward until reaching the baseDir.
for {
candidate := filepath.Join(currentDir, "overrides.yaml")
if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
overridePath = candidate
break
}
if currentDir == baseDir {
break
}
parent := filepath.Dir(currentDir)
if parent == currentDir {
break
}
currentDir = parent
}
if overridePath != "" {
pairs = append(pairs, valuePair{override: overridePath, service: path})
}
}
return nil
})
if err != nil {
return nil, err
}

// Sort pairs for consistent output.
sort.Slice(pairs, func(i, j int) bool {
if pairs[i].override == pairs[j].override {
return pairs[i].service < pairs[j].service
}
return pairs[i].override < pairs[j].override
})
return pairs, nil
}

func main() {
var ignoreList IgnoreList
var valuesFiles ValueFiles
Expand All @@ -127,11 +184,12 @@ func main() {
flag.Parse()

args := flag.Args()
if len(args) < 1 || len(valuesFiles) == 0 {
fmt.Printf("Usage: helm kc [--ignore field1,field2,...] <chart> -f <values-file> [-f <additional-values-file> ...]\n")
if len(args) < 1 {
fmt.Printf("Usage: helm kc [--ignore field1,field2,...] <chart> [-f <values-file> ...]\n")
fmt.Printf("\nExamples:\n")
fmt.Printf(" helm kc ./mychart -f values.yaml\n")
fmt.Printf(" helm kc ./mychart -f overrides.yaml -f infra/web_service.yaml\n")
fmt.Printf("If no -f is provided, the plugin auto-detects valid pairs from the environment tree.\n")
os.Exit(1)
}

Expand Down Expand Up @@ -161,32 +219,84 @@ func main() {
os.Exit(1)
}

valueOpts := &values.Options{
ValueFiles: valuesFiles,
defaultValues := chart.Values

// If the user provided explicit -f values, merge and validate them as before.
if len(valuesFiles) > 0 {
valueOpts := &values.Options{
ValueFiles: valuesFiles,
}
providedValues, err := valueOpts.MergeValues(nil)
if err != nil {
fmt.Printf("Failed to load values: %v\n", err)
os.Exit(1)
}
fmt.Printf("\nValidating Helm chart values:\n")
fmt.Printf("==============================\n")
fmt.Printf("Chart: %s\n", chartDir)
fmt.Printf("Values files: %s\n", valuesFiles.String())
if len(ignoreList) > 0 {
fmt.Printf("Ignoring fields: %s\n", ignoreList.String())
}
fmt.Printf("\nStarting validation...\n\n")

issuesFound := false
validateChartValues(defaultValues, providedValues, "", &issuesFound, ignoreList)
if !issuesFound {
fmt.Printf("\nValidation completed: No issues found.\n")
} else {
fmt.Printf("\nValidation completed: Issues were found.\n")
os.Exit(1)
}
return
}
providedValues, err := valueOpts.MergeValues(nil)

// No -f flags provided: auto-detect valid pairs.
// Use the current working directory as the base for environment search.
envDir, err := os.Getwd()
if err != nil {
fmt.Printf("Failed to load values: %v\n", err)
fmt.Printf("Error determining current directory: %v\n", err)
os.Exit(1)
}
defaultValues := chart.Values

fmt.Printf("\nValidating Helm chart values:\n")
fmt.Printf("==============================\n")
fmt.Printf("Chart: %s\n", chartDir)
fmt.Printf("Values files: %s\n", valuesFiles.String())
if len(ignoreList) > 0 {
fmt.Printf("Ignoring fields: %s\n", ignoreList.String())
chartName := filepath.Base(chartDir)
pairs, err := detectPairs(envDir, chartName)
if err != nil {
fmt.Printf("Error auto-detecting value pairs: %v\n", err)
os.Exit(1)
}
if len(pairs) == 0 {
fmt.Printf("No valid pairs of values files (overrides.yaml + %s.yaml) found in base directory: %s\n", chartName, envDir)
os.Exit(1)
}
fmt.Printf("\nStarting validation...\n\n")

issuesFound := false
validateChartValues(defaultValues, providedValues, "", &issuesFound, ignoreList)
overallIssues := false
for _, p := range pairs {
fmt.Printf("\nValidating pair:\n Overrides: %s\n %s: %s\n", p.override, chartName, p.service)
valueOpts := &values.Options{
// The order matters: the overrides file is applied first.
ValueFiles: []string{p.override, p.service},
}
providedValues, err := valueOpts.MergeValues(nil)
if err != nil {
fmt.Printf("Failed to load values for pair (%s, %s): %v\n", p.override, p.service, err)
overallIssues = true
continue
}

if !issuesFound {
fmt.Printf("\nValidation completed: No issues found.\n")
} else {
fmt.Printf("\nValidation completed: Issues were found.\n")
issuesFound := false
validateChartValues(defaultValues, providedValues, "", &issuesFound, ignoreList)
if issuesFound {
fmt.Printf("Issues found for pair (%s, %s)\n", p.override, p.service)
overallIssues = true
} else {
fmt.Printf("No issues found for pair (%s, %s)\n", p.override, p.service)
}
}

if overallIssues {
os.Exit(1)
} else {
fmt.Printf("\nValidation completed: No issues found in any pair.\n")
}
}
104 changes: 104 additions & 0 deletions cmd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"os"
"path/filepath"
"sort"
"testing"

"helm.sh/helm/v3/pkg/cli/values"
Expand Down Expand Up @@ -147,3 +149,105 @@ nested:
t.Errorf("expected nested.key3 to be 'value3', got %v", nested["key3"])
}
}

// TestDetectPairs verifies that detectPairs correctly finds valid pairs of values files.
// For each service file (named "<chartName>.yaml"), detectPairs should locate the nearest
// overrides.yaml (traversing upward until the base directory is reached).
func TestDetectPairs(t *testing.T) {
// Create a temporary base directory to simulate the environment tree.
baseDir, err := os.MkdirTemp("", "detectpairs")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(baseDir)

chartName := "web_service"

// --- Pair 1 ---
// Create a directory "pair1" with an overrides.yaml and a service file.
pair1Dir := filepath.Join(baseDir, "pair1")
if err := os.MkdirAll(pair1Dir, 0755); err != nil {
t.Fatalf("failed to create pair1 dir: %v", err)
}
override1 := filepath.Join(pair1Dir, "overrides.yaml")
if err := os.WriteFile(override1, []byte("key: override1"), 0644); err != nil {
t.Fatalf("failed to write override1: %v", err)
}
pair1ServiceDir := filepath.Join(pair1Dir, "services")
if err := os.MkdirAll(pair1ServiceDir, 0755); err != nil {
t.Fatalf("failed to create pair1 service dir: %v", err)
}
service1 := filepath.Join(pair1ServiceDir, "web_service.yaml")
if err := os.WriteFile(service1, []byte("key: service1"), 0644); err != nil {
t.Fatalf("failed to write service1: %v", err)
}

// --- Pair 2 ---
// Create a directory "pair2/sub" with an overrides.yaml and a service file.
pair2Dir := filepath.Join(baseDir, "pair2", "sub")
if err := os.MkdirAll(pair2Dir, 0755); err != nil {
t.Fatalf("failed to create pair2 dir: %v", err)
}
override2 := filepath.Join(pair2Dir, "overrides.yaml")
if err := os.WriteFile(override2, []byte("key: override2"), 0644); err != nil {
t.Fatalf("failed to write override2: %v", err)
}
pair2ServiceDir := filepath.Join(pair2Dir, "services")
if err := os.MkdirAll(pair2ServiceDir, 0755); err != nil {
t.Fatalf("failed to create pair2 service dir: %v", err)
}
service2 := filepath.Join(pair2ServiceDir, "web_service.yaml")
if err := os.WriteFile(service2, []byte("key: service2"), 0644); err != nil {
t.Fatalf("failed to write service2: %v", err)
}

// --- No Pair ---
// Create a directory "nopair" with a service file but no overrides.yaml in its ancestry.
noPairDir := filepath.Join(baseDir, "nopair", "services")
if err := os.MkdirAll(noPairDir, 0755); err != nil {
t.Fatalf("failed to create nopair dir: %v", err)
}
noPairService := filepath.Join(noPairDir, "web_service.yaml")
if err := os.WriteFile(noPairService, []byte("key: nopair"), 0644); err != nil {
t.Fatalf("failed to write noPairService: %v", err)
}

// Also create a file with a different name that should be ignored.
ignoreFile := filepath.Join(pair1ServiceDir, "not_web_service.yaml")
if err := os.WriteFile(ignoreFile, []byte("key: ignore"), 0644); err != nil {
t.Fatalf("failed to write ignoreFile: %v", err)
}

// Call detectPairs using the temporary baseDir and the chart name.
pairs, err := detectPairs(baseDir, chartName)
if err != nil {
t.Fatalf("detectPairs returned error: %v", err)
}

// We expect exactly 2 pairs (from pair1 and pair2).
if len(pairs) != 2 {
t.Fatalf("expected 2 pairs, got %d", len(pairs))
}

// Sort the pairs by service path for predictable order.
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].service < pairs[j].service
})

expectedPairs := []struct {
override string
service string
}{
{override: override1, service: service1},
{override: override2, service: service2},
}

for i, ep := range expectedPairs {
if pairs[i].override != ep.override {
t.Errorf("pair %d: expected override %q, got %q", i, ep.override, pairs[i].override)
}
if pairs[i].service != ep.service {
t.Errorf("pair %d: expected service %q, got %q", i, ep.service, pairs[i].service)
}
}
}

0 comments on commit 247751c

Please sign in to comment.