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
53 changes: 48 additions & 5 deletions pkg/reposerverextract/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -737,12 +737,20 @@ func buildManifestRequestForSource(
}
}

// Copy the content source directory into the temp tree.
// Kustomize can reference files outside the source dir, so stage the
// whole checkout (like the no-refs fast path).
srcContentDir := filepath.Join(branchFolder, primarySource.Path)
dstContentDir := filepath.Join(tempDir, primarySource.Path)
if err := copyDir(srcContentDir, dstContentDir); err != nil {
cleanup()
return nil, "", nil, fmt.Errorf("failed to copy content source dir %q: %w", srcContentDir, err)
if isKustomizeSource(srcContentDir) {
if err := stageCheckout(branchFolder, tempDir); err != nil {
cleanup()
return nil, "", nil, fmt.Errorf("failed to stage branch folder %q: %w", branchFolder, err)
}
} else {
dstContentDir := filepath.Join(tempDir, primarySource.Path)
if err := copyDir(srcContentDir, dstContentDir); err != nil {
cleanup()
return nil, "", nil, fmt.Errorf("failed to copy content source dir %q: %w", srcContentDir, err)
}
}

// Copy only the ref files used by this content source into
Expand Down Expand Up @@ -1115,6 +1123,41 @@ func hasExternalRefSource(refSources []v1alpha1.ApplicationSource, repoSelector
return false
}

// stageCheckout mirrors a checkout into dst: files are hardlinked (staging
// is read-only), symlinks stay symlinks, .git is skipped. Links are left for
// the repo server and kustomize to validate, like on a real clone.
func stageCheckout(src, dst string) error {
return filepath.Walk(src, func(srcPath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() && info.Name() == ".git" {
return filepath.SkipDir
}
rel, err := filepath.Rel(src, srcPath)
if err != nil {
return err
}
dstPath := filepath.Join(dst, rel)
switch {
case info.IsDir():
return os.MkdirAll(dstPath, 0o755)
case info.Mode()&os.ModeSymlink != 0:
target, err := os.Readlink(srcPath)
if err != nil {
return err
}
return os.Symlink(target, dstPath)
default:
// parent dirs already exist: Walk visits directories first
if err := os.Link(srcPath, dstPath); err == nil {
return nil
}
return copyFile(srcPath, dstPath)
}
})
}

// copyDir recursively copies src into dst, creating dst if needed.
func copyDir(src, dst string) error {
return filepath.Walk(src, func(srcPath string, info os.FileInfo, err error) error {
Expand Down
79 changes: 79 additions & 0 deletions pkg/reposerverextract/extract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,85 @@ spec:
assertDefaultProjectFields(t, req)
}

// Multi-source: a kustomize content source referencing files outside its own
// directory must stage the whole checkout (like the no-refs fast path), or
// the escaping reference fails with "no such file or directory".
func TestBuildManifestRequest_MultiSource_Kustomize_WithRef_StagesBranchRoot(t *testing.T) {
branchFolder := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(branchFolder, "clusters", "dev", "my-app"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(branchFolder, "clusters", "dev", "my-app", "kustomization.yaml"),
[]byte("resources:\n - ../../../base/my-app\n"), 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(branchFolder, "base", "my-app"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(branchFolder, "base", "my-app", "kustomization.yaml"),
[]byte("resources: []\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(branchFolder, "values.yaml"), []byte("a: 1\n"), 0o644))

app := makeApp(t, `
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-dev
spec:
destination:
namespace: production
sources:
- repoURL: https://github.com/org/repo.git
ref: values
targetRevision: HEAD
- repoURL: https://github.com/org/repo.git
path: clusters/dev/my-app
targetRevision: HEAD
`)

contentSources, refSources, hasMultipleSources, err := splitSources(app)
require.NoError(t, err)
require.Len(t, contentSources, 1)
require.Len(t, refSources, 1)

req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{
repoSelector: testRepoSelector(t, ""),
})
require.NoError(t, err)
require.NotEmpty(t, streamDir)
defer cleanup()

assert.Equal(t, "clusters/dev/my-app", req.ApplicationSource.Path)
_, statErr := os.Stat(filepath.Join(streamDir, "base", "my-app", "kustomization.yaml"))
assert.NoError(t, statErr, "files referenced outside the kustomize source dir must be staged")
}

// symlinks stay symlinks (the repo server and kustomize validate them, like
// on a real clone), files are hardlinked, .git is skipped
func TestStageCheckout_PreservesSymlinksAndHardlinks(t *testing.T) {
src := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(src, "app.yaml"), []byte("a: 1"), 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(src, ".git"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(src, ".git", "HEAD"), []byte("ref"), 0o644))
require.NoError(t, os.Symlink("app.yaml", filepath.Join(src, "link-in.yaml")))
require.NoError(t, os.Symlink("../outside.yaml", filepath.Join(src, "link-out.yaml")))

dst := filepath.Join(t.TempDir(), "dst")
require.NoError(t, stageCheckout(src, dst))

// symlinks preserved verbatim, including ones pointing outside the tree
target, err := os.Readlink(filepath.Join(dst, "link-in.yaml"))
require.NoError(t, err)
assert.Equal(t, "app.yaml", target)
target, err = os.Readlink(filepath.Join(dst, "link-out.yaml"))
require.NoError(t, err)
assert.Equal(t, "../outside.yaml", target)

// regular files hardlinked, .git skipped
srcInfo, err := os.Stat(filepath.Join(src, "app.yaml"))
require.NoError(t, err)
dstInfo, err := os.Stat(filepath.Join(dst, "app.yaml"))
require.NoError(t, err)
assert.True(t, os.SameFile(srcInfo, dstInfo), "regular files should be hardlinked")
assert.NoDirExists(t, filepath.Join(dst, ".git"))
}

// ─────────────────────────────────────────────────────────────────────────────
// 4b. Multi-source: external chart with a ref+path dual-purpose source (GH #401)
//
Expand Down