mirror of https://github.com/databricks/cli.git
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:
parent
c5cd20de23
commit
a5b86093ec
|
@ -28,3 +28,6 @@ __pycache__
|
|||
.terraform.lock.hcl
|
||||
|
||||
.vscode/launch.json
|
||||
.vscode/tasks.json
|
||||
|
||||
.databricks
|
||||
|
|
|
@ -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 *
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"properties": {
|
||||
"project_name": {
|
||||
"type": "string",
|
||||
"default": "my_project",
|
||||
"description": "Name of the directory"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"project_name": "my_project"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# {{.project_name}}
|
||||
|
||||
The '{{.project_name}}' bundle was generated using the default-python template.
|
|
@ -0,0 +1,2 @@
|
|||
{{workspace_host}}
|
||||
{{smallest_node_type}}
|
Loading…
Reference in New Issue