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)) 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)