helmfile/pkg/state/chart_dependencies_rewrite_...

648 lines
16 KiB
Go

package state
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"go.uber.org/zap"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/yaml"
)
func TestRewriteChartDependencies(t *testing.T) {
tests := []struct {
name string
chartYaml string
expectModified bool
expectError bool
validate func(t *testing.T, modifiedChartYaml string)
}{
{
name: "no Chart.yaml exists",
chartYaml: "",
expectModified: false,
expectError: false,
},
{
name: "no dependencies",
chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
`,
expectModified: false,
expectError: false,
},
{
name: "absolute file:// dependency - not modified",
chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
- name: dep1
repository: file:///absolute/path/to/chart
version: 1.0.0
`,
expectModified: false,
expectError: false,
validate: func(t *testing.T, modifiedChartYaml string) {
if !strings.Contains(modifiedChartYaml, "file:///absolute/path/to/chart") {
t.Errorf("absolute path should not be modified")
}
},
},
{
name: "relative file:// dependency - should be modified",
chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
- name: dep1
repository: file://../relative-chart
version: 1.0.0
`,
expectModified: true,
expectError: false,
validate: func(t *testing.T, modifiedChartYaml string) {
if strings.Contains(modifiedChartYaml, "file://../relative-chart") {
t.Errorf("relative path should have been converted to absolute")
}
if !strings.Contains(modifiedChartYaml, "file://") {
t.Errorf("should still have file:// prefix")
}
},
},
{
name: "mixed dependencies - only relative file:// modified",
chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
- name: dep1
repository: https://charts.example.com
version: 1.0.0
- name: dep2
repository: file://../relative-chart
version: 2.0.0
- name: dep3
repository: file:///absolute/chart
version: 3.0.0
- name: dep4
repository: oci://registry.example.com/charts/mychart
version: 4.0.0
`,
expectModified: true,
expectError: false,
validate: func(t *testing.T, modifiedChartYaml string) {
if !strings.Contains(modifiedChartYaml, "https://charts.example.com") {
t.Errorf("https repository should not be modified")
}
if strings.Contains(modifiedChartYaml, "file://../relative-chart") {
t.Errorf("relative file:// path should have been converted")
}
if !strings.Contains(modifiedChartYaml, "file:///absolute/chart") {
t.Errorf("absolute file:// path should not be modified")
}
if !strings.Contains(modifiedChartYaml, "oci://registry.example.com") {
t.Errorf("oci repository should not be modified")
}
},
},
{
name: "multiple relative dependencies",
chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
- name: dep1
repository: file://../chart1
version: 1.0.0
- name: dep2
repository: file://./chart2
version: 2.0.0
- name: dep3
repository: file://../../../chart3
version: 3.0.0
`,
expectModified: true,
expectError: false,
validate: func(t *testing.T, modifiedChartYaml string) {
if strings.Contains(modifiedChartYaml, "file://../chart1") ||
strings.Contains(modifiedChartYaml, "file://./chart2") ||
strings.Contains(modifiedChartYaml, "file://../../../chart3") {
t.Errorf("all relative paths should have been converted")
}
},
},
{
name: "extra fields",
chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
- name: dep1
repository: https://charts.example.com
version: 1.0.0
condition: dep.install
import-values:
- child: persistence
parent: global.persistence
extra-field2: bbb
extra-field: aaa
`,
expectModified: false,
expectError: false,
validate: func(t *testing.T, modifiedChartYaml string) {
requiredFields := []string{
"condition: dep.install",
"import-values:",
"child: persistence",
"parent: global.persistence",
"extra-field2: bbb",
"extra-field: aaa",
}
for _, field := range requiredFields {
if !strings.Contains(modifiedChartYaml, field) {
t.Errorf("field %q should be preserved", field)
}
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "helmfile-test-")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
if tt.chartYaml != "" {
chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
if err := os.WriteFile(chartYamlPath, []byte(tt.chartYaml), 0644); err != nil {
t.Fatalf("failed to write Chart.yaml: %v", err)
}
}
logger := zap.NewNop().Sugar()
st := &HelmState{
logger: logger,
fs: filesystem.DefaultFileSystem(),
}
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
}
return
}
if tt.expectModified {
if rewrittenPath == tempDir {
t.Errorf("expected rewrittenPath != tempDir when modifications are needed, got same path")
}
} else {
if rewrittenPath != tempDir {
t.Errorf("expected rewrittenPath == tempDir when no modifications are needed, got %q", rewrittenPath)
}
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer cleanup()
if tt.chartYaml == "" {
return
}
modifiedChartBytes, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
if err != nil {
t.Fatalf("failed to read Chart.yaml: %v", err)
}
modifiedChartYaml := string(modifiedChartBytes)
if tt.validate != nil {
tt.validate(t, modifiedChartYaml)
}
})
}
}
func TestRewriteChartDependencies_OriginalNotModified(t *testing.T) {
tempDir, err := os.MkdirTemp("", "helmfile-test-")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
originalChart := `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
- name: dep1
repository: file://../relative-chart
version: 1.0.0
`
chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
if err := os.WriteFile(chartYamlPath, []byte(originalChart), 0644); err != nil {
t.Fatalf("failed to write Chart.yaml: %v", err)
}
logger := zap.NewNop().Sugar()
st := &HelmState{
logger: logger,
fs: filesystem.DefaultFileSystem(),
}
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
t.Cleanup(func() {
if rewrittenPath == tempDir {
return
}
cleanup()
if _, statErr := os.Stat(rewrittenPath); !os.IsNotExist(statErr) {
t.Errorf("expected rewritten chart path %q to be removed after cleanup", rewrittenPath)
}
})
if rewrittenPath == tempDir {
t.Errorf("expected a different path when modifications are needed, got same path")
}
modifiedContent, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
if err != nil {
t.Fatalf("failed to read modified Chart.yaml: %v", err)
}
if string(modifiedContent) == originalChart {
t.Errorf("Chart.yaml in the copy should have been modified")
}
originalContent, err := os.ReadFile(chartYamlPath)
if err != nil {
t.Fatalf("failed to read original Chart.yaml: %v", err)
}
if string(originalContent) != originalChart {
t.Errorf("original Chart.yaml should not have been modified\nexpected:\n%s\ngot:\n%s",
originalChart, string(originalContent))
}
}
func TestRewriteChartDependencies_PreservesOtherFields(t *testing.T) {
tempDir, err := os.MkdirTemp("", "helmfile-test-")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
chartYaml := `apiVersion: v2
name: test-chart
version: 1.0.0
description: A test chart
keywords:
- test
- example
maintainers:
- name: Test User
email: test@example.com
dependencies:
- name: dep1
repository: file://../relative-chart
version: 1.0.0
condition: test
`
chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
if err := os.WriteFile(chartYamlPath, []byte(chartYaml), 0644); err != nil {
t.Fatalf("failed to write Chart.yaml: %v", err)
}
logger := zap.NewNop().Sugar()
st := &HelmState{
logger: logger,
fs: filesystem.DefaultFileSystem(),
}
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
t.Cleanup(func() {
if rewrittenPath == tempDir {
return
}
cleanup()
if _, statErr := os.Stat(rewrittenPath); !os.IsNotExist(statErr) {
t.Errorf("expected rewritten chart path %q to be removed after cleanup", rewrittenPath)
}
})
modifiedContent, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
if err != nil {
t.Fatalf("failed to read modified Chart.yaml: %v", err)
}
content := string(modifiedContent)
requiredFields := []string{
"apiVersion: v2",
"name: test-chart",
"version: 1.0.0",
"description: A test chart",
"keywords:",
"maintainers:",
"condition:",
}
for _, field := range requiredFields {
if !strings.Contains(content, field) {
t.Errorf("field %q should be preserved", field)
}
}
if strings.Contains(content, "file://../relative-chart") {
t.Errorf("relative path should have been converted to absolute")
}
originalContent, err := os.ReadFile(chartYamlPath)
if err != nil {
t.Fatalf("failed to read original Chart.yaml: %v", err)
}
if string(originalContent) != chartYaml {
t.Errorf("original Chart.yaml should not have been modified")
}
}
func TestRewriteChartDependencies_ErrorHandling(t *testing.T) {
tests := []struct {
name string
setupFunc func(tempDir string) error
expectError bool
}{
{
name: "invalid yaml in Chart.yaml",
setupFunc: func(tempDir string) error {
invalidYaml := `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
- name: dep1
repository: file://../chart
version: 1.0.0
invalid yaml here!
`
return os.WriteFile(filepath.Join(tempDir, "Chart.yaml"), []byte(invalidYaml), 0644)
},
expectError: true,
},
{
name: "unreadable Chart.yaml",
setupFunc: func(tempDir string) error {
chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
if err := os.WriteFile(chartYamlPath, []byte("test"), 0000); err != nil {
return err
}
return nil
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "helmfile-test-")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
if err := tt.setupFunc(tempDir); err != nil {
t.Fatalf("setup failed: %v", err)
}
logger := zap.NewNop().Sugar()
st := &HelmState{
logger: logger,
fs: filesystem.DefaultFileSystem(),
}
_, _, err = st.rewriteChartDependencies(tempDir)
if tt.expectError && err == nil {
t.Errorf("expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}
func TestRewriteChartDependencies_WindowsStylePath(t *testing.T) {
tempDir, err := os.MkdirTemp("", "helmfile-test-")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
chartYaml := `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
- name: dep1
repository: file://./subdir/chart
version: 1.0.0
`
chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
if err := os.WriteFile(chartYamlPath, []byte(chartYaml), 0644); err != nil {
t.Fatalf("failed to write Chart.yaml: %v", err)
}
logger := zap.NewNop().Sugar()
st := &HelmState{
logger: logger,
fs: filesystem.DefaultFileSystem(),
}
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
t.Cleanup(func() {
if rewrittenPath == tempDir {
return
}
cleanup()
if _, statErr := os.Stat(rewrittenPath); !os.IsNotExist(statErr) {
t.Errorf("expected rewritten chart path %q to be removed after cleanup", rewrittenPath)
}
})
data, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
if err != nil {
t.Fatalf("failed to read Chart.yaml: %v", err)
}
content := string(data)
if strings.Contains(content, "file://./subdir/chart") {
t.Errorf("relative path with ./ should have been converted")
}
}
func TestRewriteChartDependencies_RaceCondition(t *testing.T) {
tempDir, err := os.MkdirTemp("", "helmfile-test-")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
chartYaml := `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
- name: dep1
repository: file://../relative-chart
version: 1.0.0
`
chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
if err := os.WriteFile(chartYamlPath, []byte(chartYaml), 0644); err != nil {
t.Fatalf("failed to write Chart.yaml: %v", err)
}
numGoroutines := 10
var wg sync.WaitGroup
var readyWg sync.WaitGroup
errCh := make(chan error, numGoroutines)
ready := make(chan struct{})
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
readyWg.Add(1)
go func() {
defer wg.Done()
readyWg.Done()
<-ready
logger := zap.NewNop().Sugar()
st := &HelmState{
logger: logger,
fs: filesystem.DefaultFileSystem(),
}
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if err != nil {
errCh <- err
return
}
defer cleanup()
data, readErr := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
if readErr != nil {
errCh <- readErr
return
}
type ChartDependency struct {
Name string `yaml:"name"`
Repository string `yaml:"repository"`
Version string `yaml:"version"`
}
type ChartMeta struct {
APIVersion string `yaml:"apiVersion"`
Name string `yaml:"name"`
Version string `yaml:"version"`
Dependencies []ChartDependency `yaml:"dependencies,omitempty"`
}
var meta ChartMeta
if unmarshalErr := yaml.Unmarshal(data, &meta); unmarshalErr != nil {
errCh <- unmarshalErr
return
}
if meta.Name != "test-chart" {
errCh <- fmt.Errorf("expected chart name 'test-chart', got %q", meta.Name)
return
}
}()
}
readyWg.Wait()
close(ready)
wg.Wait()
close(errCh)
for err := range errCh {
t.Errorf("goroutine error: %v", err)
}
data, err := os.ReadFile(chartYamlPath)
if err != nil {
t.Fatalf("failed to read original Chart.yaml: %v", err)
}
type ChartDependency struct {
Name string `yaml:"name"`
Repository string `yaml:"repository"`
Version string `yaml:"version"`
}
type ChartMeta struct {
APIVersion string `yaml:"apiVersion"`
Name string `yaml:"name"`
Version string `yaml:"version"`
Dependencies []ChartDependency `yaml:"dependencies,omitempty"`
}
var chartMeta ChartMeta
if err := yaml.Unmarshal(data, &chartMeta); err != nil {
t.Fatalf("original Chart.yaml is not valid YAML: %v", err)
}
if chartMeta.Name != "test-chart" {
t.Errorf("expected original chart name 'test-chart', got %q", chartMeta.Name)
}
if len(chartMeta.Dependencies) == 0 {
t.Fatalf("expected original Chart.yaml to contain at least one dependency, got %d", len(chartMeta.Dependencies))
}
const wantDependencyName = "dep1"
if chartMeta.Dependencies[0].Name != wantDependencyName {
t.Errorf("expected first dependency name %q, got %q", wantDependencyName, chartMeta.Dependencies[0].Name)
}
const wantRepository = "file://../relative-chart"
if chartMeta.Dependencies[0].Repository != wantRepository {
t.Errorf("expected original dependency repository %q, got %q", wantRepository, chartMeta.Dependencies[0].Repository)
}
}