Repository: helm/helm
Pull request #31041 (2025-07-09 11:41:07)

Updating link handling
**What this PR does / why we need it**:

Backport of https://github.com/helm/helm/commit/4b8e61093d8f579f1165cdc6bd4b43fa5455f571 to release-3.17 hopefully to be cut into 3.17.4

**If applicable**:
- [ ] this PR contains user facing changes (the `docs needed` label should be applied if so)
- [X] this PR contains unit tests
- [x] this PR has been tested for backwards compatibility



Patch:
diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go
index ec4056d27..cc7850aae 100644
--- a/pkg/downloader/manager.go
+++ b/pkg/downloader/manager.go
@@ -852,6 +852,20 @@ func writeLock(chartpath string, lock *chart.Lock, legacyLockfile bool) error {
 		lockfileName = "requirements.lock"
 	}
 	dest := filepath.Join(chartpath, lockfileName)
+
+	info, err := os.Lstat(dest)
+	if err != nil && !os.IsNotExist(err) {
+		return fmt.Errorf("error getting info for %q: %w", dest, err)
+	} else if err == nil {
+		if info.Mode()&os.ModeSymlink != 0 {
+			link, err := os.Readlink(dest)
+			if err != nil {
+				return fmt.Errorf("error reading symlink for %q: %w", dest, err)
+			}
+			return fmt.Errorf("the %s file is a symlink to %q", lockfileName, link)
+		}
+	}
+
 	return os.WriteFile(dest, data, 0644)
 }
 


Test patch:
diff --git a/pkg/downloader/manager_test.go b/pkg/downloader/manager_test.go
index db2487d16..63de10ac4 100644
--- a/pkg/downloader/manager_test.go
+++ b/pkg/downloader/manager_test.go
@@ -20,7 +20,11 @@ import (
 	"os"
 	"path/filepath"
 	"reflect"
+	"strings"
 	"testing"
+	"time"
+
+	"sigs.k8s.io/yaml"
 
 	"helm.sh/helm/v3/pkg/chart"
 	"helm.sh/helm/v3/pkg/chart/loader"
@@ -598,3 +602,146 @@ func TestKey(t *testing.T) {
 		}
 	}
 }
+
+func TestWriteLock(t *testing.T) {
+	fixedTime, err := time.Parse(time.RFC3339, "2025-07-04T00:00:00Z")
+	if err != nil {
+		t.Fatalf("failed to parse fixed time: %v", err)
+	}
+	lock := &chart.Lock{
+		Generated: fixedTime,
+		Digest:    "sha256:12345",
+		Dependencies: []*chart.Dependency{
+			{
+				Name:       "fantastic-chart",
+				Version:    "1.2.3",
+				Repository: "https://example.com/charts",
+			},
+		},
+	}
+	expectedContent, err := yaml.Marshal(lock)
+	if err != nil {
+		t.Fatalf("failed to marshal lock: %v", err)
+	}
+
+	t.Run("v2 lock file", func(t *testing.T) {
+		dir := t.TempDir()
+		err := writeLock(dir, lock, false)
+		if err != nil {
+			t.Fatalf("failed to write lock file: %v", err)
+		}
+
+		lockfilePath := filepath.Join(dir, "Chart.lock")
+		_, err = os.Stat(lockfilePath)
+		if err != nil {
+			t.Fatalf("Chart.lock should exist: %v", err)
+		}
+
+		content, err := os.ReadFile(lockfilePath)
+		if err != nil {
+			t.Fatalf("failed to read Chart.lock: %v", err)
+		}
+		if !bytes.Equal(content, expectedContent) {
+			t.Fatalf("Chart.lock content mismatch:\nExpected: %s\nGot: %s", expectedContent, content)
+		}
+
+		// Check that requirements.lock does not exist
+		_, err = os.Stat(filepath.Join(dir, "requirements.lock"))
+		if err == nil {
+			t.Fatal("requirements.lock should not exist")
+		}
+		if !os.IsNotExist(err) {
+			t.Fatalf("requirements.lock should not exist, got error: %v", err)
+		}
+	})
+
+	t.Run("v1 lock file", func(t *testing.T) {
+		dir := t.TempDir()
+		err := writeLock(dir, lock, true)
+		if err != nil {
+			t.Fatalf("failed to write lock file: %v", err)
+		}
+
+		lockfilePath := filepath.Join(dir, "requirements.lock")
+		_, err = os.Stat(lockfilePath)
+		if err != nil {
+			t.Fatalf("requirements.lock should exist: %v", err)
+		}
+
+		content, err := os.ReadFile(lockfilePath)
+		if err != nil {
+			t.Fatalf("failed to read requirements.lock: %v", err)
+		}
+		if !bytes.Equal(content, expectedContent) {
+			t.Fatalf("requirements.lock content mismatch:\nExpected: %s\nGot: %s", expectedContent, content)
+		}
+
+		// Check that Chart.lock does not exist
+		_, err = os.Stat(filepath.Join(dir, "Chart.lock"))
+		if err == nil {
+			t.Fatal("Chart.lock should not exist")
+		}
+		if !os.IsNotExist(err) {
+			t.Fatalf("Chart.lock should not exist, got error: %v", err)
+		}
+	})
+
+	t.Run("overwrite existing lock file", func(t *testing.T) {
+		dir := t.TempDir()
+		lockfilePath := filepath.Join(dir, "Chart.lock")
+		err := os.WriteFile(lockfilePath, []byte("old content"), 0644)
+		if err != nil {
+			t.Fatalf("failed to create initial Chart.lock: %v", err)
+		}
+
+		err = writeLock(dir, lock, false)
+		if err != nil {
+			t.Fatalf("failed to overwrite lock file: %v", err)
+		}
+
+		content, err := os.ReadFile(lockfilePath)
+		if err != nil {
+			t.Fatalf("failed to read overwritten Chart.lock: %v", err)
+		}
+		if !bytes.Equal(content, expectedContent) {
+			t.Fatalf("Overwritten Chart.lock content mismatch:\nExpected: %s\nGot: %s", expectedContent, content)
+		}
+	})
+
+	t.Run("lock file is a symlink", func(t *testing.T) {
+		dir := t.TempDir()
+		dummyFile := filepath.Join(dir, "dummy.txt")
+		err := os.WriteFile(dummyFile, []byte("dummy"), 0644)
+		if err != nil {
+			t.Fatalf("failed to create dummy file: %v", err)
+		}
+
+		lockfilePath := filepath.Join(dir, "Chart.lock")
+		err = os.Symlink(dummyFile, lockfilePath)
+		if err != nil {
+			t.Fatalf("failed to create symlink for Chart.lock: %v", err)
+		}
+
+		err = writeLock(dir, lock, false)
+		if err == nil {
+			t.Fatal("expected error when writing lock file to a symlink")
+		}
+		if !strings.Contains(err.Error(), "the Chart.lock file is a symlink to") {
+			t.Fatalf("unexpected error: %v", err)
+		}
+	})
+
+	t.Run("chart path is not a directory", func(t *testing.T) {
+		dir := t.TempDir()
+		filePath := filepath.Join(dir, "not-a-dir")
+		err := os.WriteFile(filePath, []byte("file"), 0644)
+		if err != nil {
+			t.Fatalf("failed to create file: %v", err)
+		}
+
+		err = writeLock(filePath, lock, false)
+		if err == nil {
+			t.Fatal("expected error when writing lock file to a non-directory path")
+		}
+	})
+}