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
This commit is contained in:
Lennart Kats (databricks) 2023-08-25 11:03:42 +02:00 committed by GitHub
parent c5cd20de23
commit a5b86093ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 326 additions and 89 deletions

3
.gitignore vendored
View File

@ -28,3 +28,6 @@ __pycache__
.terraform.lock.hcl .terraform.lock.hcl
.vscode/launch.json .vscode/launch.json
.vscode/tasks.json
.databricks

3
.vscode/__builtins__.pyi vendored Normal file
View File

@ -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 *

View File

@ -7,5 +7,6 @@
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"files.trimFinalNewlines": true, "files.trimFinalNewlines": true,
"python.envFile": "${workspaceFolder}/.databricks/.databricks.env", "python.envFile": "${workspaceFolder}/.databricks/.databricks.env",
"databricks.python.envFile": "${workspaceFolder}/.env" "databricks.python.envFile": "${workspaceFolder}/.env",
"python.analysis.stubPath": ".vscode"
} }

View File

@ -8,7 +8,8 @@ import (
"github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config" "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/jobs"
"github.com/databricks/databricks-sdk-go/service/ml" "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 { func validateProductionMode(ctx context.Context, b *bundle.Bundle, isPrincipalUsed bool) error {
if b.Config.Bundle.Git.Inferred { if b.Config.Bundle.Git.Inferred {
env := b.Config.Bundle.Target 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 r := b.Config.Resources
@ -138,21 +139,6 @@ func validateProductionMode(ctx context.Context, b *bundle.Bundle, isPrincipalUs
return nil 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. // Determines whether run_as is explicitly set for all resources.
// We do this in a best-effort fashion rather than check the top-level // 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. // '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) return transformDevelopmentMode(b)
case config.Production: case config.Production:
isPrincipal, err := isServicePrincipalUsed(ctx, b) isPrincipal := auth.IsServicePrincipal(ctx, b.WorkspaceClient(), b.Config.Workspace.CurrentUser.Id)
if err != nil {
return err
}
return validateProductionMode(ctx, b, isPrincipal) return validateProductionMode(ctx, b, isPrincipal)
case "": case "":
// No action // No action

View File

@ -118,17 +118,6 @@ func TestProcessTargetModeProduction(t *testing.T) {
assert.False(t, bundle.Config.Resources.Pipelines["pipeline1"].PipelineSpec.Development) 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) { func TestProcessTargetModeProductionOkForPrincipal(t *testing.T) {
bundle := mockBundle(config.Production) bundle := mockBundle(config.Production)

View File

@ -1,10 +1,12 @@
package bundle package bundle
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/git" "github.com/databricks/cli/libs/git"
"github.com/databricks/cli/libs/template" "github.com/databricks/cli/libs/template"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -36,9 +38,9 @@ func repoName(url string) string {
func newInitCommand() *cobra.Command { func newInitCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "init TEMPLATE_PATH", Use: "init [TEMPLATE_PATH]",
Short: "Initialize Template", Short: "Initialize Template",
Args: cobra.ExactArgs(1), Args: cobra.MaximumNArgs(1),
} }
var configFile string 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(&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.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 { cmd.RunE = func(cmd *cobra.Command, args []string) error {
templatePath := args[0]
ctx := cmd.Context() 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) { if !isRepoUrl(templatePath) {
// skip downloading the repo because input arg is not a URL. We assume // skip downloading the repo because input arg is not a URL. We assume

View File

@ -113,6 +113,10 @@ TRY_AUTH: // or try picking a config profile dynamically
return nil 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 { func transformLoadError(path string, err error) error {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return fmt.Errorf("no configuration file found at %s; please create one first", path) return fmt.Errorf("no configuration file found at %s; please create one first", path)

View File

@ -43,7 +43,7 @@ func getTarget(cmd *cobra.Command) (value string) {
return target return target
} }
func getProfile(cmd *cobra.Command) (value string) { func GetProfile(cmd *cobra.Command) (value string) {
// The command line flag takes precedence. // The command line flag takes precedence.
flag := cmd.Flag("profile") flag := cmd.Flag("profile")
if flag != nil { if flag != nil {
@ -70,7 +70,7 @@ func loadBundle(cmd *cobra.Command, args []string, load func(ctx context.Context
return nil, nil return nil, nil
} }
profile := getProfile(cmd) profile := GetProfile(cmd)
if profile != "" { if profile != "" {
b.Config.Workspace.Profile = profile b.Config.Workspace.Profile = profile
} }

View File

@ -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
}

View File

@ -1,10 +1,16 @@
package template package template
import ( import (
"context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"regexp" "regexp"
"text/template" "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 { type ErrFail struct {
@ -20,7 +26,11 @@ type pair struct {
v any v any
} }
var helperFuncs = template.FuncMap{ 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) { "fail": func(format string, args ...any) (any, error) {
return nil, ErrFail{fmt.Sprintf(format, args...)} return nil, ErrFail{fmt.Sprintf(format, args...)}
}, },
@ -51,4 +61,52 @@ var helperFuncs = template.FuncMap{
} }
return result 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
},
}
} }

View File

@ -2,9 +2,15 @@ package template
import ( import (
"context" "context"
"os"
"strings" "strings"
"testing" "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/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -13,7 +19,9 @@ func TestTemplatePrintStringWithoutProcessing(t *testing.T) {
ctx := context.Background() ctx := context.Background()
tmpDir := t.TempDir() 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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -28,7 +36,9 @@ func TestTemplateRegexpCompileFunction(t *testing.T) {
ctx := context.Background() ctx := context.Background()
tmpDir := t.TempDir() 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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -44,7 +54,9 @@ func TestTemplateUrlFunction(t *testing.T) {
ctx := context.Background() ctx := context.Background()
tmpDir := t.TempDir() 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) require.NoError(t, err)
@ -59,7 +71,9 @@ func TestTemplateMapPairFunction(t *testing.T) {
ctx := context.Background() ctx := context.Background()
tmpDir := t.TempDir() 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) require.NoError(t, err)
@ -69,3 +83,49 @@ func TestTemplateMapPairFunction(t *testing.T) {
assert.Len(t, r.files, 1) assert.Len(t, r.files, 1)
assert.Equal(t, "false 123 hello 12.3", string(r.files[0].(*inMemoryFile).content)) 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")
}

View File

@ -2,6 +2,10 @@ package template
import ( import (
"context" "context"
"embed"
"io/fs"
"os"
"path"
"path/filepath" "path/filepath"
) )
@ -9,6 +13,9 @@ const libraryDirName = "library"
const templateDirName = "template" const templateDirName = "template"
const schemaFileName = "databricks_template_schema.json" 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 // This function materializes the input templates as a project, using user defined
// configurations. // configurations.
// Parameters: // Parameters:
@ -18,9 +25,21 @@ const schemaFileName = "databricks_template_schema.json"
// templateRoot: root of the template definition // templateRoot: root of the template definition
// outputDir: root of directory where to initialize the template // outputDir: root of directory where to initialize the template
func Materialize(ctx context.Context, configFilePath, templateRoot, outputDir string) error { 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) templatePath := filepath.Join(templateRoot, templateDirName)
libraryPath := filepath.Join(templateRoot, libraryDirName) libraryPath := filepath.Join(templateRoot, libraryDirName)
schemaPath := filepath.Join(templateRoot, schemaFileName) schemaPath := filepath.Join(templateRoot, schemaFileName)
helpers := loadHelpers(ctx)
config, err := newConfig(ctx, schemaPath) config, err := newConfig(ctx, schemaPath)
if err != nil { 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 // 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 { if err != nil {
return err return err
} }
@ -56,5 +75,46 @@ func Materialize(ctx context.Context, configFilePath, templateRoot, outputDir st
if err != nil { if err != nil {
return err 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
} }

View File

@ -57,9 +57,9 @@ type renderer struct {
instanceRoot string 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 // 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 // Load user defined associated templates from the library root
libraryGlob := filepath.Join(libraryRoot, "*") libraryGlob := filepath.Join(libraryRoot, "*")
@ -104,7 +104,7 @@ func (r *renderer) executeTemplate(templateDefinition string) (string, error) {
// Parse the template text // Parse the template text
tmpl, err = tmpl.Parse(templateDefinition) tmpl, err = tmpl.Parse(templateDefinition)
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("error in %s: %w", templateDefinition, err)
} }
// Execute template and get result // Execute template and get result

View File

@ -12,6 +12,7 @@ import (
"testing" "testing"
"text/template" "text/template"
"github.com/databricks/cli/cmd/root"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -31,7 +32,10 @@ func assertFilePermissions(t *testing.T, path string, perm fs.FileMode) {
func TestRendererWithAssociatedTemplateInLibrary(t *testing.T) { func TestRendererWithAssociatedTemplateInLibrary(t *testing.T) {
tmpDir := t.TempDir() 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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -202,9 +206,11 @@ func TestRendererPersistToDisk(t *testing.T) {
func TestRendererWalk(t *testing.T) { func TestRendererWalk(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, nil)
tmpDir := t.TempDir() 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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -241,9 +247,11 @@ func TestRendererWalk(t *testing.T) {
func TestRendererFailFunction(t *testing.T) { func TestRendererFailFunction(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, nil)
tmpDir := t.TempDir() 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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -252,9 +260,11 @@ func TestRendererFailFunction(t *testing.T) {
func TestRendererSkipsDirsEagerly(t *testing.T) { func TestRendererSkipsDirsEagerly(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, nil)
tmpDir := t.TempDir() 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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -267,9 +277,11 @@ func TestRendererSkipsDirsEagerly(t *testing.T) {
func TestRendererSkipAllFilesInCurrentDirectory(t *testing.T) { func TestRendererSkipAllFilesInCurrentDirectory(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, nil)
tmpDir := t.TempDir() 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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -288,9 +300,11 @@ func TestRendererSkipAllFilesInCurrentDirectory(t *testing.T) {
func TestRendererSkipPatternsAreRelativeToFileDirectory(t *testing.T) { func TestRendererSkipPatternsAreRelativeToFileDirectory(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, nil)
tmpDir := t.TempDir() 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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -304,9 +318,11 @@ func TestRendererSkipPatternsAreRelativeToFileDirectory(t *testing.T) {
func TestRendererSkip(t *testing.T) { func TestRendererSkip(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, nil)
tmpDir := t.TempDir() 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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -335,8 +351,10 @@ func TestRendererReadsPermissionsBits(t *testing.T) {
} }
tmpDir := t.TempDir() tmpDir := t.TempDir()
ctx := context.Background() 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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -422,9 +440,11 @@ func TestRendererNoErrorOnConflictingFileIfSkipped(t *testing.T) {
func TestRendererNonTemplatesAreCreatedAsCopyFiles(t *testing.T) { func TestRendererNonTemplatesAreCreatedAsCopyFiles(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, nil)
tmpDir := t.TempDir() 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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -437,12 +457,14 @@ func TestRendererNonTemplatesAreCreatedAsCopyFiles(t *testing.T) {
func TestRendererFileTreeRendering(t *testing.T) { func TestRendererFileTreeRendering(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, nil)
tmpDir := t.TempDir() tmpDir := t.TempDir()
helpers := loadHelpers(ctx)
r, err := newRenderer(ctx, map[string]any{ r, err := newRenderer(ctx, map[string]any{
"dir_name": "my_directory", "dir_name": "my_directory",
"file_name": "my_file", "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) require.NoError(t, err)
err = r.walk() err = r.walk()
@ -462,9 +484,11 @@ func TestRendererFileTreeRendering(t *testing.T) {
func TestRendererSubTemplateInPath(t *testing.T) { func TestRendererSubTemplateInPath(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, nil)
tmpDir := t.TempDir() 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) require.NoError(t, err)
err = r.walk() err = r.walk()

View File

@ -0,0 +1,9 @@
{
"properties": {
"project_name": {
"type": "string",
"default": "my_project",
"description": "Name of the directory"
}
}
}

View File

@ -0,0 +1,3 @@
{
"project_name": "my_project"
}

View File

@ -0,0 +1,3 @@
# {{.project_name}}
The '{{.project_name}}' bundle was generated using the default-python template.

View File

@ -0,0 +1,2 @@
{{workspace_host}}
{{smallest_node_type}}