diff --git a/pkg/reposerverextract/extract.go b/pkg/reposerverextract/extract.go index b24c027c..db3f6ab2 100644 --- a/pkg/reposerverextract/extract.go +++ b/pkg/reposerverextract/extract.go @@ -726,6 +726,16 @@ func buildManifestRequestForSource( return request, "", nil, nil } + // Kustomize can reference directories outside the source path. Stream the + // existing checkout directly, as on the no-refs path, so those directories + // remain available without creating and recompressing a per-request copy of + // the whole checkout. Ref sources are only used by Helm value files and file + // parameters, so a Kustomize source does not need the .refs staging tree. + srcContentDir := filepath.Join(branchFolder, primarySource.Path) + if isKustomizeSource(srcContentDir) { + return newManifestRequest(&primarySource), branchFolder, nil, nil + } + // // ← content source files // /.refs// ← files for each ref source tempDir, err := os.MkdirTemp("", "argocd-diff-preview-*") @@ -738,20 +748,11 @@ func buildManifestRequestForSource( } } - // 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) - 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 the content source directory into the temp tree. + 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 @@ -1124,41 +1125,6 @@ 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 { diff --git a/pkg/reposerverextract/extract_test.go b/pkg/reposerverextract/extract_test.go index e2934465..366c056d 100644 --- a/pkg/reposerverextract/extract_test.go +++ b/pkg/reposerverextract/extract_test.go @@ -540,10 +540,11 @@ 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) { +// Multi-source: a kustomize content source referencing a directory outside its +// own path must stream the existing checkout, like the no-refs path. Besides +// keeping the referenced directory available, reusing this path lets the tgz +// cache share one compressed checkout across applications. +func TestBuildManifestRequest_MultiSource_Kustomize_WithRef_StreamsBranchRoot(t *testing.T) { branchFolder := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(branchFolder, "clusters", "dev", "my-app"), 0o755)) require.NoError(t, os.WriteFile( @@ -581,42 +582,12 @@ spec: repoSelector: testRepoSelector(t, ""), }) require.NoError(t, err) - require.NotEmpty(t, streamDir) - defer cleanup() + assert.Equal(t, branchFolder, streamDir) + assert.Nil(t, 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")) + assert.NoError(t, statErr, "directories referenced outside the kustomize source path must remain available") } // ─────────────────────────────────────────────────────────────────────────────