Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions pkg/reposerverextract/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,27 @@ func copyDir(src, dst string) error {
if info.IsDir() {
return os.MkdirAll(dstPath, 0o755)
}
// filepath.Walk does not follow symlinks, so info describes the link
// itself (IsDir() is false even when it points at a directory). Passing
// such a link to copyFile would os.Open() the target directory and
// io.Copy() it, which fails with EISDIR ("is a directory"). Resolve the
// link and, when it targets a directory, recurse so the directory's
// contents are materialized at the destination — matching how Argo CD's
// repo-server reads a chart's on-disk content. This is common in charts
// that vendor assets via directory symlinks (e.g. files/sql -> ../../sql).
if info.Mode()&os.ModeSymlink != 0 {
target, err := os.Stat(srcPath)
if err != nil {
return err
}
if target.IsDir() {
resolved, err := filepath.EvalSymlinks(srcPath)
if err != nil {
return err
}
return copyDir(resolved, dstPath)
}
}
return copyFile(srcPath, dstPath)
})
}
Expand Down
28 changes: 28 additions & 0 deletions pkg/reposerverextract/extract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1354,3 +1354,31 @@ func TestCollectRepoURLs_Empty(t *testing.T) {
urls := collectRepoURLs([]argoapplication.ArgoResource{})
assert.Empty(t, urls)
}

// copyDir must follow a symlink that points at a directory and materialize its
// contents at the destination. Before the fix it byte-copied the link, which
// os.Open()-ed the target directory and failed with EISDIR ("is a directory").
// Charts commonly vendor assets this way (e.g. files/sql -> ../../farm-api/sql).
func TestCopyDir_FollowsDirectorySymlink(t *testing.T) {
root := t.TempDir()

// The symlink target: a real directory (outside the chart) with a file in it.
external := filepath.Join(root, "external", "sql")
require.NoError(t, os.MkdirAll(external, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(external, "V1__init.sql"), []byte("SELECT 1;"), 0o644))

// The chart dir: a regular file plus files/sql -> the external directory.
chart := filepath.Join(root, "chart")
require.NoError(t, os.MkdirAll(filepath.Join(chart, "files"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(chart, "Chart.yaml"), []byte("name: c\n"), 0o644))
require.NoError(t, os.Symlink(external, filepath.Join(chart, "files", "sql")))

dst := filepath.Join(root, "dst")
require.NoError(t, copyDir(chart, dst))

// The regular file and the symlinked directory's contents are both present.
assert.FileExists(t, filepath.Join(dst, "Chart.yaml"))
got, err := os.ReadFile(filepath.Join(dst, "files", "sql", "V1__init.sql"))
require.NoError(t, err)
assert.Equal(t, "SELECT 1;", string(got))
}
Loading