From a5b86093ecc15989bf8473699e94a2518017488a Mon Sep 17 00:00:00 2001 From: "Lennart Kats (databricks)" Date: Fri, 25 Aug 2023 11:03:42 +0200 Subject: [PATCH] Add a foundation for built-in templates (#685) ## Changes This pull request extends the templating support in preparation of a new, default template (WIP, https://github.com/databricks/cli/pull/686): * builtin templates that can be initialized using e.g. `databricks bundle init default-python` * builtin templates are embedded into the executable using go's `embed` functionality, making sure they're co-versioned with the CLI * new helpers to get the workspace name, current user name, etc. help craft a complete template * (not enabled yet) when the user types `databricks bundle init` they can interactively select the `default-python` template And makes two tangentially related changes: * IsServicePrincipal now uses the "users" API rather than the "principals" API, since the latter is too slow for our purposes. * mode: prod no longer requires the 'target.prod.git' setting. It's hard to set that from a template. (Pieter is planning an overhaul of warnings support; this would be one of the first warnings we show.) The actual `default-python` template is maintained in a separate PR: https://github.com/databricks/cli/pull/686 ## Tests Unit tests, manual testing --- .gitignore | 3 + .vscode/__builtins__.pyi | 3 + .vscode/settings.json | 3 +- bundle/config/mutator/process_target_mode.go | 25 +--- .../mutator/process_target_mode_test.go | 11 -- cmd/bundle/init.go | 25 +++- cmd/root/auth.go | 4 + cmd/root/bundle.go | 4 +- libs/auth/service_principal.go | 16 +++ libs/template/helpers.go | 120 +++++++++++++----- libs/template/helpers_test.go | 68 +++++++++- libs/template/materialize.go | 64 +++++++++- libs/template/renderer.go | 6 +- libs/template/renderer_test.go | 46 +++++-- .../databricks_template_schema.json | 9 ++ .../templates/default-python/defaults.json | 3 + .../template/{{.project_name}}/README.md | 3 + .../workspace-host/template/file.tmpl | 2 + 18 files changed, 326 insertions(+), 89 deletions(-) create mode 100644 .vscode/__builtins__.pyi create mode 100644 libs/auth/service_principal.go create mode 100644 libs/template/templates/default-python/databricks_template_schema.json create mode 100644 libs/template/templates/default-python/defaults.json create mode 100644 libs/template/templates/default-python/template/{{.project_name}}/README.md create mode 100644 libs/template/testdata/workspace-host/template/file.tmpl diff --git a/.gitignore b/.gitignore index 5f00a82b..edd1409a 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ __pycache__ .terraform.lock.hcl .vscode/launch.json +.vscode/tasks.json + +.databricks diff --git a/.vscode/__builtins__.pyi b/.vscode/__builtins__.pyi new file mode 100644 index 00000000..81f9a49e --- /dev/null +++ b/.vscode/__builtins__.pyi @@ -0,0 +1,3 @@ +# Typings for Pylance in VS Code +# see https://github.com/microsoft/pyright/blob/main/docs/builtins.md +from databricks.sdk.runtime import * diff --git a/.vscode/settings.json b/.vscode/settings.json index 76be94af..687e0fc0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,6 @@ "files.insertFinalNewline": true, "files.trimFinalNewlines": true, "python.envFile": "${workspaceFolder}/.databricks/.databricks.env", - "databricks.python.envFile": "${workspaceFolder}/.env" + "databricks.python.envFile": "${workspaceFolder}/.env", + "python.analysis.stubPath": ".vscode" } diff --git a/bundle/config/mutator/process_target_mode.go b/bundle/config/mutator/process_target_mode.go index fca4e4b0..3a00d42f 100644 --- a/bundle/config/mutator/process_target_mode.go +++ b/bundle/config/mutator/process_target_mode.go @@ -8,7 +8,8 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" - "github.com/databricks/databricks-sdk-go/service/iam" + "github.com/databricks/cli/libs/auth" + "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go/service/jobs" "github.com/databricks/databricks-sdk-go/service/ml" ) @@ -111,7 +112,7 @@ func findIncorrectPath(b *bundle.Bundle, mode config.Mode) string { func validateProductionMode(ctx context.Context, b *bundle.Bundle, isPrincipalUsed bool) error { if b.Config.Bundle.Git.Inferred { env := b.Config.Bundle.Target - return fmt.Errorf("target with 'mode: production' must specify an explicit 'targets.%s.git' configuration", env) + log.Warnf(ctx, "target with 'mode: production' should specify an explicit 'targets.%s.git' configuration", env) } r := b.Config.Resources @@ -138,21 +139,6 @@ func validateProductionMode(ctx context.Context, b *bundle.Bundle, isPrincipalUs return nil } -// Determines whether a service principal identity is used to run the CLI. -func isServicePrincipalUsed(ctx context.Context, b *bundle.Bundle) (bool, error) { - ws := b.WorkspaceClient() - - // Check if a principal with the current user's ID exists. - // We need to use the ListAll method since Get is only usable by admins. - matches, err := ws.ServicePrincipals.ListAll(ctx, iam.ListServicePrincipalsRequest{ - Filter: "id eq " + b.Config.Workspace.CurrentUser.Id, - }) - if err != nil { - return false, err - } - return len(matches) > 0, nil -} - // Determines whether run_as is explicitly set for all resources. // We do this in a best-effort fashion rather than check the top-level // 'run_as' field because the latter is not required to be set. @@ -174,10 +160,7 @@ func (m *processTargetMode) Apply(ctx context.Context, b *bundle.Bundle) error { } return transformDevelopmentMode(b) case config.Production: - isPrincipal, err := isServicePrincipalUsed(ctx, b) - if err != nil { - return err - } + isPrincipal := auth.IsServicePrincipal(ctx, b.WorkspaceClient(), b.Config.Workspace.CurrentUser.Id) return validateProductionMode(ctx, b, isPrincipal) case "": // No action diff --git a/bundle/config/mutator/process_target_mode_test.go b/bundle/config/mutator/process_target_mode_test.go index 76db64de..489632e1 100644 --- a/bundle/config/mutator/process_target_mode_test.go +++ b/bundle/config/mutator/process_target_mode_test.go @@ -118,17 +118,6 @@ func TestProcessTargetModeProduction(t *testing.T) { assert.False(t, bundle.Config.Resources.Pipelines["pipeline1"].PipelineSpec.Development) } -func TestProcessTargetModeProductionGit(t *testing.T) { - bundle := mockBundle(config.Production) - - // Pretend the user didn't set Git configuration explicitly - bundle.Config.Bundle.Git.Inferred = true - - err := validateProductionMode(context.Background(), bundle, false) - require.ErrorContains(t, err, "git") - bundle.Config.Bundle.Git.Inferred = false -} - func TestProcessTargetModeProductionOkForPrincipal(t *testing.T) { bundle := mockBundle(config.Production) diff --git a/cmd/bundle/init.go b/cmd/bundle/init.go index 9ba7e190..2127a7bc 100644 --- a/cmd/bundle/init.go +++ b/cmd/bundle/init.go @@ -1,10 +1,12 @@ package bundle import ( + "errors" "os" "path/filepath" "strings" + "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/libs/git" "github.com/databricks/cli/libs/template" "github.com/spf13/cobra" @@ -36,9 +38,9 @@ func repoName(url string) string { func newInitCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "init TEMPLATE_PATH", + Use: "init [TEMPLATE_PATH]", Short: "Initialize Template", - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), } var configFile string @@ -48,9 +50,26 @@ func newInitCommand() *cobra.Command { cmd.Flags().StringVar(&templateDir, "template-dir", "", "Directory within repository that holds the template specification.") cmd.Flags().StringVar(&outputDir, "output-dir", "", "Directory to write the initialized template to.") + cmd.PreRunE = root.MustWorkspaceClient cmd.RunE = func(cmd *cobra.Command, args []string) error { - templatePath := args[0] ctx := cmd.Context() + var templatePath string + if len(args) > 0 { + templatePath = args[0] + } else { + return errors.New("please specify a template") + + /* TODO: propose to use default-python (once template is ready) + var err error + if !cmdio.IsOutTTY(ctx) || !cmdio.IsInTTY(ctx) { + return errors.New("please specify a template") + } + templatePath, err = cmdio.Ask(ctx, "Template to use", "default-python") + if err != nil { + return err + } + */ + } if !isRepoUrl(templatePath) { // skip downloading the repo because input arg is not a URL. We assume diff --git a/cmd/root/auth.go b/cmd/root/auth.go index 2f32d260..e56074ef 100644 --- a/cmd/root/auth.go +++ b/cmd/root/auth.go @@ -113,6 +113,10 @@ TRY_AUTH: // or try picking a config profile dynamically return nil } +func SetWorkspaceClient(ctx context.Context, w *databricks.WorkspaceClient) context.Context { + return context.WithValue(ctx, &workspaceClient, w) +} + func transformLoadError(path string, err error) error { if os.IsNotExist(err) { return fmt.Errorf("no configuration file found at %s; please create one first", path) diff --git a/cmd/root/bundle.go b/cmd/root/bundle.go index e1c12336..ba7a5dfd 100644 --- a/cmd/root/bundle.go +++ b/cmd/root/bundle.go @@ -43,7 +43,7 @@ func getTarget(cmd *cobra.Command) (value string) { return target } -func getProfile(cmd *cobra.Command) (value string) { +func GetProfile(cmd *cobra.Command) (value string) { // The command line flag takes precedence. flag := cmd.Flag("profile") if flag != nil { @@ -70,7 +70,7 @@ func loadBundle(cmd *cobra.Command, args []string, load func(ctx context.Context return nil, nil } - profile := getProfile(cmd) + profile := GetProfile(cmd) if profile != "" { b.Config.Workspace.Profile = profile } diff --git a/libs/auth/service_principal.go b/libs/auth/service_principal.go new file mode 100644 index 00000000..58fcc6a7 --- /dev/null +++ b/libs/auth/service_principal.go @@ -0,0 +1,16 @@ +package auth + +import ( + "context" + + "github.com/databricks/databricks-sdk-go" +) + +// Determines whether a given user id is a service principal. +// This function uses a heuristic: if no user exists with this id, we assume +// it's a service principal. Unfortunately, the standard service principal API is too +// slow for our purposes. +func IsServicePrincipal(ctx context.Context, ws *databricks.WorkspaceClient, userId string) bool { + _, err := ws.Users.GetById(ctx, userId) + return err != nil +} diff --git a/libs/template/helpers.go b/libs/template/helpers.go index ac846658..b8f2fe45 100644 --- a/libs/template/helpers.go +++ b/libs/template/helpers.go @@ -1,10 +1,16 @@ package template import ( + "context" + "errors" "fmt" "net/url" "regexp" "text/template" + + "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/auth" + "github.com/databricks/databricks-sdk-go/service/iam" ) type ErrFail struct { @@ -20,35 +26,87 @@ type pair struct { v any } -var helperFuncs = template.FuncMap{ - "fail": func(format string, args ...any) (any, error) { - return nil, ErrFail{fmt.Sprintf(format, args...)} - }, - // Alias for https://pkg.go.dev/net/url#Parse. Allows usage of all methods of url.URL - "url": func(rawUrl string) (*url.URL, error) { - return url.Parse(rawUrl) - }, - // Alias for https://pkg.go.dev/regexp#Compile. Allows usage of all methods of regexp.Regexp - "regexp": func(expr string) (*regexp.Regexp, error) { - return regexp.Compile(expr) - }, - // A key value pair. This is used with the map function to generate maps - // to use inside a template - "pair": func(k string, v any) pair { - return pair{k, v} - }, - // map converts a list of pairs to a map object. This is useful to pass multiple - // objects to templates defined in the library directory. Go text template - // syntax for invoking a template only allows specifying a single argument, - // this function can be used to workaround that limitation. - // - // For example: {{template "my_template" (map (pair "foo" $arg1) (pair "bar" $arg2))}} - // $arg1 and $arg2 can be referred from inside "my_template" as ".foo" and ".bar" - "map": func(pairs ...pair) map[string]any { - result := make(map[string]any, 0) - for _, p := range pairs { - result[p.k] = p.v - } - return result - }, +func loadHelpers(ctx context.Context) template.FuncMap { + var user *iam.User + var is_service_principal *bool + w := root.WorkspaceClient(ctx) + return template.FuncMap{ + "fail": func(format string, args ...any) (any, error) { + return nil, ErrFail{fmt.Sprintf(format, args...)} + }, + // Alias for https://pkg.go.dev/net/url#Parse. Allows usage of all methods of url.URL + "url": func(rawUrl string) (*url.URL, error) { + return url.Parse(rawUrl) + }, + // Alias for https://pkg.go.dev/regexp#Compile. Allows usage of all methods of regexp.Regexp + "regexp": func(expr string) (*regexp.Regexp, error) { + return regexp.Compile(expr) + }, + // A key value pair. This is used with the map function to generate maps + // to use inside a template + "pair": func(k string, v any) pair { + return pair{k, v} + }, + // map converts a list of pairs to a map object. This is useful to pass multiple + // objects to templates defined in the library directory. Go text template + // syntax for invoking a template only allows specifying a single argument, + // this function can be used to workaround that limitation. + // + // For example: {{template "my_template" (map (pair "foo" $arg1) (pair "bar" $arg2))}} + // $arg1 and $arg2 can be referred from inside "my_template" as ".foo" and ".bar" + "map": func(pairs ...pair) map[string]any { + result := make(map[string]any, 0) + for _, p := range pairs { + result[p.k] = p.v + } + return result + }, + // Get smallest node type (follows Terraform's GetSmallestNodeType) + "smallest_node_type": func() (string, error) { + if w.Config.Host == "" { + return "", errors.New("cannot determine target workspace, please first setup a configuration profile using 'databricks auth login'") + } + if w.Config.IsAzure() { + return "Standard_D3_v2", nil + } else if w.Config.IsGcp() { + return "n1-standard-4", nil + } + return "i3.xlarge", nil + }, + "workspace_host": func() (string, error) { + if w.Config.Host == "" { + return "", errors.New("cannot determine target workspace, please first setup a configuration profile using 'databricks auth login'") + } + return w.Config.Host, nil + }, + "user_name": func() (string, error) { + if user == nil { + var err error + user, err = w.CurrentUser.Me(ctx) + if err != nil { + return "", err + } + } + result := user.UserName + if result == "" { + result = user.Id + } + return result, nil + }, + "is_service_principal": func() (bool, error) { + if is_service_principal != nil { + return *is_service_principal, nil + } + if user == nil { + var err error + user, err = w.CurrentUser.Me(ctx) + if err != nil { + return false, err + } + } + result := auth.IsServicePrincipal(ctx, w, user.Id) + is_service_principal = &result + return result, nil + }, + } } diff --git a/libs/template/helpers_test.go b/libs/template/helpers_test.go index 023eed29..d495ae89 100644 --- a/libs/template/helpers_test.go +++ b/libs/template/helpers_test.go @@ -2,9 +2,15 @@ package template import ( "context" + "os" "strings" "testing" + "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/flags" + "github.com/databricks/databricks-sdk-go" + workspaceConfig "github.com/databricks/databricks-sdk-go/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -13,7 +19,9 @@ func TestTemplatePrintStringWithoutProcessing(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/print-without-processing/template", "./testdata/print-without-processing/library", tmpDir) + ctx = root.SetWorkspaceClient(ctx, nil) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/print-without-processing/template", "./testdata/print-without-processing/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -28,7 +36,9 @@ func TestTemplateRegexpCompileFunction(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/regexp-compile/template", "./testdata/regexp-compile/library", tmpDir) + ctx = root.SetWorkspaceClient(ctx, nil) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/regexp-compile/template", "./testdata/regexp-compile/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -44,7 +54,9 @@ func TestTemplateUrlFunction(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/urlparse-function/template", "./testdata/urlparse-function/library", tmpDir) + ctx = root.SetWorkspaceClient(ctx, nil) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/urlparse-function/template", "./testdata/urlparse-function/library", tmpDir) require.NoError(t, err) @@ -59,7 +71,9 @@ func TestTemplateMapPairFunction(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/map-pair/template", "./testdata/map-pair/library", tmpDir) + ctx = root.SetWorkspaceClient(ctx, nil) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/map-pair/template", "./testdata/map-pair/library", tmpDir) require.NoError(t, err) @@ -69,3 +83,49 @@ func TestTemplateMapPairFunction(t *testing.T) { assert.Len(t, r.files, 1) assert.Equal(t, "false 123 hello 12.3", string(r.files[0].(*inMemoryFile).content)) } + +func TestWorkspaceHost(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + + w := &databricks.WorkspaceClient{ + Config: &workspaceConfig.Config{ + Host: "https://myhost.com", + }, + } + ctx = root.SetWorkspaceClient(ctx, w) + + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/workspace-host/template", "./testdata/map-pair/library", tmpDir) + + require.NoError(t, err) + + err = r.walk() + assert.NoError(t, err) + + assert.Len(t, r.files, 1) + assert.Contains(t, string(r.files[0].(*inMemoryFile).content), "https://myhost.com") + assert.Contains(t, string(r.files[0].(*inMemoryFile).content), "i3.xlarge") + +} + +func TestWorkspaceHostNotConfigured(t *testing.T) { + ctx := context.Background() + cmd := cmdio.NewIO(flags.OutputJSON, strings.NewReader(""), os.Stdout, os.Stderr, "template") + ctx = cmdio.InContext(ctx, cmd) + tmpDir := t.TempDir() + + w := &databricks.WorkspaceClient{ + Config: &workspaceConfig.Config{}, + } + ctx = root.SetWorkspaceClient(ctx, w) + + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/workspace-host/template", "./testdata/map-pair/library", tmpDir) + + assert.NoError(t, err) + + err = r.walk() + require.ErrorContains(t, err, "cannot determine target workspace") + +} diff --git a/libs/template/materialize.go b/libs/template/materialize.go index 426646c3..5422160d 100644 --- a/libs/template/materialize.go +++ b/libs/template/materialize.go @@ -2,6 +2,10 @@ package template import ( "context" + "embed" + "io/fs" + "os" + "path" "path/filepath" ) @@ -9,6 +13,9 @@ const libraryDirName = "library" const templateDirName = "template" const schemaFileName = "databricks_template_schema.json" +//go:embed all:templates +var builtinTemplates embed.FS + // This function materializes the input templates as a project, using user defined // configurations. // Parameters: @@ -18,9 +25,21 @@ const schemaFileName = "databricks_template_schema.json" // templateRoot: root of the template definition // outputDir: root of directory where to initialize the template func Materialize(ctx context.Context, configFilePath, templateRoot, outputDir string) error { + // Use a temporary directory in case any builtin templates like default-python are used + tempDir, err := os.MkdirTemp("", "templates") + defer os.RemoveAll(tempDir) + if err != nil { + return err + } + templateRoot, err = prepareBuiltinTemplates(templateRoot, tempDir) + if err != nil { + return err + } + templatePath := filepath.Join(templateRoot, templateDirName) libraryPath := filepath.Join(templateRoot, libraryDirName) schemaPath := filepath.Join(templateRoot, schemaFileName) + helpers := loadHelpers(ctx) config, err := newConfig(ctx, schemaPath) if err != nil { @@ -48,7 +67,7 @@ func Materialize(ctx context.Context, configFilePath, templateRoot, outputDir st } // Walk and render the template, since input configuration is complete - r, err := newRenderer(ctx, config.values, templatePath, libraryPath, outputDir) + r, err := newRenderer(ctx, config.values, helpers, templatePath, libraryPath, outputDir) if err != nil { return err } @@ -56,5 +75,46 @@ func Materialize(ctx context.Context, configFilePath, templateRoot, outputDir st if err != nil { return err } - return r.persistToDisk() + + err = r.persistToDisk() + if err != nil { + return err + } + println("✨ Successfully initialized template") + return nil +} + +// If the given templateRoot matches +func prepareBuiltinTemplates(templateRoot string, tempDir string) (string, error) { + _, err := fs.Stat(builtinTemplates, path.Join("templates", templateRoot)) + if err != nil { + // The given path doesn't appear to be using out built-in templates + return templateRoot, nil + } + + // We have a built-in template with the same name as templateRoot! + // Now we need to make a fully copy of the builtin templates to a real file system + // since template.Parse() doesn't support embed.FS. + err = fs.WalkDir(builtinTemplates, "templates", func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + + targetPath := filepath.Join(tempDir, path) + if entry.IsDir() { + return os.Mkdir(targetPath, 0755) + } else { + content, err := fs.ReadFile(builtinTemplates, path) + if err != nil { + return err + } + return os.WriteFile(targetPath, content, 0644) + } + }) + + if err != nil { + return "", err + } + + return filepath.Join(tempDir, "templates", templateRoot), nil } diff --git a/libs/template/renderer.go b/libs/template/renderer.go index 9be1b58e..f4bd99d2 100644 --- a/libs/template/renderer.go +++ b/libs/template/renderer.go @@ -57,9 +57,9 @@ type renderer struct { instanceRoot string } -func newRenderer(ctx context.Context, config map[string]any, templateRoot, libraryRoot, instanceRoot string) (*renderer, error) { +func newRenderer(ctx context.Context, config map[string]any, helpers template.FuncMap, templateRoot, libraryRoot, instanceRoot string) (*renderer, error) { // Initialize new template, with helper functions loaded - tmpl := template.New("").Funcs(helperFuncs) + tmpl := template.New("").Funcs(helpers) // Load user defined associated templates from the library root libraryGlob := filepath.Join(libraryRoot, "*") @@ -104,7 +104,7 @@ func (r *renderer) executeTemplate(templateDefinition string) (string, error) { // Parse the template text tmpl, err = tmpl.Parse(templateDefinition) if err != nil { - return "", err + return "", fmt.Errorf("error in %s: %w", templateDefinition, err) } // Execute template and get result diff --git a/libs/template/renderer_test.go b/libs/template/renderer_test.go index f3f7f234..a2e5675e 100644 --- a/libs/template/renderer_test.go +++ b/libs/template/renderer_test.go @@ -12,6 +12,7 @@ import ( "testing" "text/template" + "github.com/databricks/cli/cmd/root" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -31,7 +32,10 @@ func assertFilePermissions(t *testing.T, path string, perm fs.FileMode) { func TestRendererWithAssociatedTemplateInLibrary(t *testing.T) { tmpDir := t.TempDir() - r, err := newRenderer(context.Background(), nil, "./testdata/email/template", "./testdata/email/library", tmpDir) + ctx := context.Background() + ctx = root.SetWorkspaceClient(ctx, nil) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/email/template", "./testdata/email/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -202,9 +206,11 @@ func TestRendererPersistToDisk(t *testing.T) { func TestRendererWalk(t *testing.T) { ctx := context.Background() + ctx = root.SetWorkspaceClient(ctx, nil) tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/walk/template", "./testdata/walk/library", tmpDir) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/walk/template", "./testdata/walk/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -241,9 +247,11 @@ func TestRendererWalk(t *testing.T) { func TestRendererFailFunction(t *testing.T) { ctx := context.Background() + ctx = root.SetWorkspaceClient(ctx, nil) tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/fail/template", "./testdata/fail/library", tmpDir) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/fail/template", "./testdata/fail/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -252,9 +260,11 @@ func TestRendererFailFunction(t *testing.T) { func TestRendererSkipsDirsEagerly(t *testing.T) { ctx := context.Background() + ctx = root.SetWorkspaceClient(ctx, nil) tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/skip-dir-eagerly/template", "./testdata/skip-dir-eagerly/library", tmpDir) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/skip-dir-eagerly/template", "./testdata/skip-dir-eagerly/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -267,9 +277,11 @@ func TestRendererSkipsDirsEagerly(t *testing.T) { func TestRendererSkipAllFilesInCurrentDirectory(t *testing.T) { ctx := context.Background() + ctx = root.SetWorkspaceClient(ctx, nil) tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/skip-all-files-in-cwd/template", "./testdata/skip-all-files-in-cwd/library", tmpDir) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/skip-all-files-in-cwd/template", "./testdata/skip-all-files-in-cwd/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -288,9 +300,11 @@ func TestRendererSkipAllFilesInCurrentDirectory(t *testing.T) { func TestRendererSkipPatternsAreRelativeToFileDirectory(t *testing.T) { ctx := context.Background() + ctx = root.SetWorkspaceClient(ctx, nil) tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/skip-is-relative/template", "./testdata/skip-is-relative/library", tmpDir) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/skip-is-relative/template", "./testdata/skip-is-relative/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -304,9 +318,11 @@ func TestRendererSkipPatternsAreRelativeToFileDirectory(t *testing.T) { func TestRendererSkip(t *testing.T) { ctx := context.Background() + ctx = root.SetWorkspaceClient(ctx, nil) tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/skip/template", "./testdata/skip/library", tmpDir) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/skip/template", "./testdata/skip/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -335,8 +351,10 @@ func TestRendererReadsPermissionsBits(t *testing.T) { } tmpDir := t.TempDir() ctx := context.Background() + ctx = root.SetWorkspaceClient(ctx, nil) - r, err := newRenderer(ctx, nil, "./testdata/executable-bit-read/template", "./testdata/executable-bit-read/library", tmpDir) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/executable-bit-read/template", "./testdata/executable-bit-read/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -422,9 +440,11 @@ func TestRendererNoErrorOnConflictingFileIfSkipped(t *testing.T) { func TestRendererNonTemplatesAreCreatedAsCopyFiles(t *testing.T) { ctx := context.Background() + ctx = root.SetWorkspaceClient(ctx, nil) tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/copy-file-walk/template", "./testdata/copy-file-walk/library", tmpDir) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/copy-file-walk/template", "./testdata/copy-file-walk/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -437,12 +457,14 @@ func TestRendererNonTemplatesAreCreatedAsCopyFiles(t *testing.T) { func TestRendererFileTreeRendering(t *testing.T) { ctx := context.Background() + ctx = root.SetWorkspaceClient(ctx, nil) tmpDir := t.TempDir() + helpers := loadHelpers(ctx) r, err := newRenderer(ctx, map[string]any{ "dir_name": "my_directory", "file_name": "my_file", - }, "./testdata/file-tree-rendering/template", "./testdata/file-tree-rendering/library", tmpDir) + }, helpers, "./testdata/file-tree-rendering/template", "./testdata/file-tree-rendering/library", tmpDir) require.NoError(t, err) err = r.walk() @@ -462,9 +484,11 @@ func TestRendererFileTreeRendering(t *testing.T) { func TestRendererSubTemplateInPath(t *testing.T) { ctx := context.Background() + ctx = root.SetWorkspaceClient(ctx, nil) tmpDir := t.TempDir() - r, err := newRenderer(ctx, nil, "./testdata/template-in-path/template", "./testdata/template-in-path/library", tmpDir) + helpers := loadHelpers(ctx) + r, err := newRenderer(ctx, nil, helpers, "./testdata/template-in-path/template", "./testdata/template-in-path/library", tmpDir) require.NoError(t, err) err = r.walk() diff --git a/libs/template/templates/default-python/databricks_template_schema.json b/libs/template/templates/default-python/databricks_template_schema.json new file mode 100644 index 00000000..b680c5fb --- /dev/null +++ b/libs/template/templates/default-python/databricks_template_schema.json @@ -0,0 +1,9 @@ +{ + "properties": { + "project_name": { + "type": "string", + "default": "my_project", + "description": "Name of the directory" + } + } +} diff --git a/libs/template/templates/default-python/defaults.json b/libs/template/templates/default-python/defaults.json new file mode 100644 index 00000000..99ecd36d --- /dev/null +++ b/libs/template/templates/default-python/defaults.json @@ -0,0 +1,3 @@ +{ + "project_name": "my_project" +} diff --git a/libs/template/templates/default-python/template/{{.project_name}}/README.md b/libs/template/templates/default-python/template/{{.project_name}}/README.md new file mode 100644 index 00000000..3187b9ed --- /dev/null +++ b/libs/template/templates/default-python/template/{{.project_name}}/README.md @@ -0,0 +1,3 @@ +# {{.project_name}} + +The '{{.project_name}}' bundle was generated using the default-python template. diff --git a/libs/template/testdata/workspace-host/template/file.tmpl b/libs/template/testdata/workspace-host/template/file.tmpl new file mode 100644 index 00000000..2098e41b --- /dev/null +++ b/libs/template/testdata/workspace-host/template/file.tmpl @@ -0,0 +1,2 @@ +{{workspace_host}} +{{smallest_node_type}}