From 964ff19b5b3d0c01a27f0aa6a8b2bb13480cf23b Mon Sep 17 00:00:00 2001 From: Vivek1106-04 Date: Sat, 19 Sep 2026 12:08:30 +0530 Subject: [PATCH 1/2] Reject symlinked directories in templates ## Changes `bundle init` failed with `read : is a directory` when a template's `template/` tree contained a symlink to a directory, after already having created part of the output. `fs.ReadDir` reports entries via `Lstat`, so `IsDir()` is false for a symlink pointing at a directory, and the walker treated it as a file. The failure only surfaced during materialization, when the copied file was read. The walker now resolves symlinked entries and rejects links to directories with an actionable error, before anything is written to disk. Symlinks to files keep working as before. ## Why The previous message described an internal read failure rather than the unsupported construct, and the partially materialized output directory had to be cleaned up by hand. Rejecting is preferred over following the link: a followed symlink can form a cycle (`ln -s . loop`), which would make the breadth-first walk loop forever. Fixes #6391 ## Tests - New unit test in `libs/template/renderer_test.go` covering both symlink kinds (to a file: unchanged, rendered; to a directory: rejected). - New acceptance test `bundle/templates-machinery/symlinked-directory` asserting the error and that no output is left behind. Before this change it produced `Error: read ./template/linked: is a directory` plus a stray `linked` directory. - `go test ./libs/template/...`, `go test ./acceptance -run 'TestAccept/bundle/templates'`, `./task fmt`, `./task ws`, `./task lint`. --- .../databricks_template_schema.json | 1 + .../symlinked-directory/out.test.toml | 4 ++ .../symlinked-directory/output.txt | 4 ++ .../symlinked-directory/script | 9 ++++ .../template/shared/file.txt | 1 + .../symlinked-directory/test.toml | 2 + libs/template/renderer.go | 18 ++++++- libs/template/renderer_test.go | 51 +++++++++++++++++++ 8 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 acceptance/bundle/templates-machinery/symlinked-directory/databricks_template_schema.json create mode 100644 acceptance/bundle/templates-machinery/symlinked-directory/out.test.toml create mode 100644 acceptance/bundle/templates-machinery/symlinked-directory/output.txt create mode 100644 acceptance/bundle/templates-machinery/symlinked-directory/script create mode 100644 acceptance/bundle/templates-machinery/symlinked-directory/template/shared/file.txt create mode 100644 acceptance/bundle/templates-machinery/symlinked-directory/test.toml diff --git a/acceptance/bundle/templates-machinery/symlinked-directory/databricks_template_schema.json b/acceptance/bundle/templates-machinery/symlinked-directory/databricks_template_schema.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/acceptance/bundle/templates-machinery/symlinked-directory/databricks_template_schema.json @@ -0,0 +1 @@ +{} diff --git a/acceptance/bundle/templates-machinery/symlinked-directory/out.test.toml b/acceptance/bundle/templates-machinery/symlinked-directory/out.test.toml new file mode 100644 index 00000000000..56f228d004e --- /dev/null +++ b/acceptance/bundle/templates-machinery/symlinked-directory/out.test.toml @@ -0,0 +1,4 @@ +Cloud = false +GOOS.windows = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/templates-machinery/symlinked-directory/output.txt b/acceptance/bundle/templates-machinery/symlinked-directory/output.txt new file mode 100644 index 00000000000..a33a36010b4 --- /dev/null +++ b/acceptance/bundle/templates-machinery/symlinked-directory/output.txt @@ -0,0 +1,4 @@ +Error: linked: symbolic links to directories are not supported in templates + +>>> find.py linked|shared +template/shared/file.txt diff --git a/acceptance/bundle/templates-machinery/symlinked-directory/script b/acceptance/bundle/templates-machinery/symlinked-directory/script new file mode 100644 index 00000000000..dff8c2db8b2 --- /dev/null +++ b/acceptance/bundle/templates-machinery/symlinked-directory/script @@ -0,0 +1,9 @@ +export NO_COLOR=1 + +# The symlink is created here rather than committed: the acceptance harness copies +# test inputs with filepath.Walk, which reads a symlinked directory as a file. +ln -s shared template/linked + +musterr $CLI bundle init . + +trace find.py 'linked|shared' diff --git a/acceptance/bundle/templates-machinery/symlinked-directory/template/shared/file.txt b/acceptance/bundle/templates-machinery/symlinked-directory/template/shared/file.txt new file mode 100644 index 00000000000..ce013625030 --- /dev/null +++ b/acceptance/bundle/templates-machinery/symlinked-directory/template/shared/file.txt @@ -0,0 +1 @@ +hello diff --git a/acceptance/bundle/templates-machinery/symlinked-directory/test.toml b/acceptance/bundle/templates-machinery/symlinked-directory/test.toml new file mode 100644 index 00000000000..9aeb6cef49f --- /dev/null +++ b/acceptance/bundle/templates-machinery/symlinked-directory/test.toml @@ -0,0 +1,2 @@ +GOOS.windows = false +Ignore = ["template/linked"] diff --git a/libs/template/renderer.go b/libs/template/renderer.go index cce0861c0d2..6704f907419 100644 --- a/libs/template/renderer.go +++ b/libs/template/renderer.go @@ -311,8 +311,24 @@ func (r *renderer) walk() error { continue } + entryPath := path.Join(currentDirectory, entry.Name()) + + // fs.ReadDir reports entries via Lstat, so IsDir is false for a symlink + // pointing at a directory. Such an entry is not readable as a file, and + // following it could traverse a symlink cycle, so reject it here rather + // than fail partway through materializing the template. + if entry.Type()&fs.ModeSymlink != 0 { + info, err := fs.Stat(r.srcFS, entryPath) + if err != nil { + return err + } + if info.IsDir() { + return fmt.Errorf("%s: symbolic links to directories are not supported in templates", entryPath) + } + } + // Generate in memory representation of file - f, err := r.computeFile(path.Join(currentDirectory, entry.Name())) + f, err := r.computeFile(entryPath) if err != nil { return err } diff --git a/libs/template/renderer_test.go b/libs/template/renderer_test.go index 64ffdb6dbdc..e938af108f7 100644 --- a/libs/template/renderer_test.go +++ b/libs/template/renderer_test.go @@ -419,6 +419,57 @@ func TestRendererWalk(t *testing.T) { assert.Equal(t, "file four", getContent(r, "dir2/file4")) } +func TestRendererWalkSymlinks(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlink creation needs privilege on Windows") + } + + tests := []struct { + name string + target string + expectedErr string + }{ + { + name: "symlink to file", + target: "shared/file.txt", + }, + { + name: "symlink to directory", + target: "shared", + expectedErr: "linked: symbolic links to directories are not supported in templates", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := t.Context() + ctx = cmdctx.SetWorkspaceClient(ctx, nil) + + templateDir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(templateDir, "template", "shared"), 0o755)) + require.NoError(t, os.MkdirAll(filepath.Join(templateDir, "library"), 0o755)) + testutil.WriteFile(t, filepath.Join(templateDir, "template", "shared", "file.txt"), "hello") + require.NoError(t, os.Symlink(tt.target, filepath.Join(templateDir, "template", "linked"))) + + r, err := newRenderer(ctx, nil, nil, os.DirFS(templateDir), "template", "library") + require.NoError(t, err) + + err = r.walk() + if tt.expectedErr != "" { + assert.EqualError(t, err, tt.expectedErr) + return + } + + require.NoError(t, err) + paths := make([]string, 0, len(r.files)) + for _, f := range r.files { + paths = append(paths, f.RelPath()) + } + assert.Equal(t, []string{"linked", "shared/file.txt"}, paths) + }) + } +} + func TestRendererFailFunction(t *testing.T) { ctx := t.Context() ctx = cmdctx.SetWorkspaceClient(ctx, nil) From 3e98fc3e270ff1bc96eed1018f7df466b68d461d Mon Sep 17 00:00:00 2001 From: Vivek1106-04 Date: Sat, 19 Sep 2026 12:11:33 +0530 Subject: [PATCH 2/2] Add changelog fragment --- .nextchanges/bundles/template-symlinked-directory.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nextchanges/bundles/template-symlinked-directory.md diff --git a/.nextchanges/bundles/template-symlinked-directory.md b/.nextchanges/bundles/template-symlinked-directory.md new file mode 100644 index 00000000000..70d04910b9b --- /dev/null +++ b/.nextchanges/bundles/template-symlinked-directory.md @@ -0,0 +1 @@ +* Report a clear error for symlinked directories in `bundle init` templates instead of failing partway through with `is a directory`. ([#6758](https://github.com/databricks/cli/pull/6758))