1112 lines
31 KiB
Go
1112 lines
31 KiB
Go
package state
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
helmchart "helm.sh/helm/v3/pkg/chart"
|
|
|
|
"github.com/helmfile/helmfile/pkg/filesystem"
|
|
"github.com/helmfile/helmfile/pkg/runtime"
|
|
"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)
|
|
}
|
|
}
|
|
|
|
// TestRewriteChartDependencies_RefreshesChartLock verifies that when Chart.yaml has
|
|
// its file:// dependencies rewritten to absolute paths, an existing Chart.lock is
|
|
// also updated in the temp copy: the digest is recomputed (otherwise `helm dep
|
|
// build` would error with "lock out of sync") and matching file:// repository URLs
|
|
// are mirrored over from the rewritten Chart.yaml (otherwise `helm dep build` would
|
|
// resolve the lock's relative file:// path against the temp directory and fail).
|
|
// Locked versions are preserved verbatim.
|
|
func TestRewriteChartDependencies_RefreshesChartLock(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
chartYaml := `apiVersion: v2
|
|
name: parent-chart
|
|
version: 1.0.0
|
|
dependencies:
|
|
- name: local-dep
|
|
repository: file://../local-dep
|
|
version: 1.0.0
|
|
- name: remote-dep
|
|
repository: https://example.com/charts
|
|
version: "*"
|
|
`
|
|
if err := os.WriteFile(filepath.Join(tempDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
|
|
t.Fatalf("writing Chart.yaml: %v", err)
|
|
}
|
|
|
|
const originalDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
chartLock := `dependencies:
|
|
- name: local-dep
|
|
repository: file://../local-dep
|
|
version: 1.0.0
|
|
- name: remote-dep
|
|
repository: https://example.com/charts
|
|
version: 1.2.3
|
|
digest: ` + originalDigest + `
|
|
generated: "2024-01-01T00:00:00Z"
|
|
`
|
|
if err := os.WriteFile(filepath.Join(tempDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
|
|
t.Fatalf("writing Chart.lock: %v", err)
|
|
}
|
|
|
|
logger := zap.NewNop().Sugar()
|
|
st := &HelmState{
|
|
basePath: tempDir,
|
|
fs: filesystem.DefaultFileSystem(),
|
|
logger: logger,
|
|
}
|
|
|
|
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
|
|
if err != nil {
|
|
t.Fatalf("rewriteChartDependencies failed: %v", err)
|
|
}
|
|
defer cleanup()
|
|
|
|
if rewrittenPath == tempDir {
|
|
t.Fatalf("expected a temp copy to be created, got original path %q", rewrittenPath)
|
|
}
|
|
|
|
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
|
|
if err != nil {
|
|
t.Fatalf("reading rewritten Chart.lock: %v", err)
|
|
}
|
|
|
|
var lock struct {
|
|
Dependencies []struct {
|
|
Name string `yaml:"name"`
|
|
Repository string `yaml:"repository"`
|
|
Version string `yaml:"version"`
|
|
} `yaml:"dependencies"`
|
|
Digest string `yaml:"digest"`
|
|
Generated string `yaml:"generated"`
|
|
}
|
|
if err := yaml.Unmarshal(lockData, &lock); err != nil {
|
|
t.Fatalf("parsing rewritten Chart.lock: %v", err)
|
|
}
|
|
|
|
if lock.Digest == originalDigest {
|
|
t.Errorf("expected digest to be recomputed; still %q", lock.Digest)
|
|
}
|
|
if !strings.HasPrefix(lock.Digest, "sha256:") {
|
|
t.Errorf("expected sha256 digest, got %q", lock.Digest)
|
|
}
|
|
|
|
if len(lock.Dependencies) != 2 {
|
|
t.Fatalf("expected 2 lock dependencies, got %d", len(lock.Dependencies))
|
|
}
|
|
|
|
// The local file:// dependency's repository must be mirrored to the absolute
|
|
// path so `helm dep build` can resolve it from the temp chart directory.
|
|
localDep := lock.Dependencies[0]
|
|
if localDep.Name != "local-dep" {
|
|
t.Fatalf("expected first lock dep name 'local-dep', got %q", localDep.Name)
|
|
}
|
|
if !filepath.IsAbs(strings.TrimPrefix(localDep.Repository, "file://")) {
|
|
t.Errorf("expected local-dep repository to be an absolute file:// path, got %q", localDep.Repository)
|
|
}
|
|
if localDep.Version != "1.0.0" {
|
|
t.Errorf("expected local-dep version preserved as 1.0.0, got %q", localDep.Version)
|
|
}
|
|
|
|
// Remote (non-file://) deps must be untouched.
|
|
remoteDep := lock.Dependencies[1]
|
|
if remoteDep.Repository != "https://example.com/charts" {
|
|
t.Errorf("expected remote dep repository unchanged, got %q", remoteDep.Repository)
|
|
}
|
|
if remoteDep.Version != "1.2.3" {
|
|
t.Errorf("expected remote dep version preserved as 1.2.3, got %q", remoteDep.Version)
|
|
}
|
|
|
|
// The original Chart.lock on disk must be untouched.
|
|
originalLock, err := os.ReadFile(filepath.Join(tempDir, "Chart.lock"))
|
|
if err != nil {
|
|
t.Fatalf("reading original Chart.lock: %v", err)
|
|
}
|
|
if string(originalLock) != chartLock {
|
|
t.Errorf("original Chart.lock was modified; expected unchanged content")
|
|
}
|
|
}
|
|
|
|
// TestRewriteChartDependencies_RefreshesChartLockWithExtraFields verifies that
|
|
// Chart.lock digest recomputation includes all dependency fields (alias, condition,
|
|
// tags, import-values, enabled) — not just name/repository/version — so the digest
|
|
// stays compatible with Helm's resolver.HashReq for charts using those fields.
|
|
// It proves field coverage by running two chart variants under a shared root
|
|
// (so file:// paths resolve to the same absolute location) and asserting the
|
|
// digests differ only due to extra fields.
|
|
func TestRewriteChartDependencies_RefreshesChartLockWithExtraFields(t *testing.T) {
|
|
// Use a shared root so both chart variants resolve file://../local-dep to the
|
|
// same absolute path — isolating the digest difference to field content only.
|
|
sharedRoot := t.TempDir()
|
|
chartDir := filepath.Join(sharedRoot, "parent")
|
|
if err := os.MkdirAll(chartDir, 0755); err != nil {
|
|
t.Fatalf("creating chart dir: %v", err)
|
|
}
|
|
|
|
// Run rewriteChartDependencies for a given Chart.yaml and return the recomputed digest.
|
|
getDigest := func(t *testing.T, chartYaml, chartLock string) string {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
|
|
t.Fatalf("writing Chart.yaml: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(chartDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
|
|
t.Fatalf("writing Chart.lock: %v", err)
|
|
}
|
|
logger := zap.NewNop().Sugar()
|
|
st := &HelmState{
|
|
basePath: chartDir,
|
|
fs: filesystem.DefaultFileSystem(),
|
|
logger: logger,
|
|
}
|
|
rewrittenPath, cleanup, err := st.rewriteChartDependencies(chartDir)
|
|
if err != nil {
|
|
t.Fatalf("rewriteChartDependencies failed: %v", err)
|
|
}
|
|
defer cleanup()
|
|
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
|
|
if err != nil {
|
|
t.Fatalf("reading rewritten Chart.lock: %v", err)
|
|
}
|
|
var lock struct {
|
|
Digest string `yaml:"digest"`
|
|
}
|
|
if err := yaml.Unmarshal(lockData, &lock); err != nil {
|
|
t.Fatalf("parsing rewritten Chart.lock: %v", err)
|
|
}
|
|
return lock.Digest
|
|
}
|
|
|
|
const originalDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
baseLock := `dependencies:
|
|
- name: local-dep
|
|
repository: file://../local-dep
|
|
version: 1.0.0
|
|
alias: my-local
|
|
- name: local-dep
|
|
repository: file://../local-dep-alt
|
|
version: 2.0.0
|
|
alias: my-local-alt
|
|
digest: ` + originalDigest + `
|
|
generated: "2024-01-01T00:00:00Z"
|
|
`
|
|
|
|
// Chart.yaml with extra fields (alias, condition, tags, import-values).
|
|
chartYamlWithExtras := `apiVersion: v2
|
|
name: parent-chart
|
|
version: 1.0.0
|
|
dependencies:
|
|
- name: local-dep
|
|
repository: file://../local-dep
|
|
version: 1.0.0
|
|
alias: my-local
|
|
condition: local-dep.enabled
|
|
tags:
|
|
- frontend
|
|
- optional
|
|
import-values:
|
|
- child: config
|
|
parent: global.config
|
|
- name: local-dep
|
|
repository: file://../local-dep-alt
|
|
version: 2.0.0
|
|
alias: my-local-alt
|
|
`
|
|
|
|
// Same chart without condition/tags/import-values — only alias remains.
|
|
chartYamlWithoutExtras := `apiVersion: v2
|
|
name: parent-chart
|
|
version: 1.0.0
|
|
dependencies:
|
|
- name: local-dep
|
|
repository: file://../local-dep
|
|
version: 1.0.0
|
|
alias: my-local
|
|
- name: local-dep
|
|
repository: file://../local-dep-alt
|
|
version: 2.0.0
|
|
alias: my-local-alt
|
|
`
|
|
|
|
digestWith := getDigest(t, chartYamlWithExtras, baseLock)
|
|
digestWithout := getDigest(t, chartYamlWithoutExtras, baseLock)
|
|
|
|
if !strings.HasPrefix(digestWith, "sha256:") {
|
|
t.Errorf("expected sha256 digest, got %q", digestWith)
|
|
}
|
|
if digestWith == originalDigest {
|
|
t.Errorf("expected digest to be recomputed; still %q", digestWith)
|
|
}
|
|
if digestWith == digestWithout {
|
|
t.Errorf("digest should differ when extra fields (condition, tags, import-values) are present, but both are %q", digestWith)
|
|
}
|
|
|
|
// Also verify alias-based matching: both deps have name "local-dep" but
|
|
// different aliases; both should get their file:// paths rewritten.
|
|
if err := os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(chartYamlWithExtras), 0644); err != nil {
|
|
t.Fatalf("writing Chart.yaml: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(chartDir, "Chart.lock"), []byte(baseLock), 0644); err != nil {
|
|
t.Fatalf("writing Chart.lock: %v", err)
|
|
}
|
|
logger := zap.NewNop().Sugar()
|
|
st := &HelmState{
|
|
basePath: chartDir,
|
|
fs: filesystem.DefaultFileSystem(),
|
|
logger: logger,
|
|
}
|
|
rewrittenPath, cleanup, err := st.rewriteChartDependencies(chartDir)
|
|
if err != nil {
|
|
t.Fatalf("rewriteChartDependencies failed: %v", err)
|
|
}
|
|
defer cleanup()
|
|
|
|
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
|
|
if err != nil {
|
|
t.Fatalf("reading rewritten Chart.lock: %v", err)
|
|
}
|
|
var lock struct {
|
|
Dependencies []struct {
|
|
Name string `yaml:"name"`
|
|
Repository string `yaml:"repository"`
|
|
Version string `yaml:"version"`
|
|
Alias string `yaml:"alias"`
|
|
} `yaml:"dependencies"`
|
|
}
|
|
if err := yaml.Unmarshal(lockData, &lock); err != nil {
|
|
t.Fatalf("parsing rewritten Chart.lock: %v", err)
|
|
}
|
|
if len(lock.Dependencies) != 2 {
|
|
t.Fatalf("expected 2 lock dependencies, got %d", len(lock.Dependencies))
|
|
}
|
|
|
|
dep1 := lock.Dependencies[0]
|
|
if dep1.Alias != "my-local" {
|
|
t.Errorf("expected first lock dep alias 'my-local', got %q", dep1.Alias)
|
|
}
|
|
if !filepath.IsAbs(strings.TrimPrefix(dep1.Repository, "file://")) {
|
|
t.Errorf("expected first dep repository to be an absolute file:// path, got %q", dep1.Repository)
|
|
}
|
|
|
|
dep2 := lock.Dependencies[1]
|
|
if dep2.Alias != "my-local-alt" {
|
|
t.Errorf("expected second lock dep alias 'my-local-alt', got %q", dep2.Alias)
|
|
}
|
|
if !filepath.IsAbs(strings.TrimPrefix(dep2.Repository, "file://")) {
|
|
t.Errorf("expected second dep repository to be an absolute file:// path, got %q", dep2.Repository)
|
|
}
|
|
}
|
|
|
|
// TestRewriteChartDependencies_GoYamlV2ImportValues verifies that Chart.lock
|
|
// refresh works under go-yaml v2 (HELMFILE_GO_YAML_V3=false), where nested
|
|
// maps in import-values decode as map[interface{}]interface{} which json.Marshal
|
|
// cannot handle without normalization.
|
|
func TestRewriteChartDependencies_GoYamlV2ImportValues(t *testing.T) {
|
|
prev := runtime.GoYamlV3
|
|
runtime.GoYamlV3 = false
|
|
t.Cleanup(func() {
|
|
runtime.GoYamlV3 = prev
|
|
})
|
|
|
|
tempDir := t.TempDir()
|
|
|
|
chartYaml := `apiVersion: v2
|
|
name: parent-chart
|
|
version: 1.0.0
|
|
dependencies:
|
|
- name: local-dep
|
|
repository: file://../local-dep
|
|
version: 1.0.0
|
|
import-values:
|
|
- child: config
|
|
parent: global.config
|
|
`
|
|
chartLock := `dependencies:
|
|
- name: local-dep
|
|
repository: file://../local-dep
|
|
version: 1.0.0
|
|
import-values:
|
|
- child: config
|
|
parent: global.config
|
|
digest: sha256:0000000000000000000000000000000000000000000000000000000000000000
|
|
generated: "2024-01-01T00:00:00Z"
|
|
`
|
|
|
|
if err := os.WriteFile(filepath.Join(tempDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
|
|
t.Fatalf("writing Chart.yaml: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(tempDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
|
|
t.Fatalf("writing Chart.lock: %v", err)
|
|
}
|
|
|
|
logger := zap.NewNop().Sugar()
|
|
st := &HelmState{
|
|
basePath: tempDir,
|
|
fs: filesystem.DefaultFileSystem(),
|
|
logger: logger,
|
|
}
|
|
|
|
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
|
|
if err != nil {
|
|
t.Fatalf("rewriteChartDependencies failed: %v", err)
|
|
}
|
|
defer cleanup()
|
|
|
|
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
|
|
if err != nil {
|
|
t.Fatalf("reading rewritten Chart.lock: %v", err)
|
|
}
|
|
|
|
var lock struct {
|
|
Digest string `yaml:"digest"`
|
|
}
|
|
if err := yaml.Unmarshal(lockData, &lock); err != nil {
|
|
t.Fatalf("parsing rewritten Chart.lock: %v", err)
|
|
}
|
|
|
|
if !strings.HasPrefix(lock.Digest, "sha256:") {
|
|
t.Errorf("expected sha256 digest, got %q", lock.Digest)
|
|
}
|
|
const originalDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
if lock.Digest == originalDigest {
|
|
t.Errorf("expected digest to be recomputed; still %q", lock.Digest)
|
|
}
|
|
}
|
|
|
|
// TestRewriteChartDependencies_DigestMatchesHelmHashReq verifies the recomputed
|
|
// digest matches what Helm's resolver.HashReq would produce for a known input.
|
|
// This guards against producing a digest that is "different" but still rejected
|
|
// by `helm dependency build`.
|
|
func TestRewriteChartDependencies_DigestMatchesHelmHashReq(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
chartYaml := `apiVersion: v2
|
|
name: test-chart
|
|
version: 1.0.0
|
|
dependencies:
|
|
- name: dep-a
|
|
repository: file://../dep-a
|
|
version: 2.0.0
|
|
condition: dep-a.enabled
|
|
tags:
|
|
- backend
|
|
`
|
|
chartLock := `dependencies:
|
|
- name: dep-a
|
|
repository: file://../dep-a
|
|
version: 2.0.0
|
|
digest: sha256:0000000000000000000000000000000000000000000000000000000000000000
|
|
generated: "2024-01-01T00:00:00Z"
|
|
`
|
|
|
|
if err := os.WriteFile(filepath.Join(tempDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
|
|
t.Fatalf("writing Chart.yaml: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(tempDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
|
|
t.Fatalf("writing Chart.lock: %v", err)
|
|
}
|
|
|
|
logger := zap.NewNop().Sugar()
|
|
st := &HelmState{
|
|
basePath: tempDir,
|
|
fs: filesystem.DefaultFileSystem(),
|
|
logger: logger,
|
|
}
|
|
|
|
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
|
|
if err != nil {
|
|
t.Fatalf("rewriteChartDependencies failed: %v", err)
|
|
}
|
|
defer cleanup()
|
|
|
|
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
|
|
if err != nil {
|
|
t.Fatalf("reading rewritten Chart.lock: %v", err)
|
|
}
|
|
|
|
var lock struct {
|
|
Dependencies []*helmchart.Dependency `yaml:"dependencies"`
|
|
Digest string `yaml:"digest"`
|
|
}
|
|
if err := yaml.Unmarshal(lockData, &lock); err != nil {
|
|
t.Fatalf("parsing rewritten Chart.lock: %v", err)
|
|
}
|
|
|
|
// Compute the expected digest independently using Helm's HashReq algorithm:
|
|
// sha256(json.Marshal([2][]*chart.Dependency{req, lock}))
|
|
// where req = rewritten Chart.yaml deps, lock = rewritten Chart.lock deps.
|
|
absDepA, err := filepath.Abs(filepath.Join(tempDir, "../dep-a"))
|
|
if err != nil {
|
|
t.Fatalf("resolving absolute path: %v", err)
|
|
}
|
|
|
|
req := []*helmchart.Dependency{
|
|
{
|
|
Name: "dep-a",
|
|
Repository: "file://" + absDepA,
|
|
Version: "2.0.0",
|
|
Condition: "dep-a.enabled",
|
|
Tags: []string{"backend"},
|
|
},
|
|
}
|
|
lockDeps := []*helmchart.Dependency{
|
|
{
|
|
Name: "dep-a",
|
|
Repository: "file://" + absDepA,
|
|
Version: "2.0.0",
|
|
},
|
|
}
|
|
|
|
payload, err := json.Marshal([2][]*helmchart.Dependency{req, lockDeps})
|
|
if err != nil {
|
|
t.Fatalf("marshaling expected digest payload: %v", err)
|
|
}
|
|
sum := sha256.Sum256(payload)
|
|
expectedDigest := "sha256:" + hex.EncodeToString(sum[:])
|
|
|
|
if lock.Digest != expectedDigest {
|
|
t.Errorf("digest mismatch with Helm's HashReq algorithm:\n got: %s\n want: %s", lock.Digest, expectedDigest)
|
|
}
|
|
}
|