diff --git a/internal/chart/v3/loader/directory.go b/internal/chart/v3/loader/directory.go index 7cf343568..50142bfb2 100644 --- a/internal/chart/v3/loader/directory.go +++ b/internal/chart/v3/loader/directory.go @@ -77,6 +77,10 @@ func LoadDir(dir string) (*chart.Chart, error) { n = filepath.ToSlash(n) if err != nil { + // For broken symlinks, respect .helmignore before erroring. + if os.IsNotExist(err) && fi != nil && rules.Ignore(n, fi) { + return nil + } return err } if fi.IsDir() { diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index 5caec7815..874a245bf 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -93,6 +93,96 @@ func TestLoadDirWithSymlink(t *testing.T) { verifyDependenciesLock(t, c) } +func TestLoadDirWithBrokenSymlinkNotInHelmignore(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlink tests require unix") + } + + tmpDir := t.TempDir() + + chartYAML := `apiVersion: v2 +name: test +version: 0.1.0 +` + valuesYAML := `{} +` + if err := os.WriteFile(filepath.Join(tmpDir, "Chart.yaml"), []byte(chartYAML), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tmpDir, "values.yaml"), []byte(valuesYAML), 0644); err != nil { + t.Fatal(err) + } + + // Create a broken symlink NOT listed in .helmignore + brokenLink := filepath.Join(tmpDir, "broken") + if err := os.Symlink("nonexistent", brokenLink); err != nil { + t.Fatal(err) + } + + // Empty .helmignore — broken symlink is not ignored + if err := os.WriteFile(filepath.Join(tmpDir, ".helmignore"), []byte(""), 0644); err != nil { + t.Fatal(err) + } + + l, err := Loader(tmpDir) + if err != nil { + t.Fatal(err) + } + if _, err := l.Load(); err == nil || !os.IsNotExist(err) { + t.Fatalf("expected broken symlink error (os.IsNotExist), got: %v", err) + } +} + +func TestLoadDirWithBrokenSymlinkInHelmignore(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlink tests require unix") + } + + tmpDir := t.TempDir() + + // Create minimal chart structure + chartYAML := `apiVersion: v2 +name: test +version: 0.1.0 +` + valuesYAML := `{} +` + tpl := `config: {} +` + if err := os.WriteFile(filepath.Join(tmpDir, "Chart.yaml"), []byte(chartYAML), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tmpDir, "values.yaml"), []byte(valuesYAML), 0644); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(tmpDir, "templates"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tmpDir, "templates", "config.yaml"), []byte(tpl), 0644); err != nil { + t.Fatal(err) + } + + // Create a broken symlink in templates/ + brokenLink := filepath.Join(tmpDir, "templates", "broken") + if err := os.Symlink("nonexistent", brokenLink); err != nil { + t.Fatal(err) + } + + // Add the broken symlink to .helmignore + if err := os.WriteFile(filepath.Join(tmpDir, ".helmignore"), []byte("templates/broken\n"), 0644); err != nil { + t.Fatal(err) + } + + // Loading should succeed despite the broken symlink + l, err := Loader(tmpDir) + if err != nil { + t.Fatal(err) + } + if _, err := l.Load(); err != nil { + t.Fatalf("loading chart with broken symlink in .helmignore should not fail: %s", err) + } +} + func TestBomTestData(t *testing.T) { testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"} for _, file := range testFiles { diff --git a/internal/sympath/walk.go b/internal/sympath/walk.go index 812bb68ce..41b6cd6f0 100644 --- a/internal/sympath/walk.go +++ b/internal/sympath/walk.go @@ -21,7 +21,6 @@ limitations under the License. package sympath import ( - "fmt" "log/slog" "os" "path/filepath" @@ -68,12 +67,17 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error { if IsSymlink(info) { resolved, err := filepath.EvalSymlinks(path) if err != nil { - return fmt.Errorf("error evaluating symlink %s: %w", path, err) + // Pass the error to walkFn so callers (e.g. chart loaders) + // can decide whether to skip it based on .helmignore rules. + return walkFn(path, info, err) } // This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons. slog.Info("found symbolic link in path. Contents of linked file included and used", "path", path, "resolved", resolved) + originalInfo := info if info, err = os.Lstat(resolved); err != nil { - return err + // Route through walkFn with the original symlink info so callers + // can decide whether to skip it based on .helmignore rules. + return walkFn(path, originalInfo, err) } if err := symwalk(path, info, walkFn); err != nil && err != filepath.SkipDir { return err diff --git a/pkg/chart/v2/loader/directory.go b/pkg/chart/v2/loader/directory.go index a609ecbbf..3d98efc4c 100644 --- a/pkg/chart/v2/loader/directory.go +++ b/pkg/chart/v2/loader/directory.go @@ -77,6 +77,10 @@ func LoadDir(dir string) (*chart.Chart, error) { n = filepath.ToSlash(n) if err != nil { + // For broken symlinks, respect .helmignore before erroring. + if os.IsNotExist(err) && fi != nil && rules.Ignore(n, fi) { + return nil + } return err } if fi.IsDir() { diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index 46d17e899..9854051fa 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -92,6 +92,96 @@ func TestLoadDirWithSymlink(t *testing.T) { verifyDependenciesLock(t, c) } +func TestLoadDirWithBrokenSymlinkNotInHelmignore(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlink tests require unix") + } + + tmpDir := t.TempDir() + + chartYAML := `apiVersion: v2 +name: test +version: 0.1.0 +` + valuesYAML := `{} +` + if err := os.WriteFile(filepath.Join(tmpDir, "Chart.yaml"), []byte(chartYAML), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tmpDir, "values.yaml"), []byte(valuesYAML), 0644); err != nil { + t.Fatal(err) + } + + // Create a broken symlink NOT listed in .helmignore + brokenLink := filepath.Join(tmpDir, "broken") + if err := os.Symlink("nonexistent", brokenLink); err != nil { + t.Fatal(err) + } + + // Empty .helmignore — broken symlink is not ignored + if err := os.WriteFile(filepath.Join(tmpDir, ".helmignore"), []byte(""), 0644); err != nil { + t.Fatal(err) + } + + l, err := Loader(tmpDir) + if err != nil { + t.Fatal(err) + } + if _, err := l.Load(); err == nil || !os.IsNotExist(err) { + t.Fatalf("expected broken symlink error (os.IsNotExist), got: %v", err) + } +} + +func TestLoadDirWithBrokenSymlinkInHelmignore(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlink tests require unix") + } + + tmpDir := t.TempDir() + + // Create minimal chart structure + chartYAML := `apiVersion: v2 +name: test +version: 0.1.0 +` + valuesYAML := `{} +` + tpl := `config: {} +` + if err := os.WriteFile(filepath.Join(tmpDir, "Chart.yaml"), []byte(chartYAML), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tmpDir, "values.yaml"), []byte(valuesYAML), 0644); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(tmpDir, "templates"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tmpDir, "templates", "config.yaml"), []byte(tpl), 0644); err != nil { + t.Fatal(err) + } + + // Create a broken symlink in templates/ + brokenLink := filepath.Join(tmpDir, "templates", "broken") + if err := os.Symlink("nonexistent", brokenLink); err != nil { + t.Fatal(err) + } + + // Add the broken symlink to .helmignore + if err := os.WriteFile(filepath.Join(tmpDir, ".helmignore"), []byte("templates/broken\n"), 0644); err != nil { + t.Fatal(err) + } + + // Loading should succeed despite the broken symlink + l, err := Loader(tmpDir) + if err != nil { + t.Fatal(err) + } + if _, err := l.Load(); err != nil { + t.Fatalf("loading chart with broken symlink in .helmignore should not fail: %s", err) + } +} + func TestBomTestData(t *testing.T) { testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"} for _, file := range testFiles {