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
7 changes: 7 additions & 0 deletions pkg/dashboard/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ func (r *DetectResult) detectLocal(dir string) {
if !entry.IsDir() {
continue
}
// Skip hidden / vendor directories so a stray pacto.yaml (e.g. under
// ~/.Trash) cannot root the local source at a large directory like $HOME,
// whose recursive scan would block ListServices indefinitely. Mirrors the
// rules used by the recursive walk (skipScanDir).
if skipScanDir(entry.Name()) {
continue
}
if _, err := os.Stat(filepath.Join(dir, entry.Name(), contractFile)); err == nil {
info.Enabled = true
info.Reason = "pacto.yaml found in subdirectory " + entry.Name()
Expand Down
28 changes: 28 additions & 0 deletions pkg/dashboard/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,34 @@ func TestDetectLocal_SubdirPactoYAML(t *testing.T) {
}
}

func TestDetectLocal_IgnoresHiddenAndVendorSubdirs(t *testing.T) {
// A pacto.yaml inside a hidden / node_modules / vendor immediate subdir must
// NOT activate the local source. Otherwise running `pacto dashboard` from a
// large root (e.g. $HOME, where ~/.Trash may contain a pacto.yaml) roots the
// local source at that directory and LocalSource.ListServices recursively
// walks the entire tree, blocking /api/services indefinitely. detection must
// use the same skip rules as the recursive walk (collectBundleDirs).
for _, sub := range []string{".Trash", ".git", "node_modules", "vendor"} {
t.Run(sub, func(t *testing.T) {
root := t.TempDir()
writeLocalPactoYAML(t, filepath.Join(root, sub), "svc", "1.0.0")

result := &DetectResult{Diagnostics: &SourceDiagnostics{}}
result.detectLocal(root)

if result.Local != nil {
t.Fatalf("expected local source NOT detected from %q subdir", sub)
}
if len(result.Sources) != 1 {
t.Fatalf("expected 1 source info, got %d", len(result.Sources))
}
if result.Sources[0].Enabled {
t.Errorf("expected local source disabled for %q subdir", sub)
}
})
}
}

func TestDetectLocal_NoPactoYAML(t *testing.T) {
root := t.TempDir()

Expand Down
15 changes: 12 additions & 3 deletions pkg/dashboard/source_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ func localBundleDirs(root string) []string {
return dirs
}

// skipScanDir reports whether a directory name must be excluded from local
// bundle discovery. Hidden directories (e.g. .git, .Trash, .cache) and dependency
// vendoring directories are never project bundle roots, and descending into them
// from a large root (such as $HOME) makes discovery walk an enormous tree. Both
// the recursive walk (collectBundleDirs) and source detection (detectLocal) use
// this so a pacto.yaml under such a directory neither activates nor is scanned.
func skipScanDir(name string) bool {
return strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor"
}

func collectBundleDirs(dir string, depth int, out *[]string) {
if depth > maxLocalScanDepth {
return
Expand All @@ -45,11 +55,10 @@ func collectBundleDirs(dir string, depth int, out *[]string) {
if !e.IsDir() {
continue
}
name := e.Name()
if strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor" {
if skipScanDir(e.Name()) {
continue
}
collectBundleDirs(filepath.Join(dir, name), depth+1, out)
collectBundleDirs(filepath.Join(dir, e.Name()), depth+1, out)
}
}

Expand Down
43 changes: 43 additions & 0 deletions tests/e2e/dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package e2e

import (
"os"
"path/filepath"
"testing"
)

Expand Down Expand Up @@ -58,6 +59,48 @@ func TestDashboardCommand(t *testing.T) {
assertContains(t, output, "enabled")
})

t.Run("hidden subdir does not activate local source", func(t *testing.T) {
// Regression: a pacto.yaml inside a hidden directory (e.g. ~/.Trash) must
// NOT activate the local source. Before the fix, running `pacto dashboard`
// from such a root (notably $HOME) rooted the local source there and
// LocalSource.ListServices recursively walked the entire tree, blocking
// /api/services indefinitely so the dashboard sat on "Loading services…".
//
// Not parallel: modifies process-wide KUBECONFIG and uses inDir.
origKC := os.Getenv("KUBECONFIG")
os.Setenv("KUBECONFIG", "/nonexistent/kubeconfig")
defer func() {
if origKC == "" {
os.Unsetenv("KUBECONFIG")
} else {
os.Setenv("KUBECONFIG", origKC)
}
}()

// Mirror ~/.Trash/pacto.yaml: a pacto.yaml directly inside a hidden
// immediate subdir of the working directory.
root := t.TempDir()
hidden := filepath.Join(root, ".Trash")
if err := os.MkdirAll(hidden, 0o755); err != nil {
t.Fatal(err)
}
contractYAML := "pactoVersion: \"1.0\"\nservice:\n name: trashed\n version: 1.0.0\n"
if err := os.WriteFile(filepath.Join(hidden, "pacto.yaml"), []byte(contractYAML), 0o644); err != nil {
t.Fatal(err)
}
inDir(t, root)

// With local (hidden-only bundle), k8s (invalid kubeconfig), oci (no ref)
// and cache (--no-cache) all unavailable, detection must report no sources
// rather than activating local off the hidden bundle.
output, err := runCommandWithCancelledCtx(t, nil, "dashboard", "--no-cache")
if err == nil {
t.Fatal("expected error: a pacto.yaml in a hidden subdir must not activate any source")
}
assertContains(t, output, "local")
assertContains(t, output, "no pacto.yaml found")
})

t.Run("custom port flag", func(t *testing.T) {
t.Parallel()

Expand Down
Loading