Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/bundle/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ func newDebugCommand() *cobra.Command {
cmd.AddCommand(debug.NewTerraformCommand())
cmd.AddCommand(debug.NewRefSchemaCommand())
cmd.AddCommand(debug.NewStatesCommand())
cmd.AddCommand(debug.NewRenderTemplateSchemaCommand())
return cmd
}
71 changes: 71 additions & 0 deletions cmd/bundle/debug/render_template_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package debug

import (
"errors"
"fmt"

"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/template"
"github.com/spf13/cobra"
)

func NewRenderTemplateSchemaCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "render-template-schema [TEMPLATE_PATH]",
Short: "Render a template schema with provided input values",
Args: root.MaximumNArgs(1),
Hidden: true,
}

var inputFile string
var templateDir string
var tag string
var branch string

cmd.Flags().StringVar(&inputFile, "input-file", "", "JSON file containing key value pairs of input parameters required for template schema rendering.")
cmd.Flags().StringVar(&templateDir, "template-dir", "", "Directory path within a Git repository containing the template.")
cmd.Flags().StringVar(&tag, "tag", "", "Git tag to use for template initialization")
cmd.Flags().StringVar(&branch, "branch", "", "Git branch to use for template initialization")

cmd.PreRunE = root.MustWorkspaceClient

cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

if len(args) == 0 {
return errors.New("template path is required")
}
templatePathOrUrl := args[0]

// Resolve git ref from tag/branch flags
ref := branch
if tag != "" {
ref = tag
}

// Resolve the template reader
reader, isGitReader := template.ResolveReader(templatePathOrUrl, templateDir, ref)
defer reader.Cleanup(ctx)

// For git reader, load schema first to initialize the temp directory
if isGitReader {
_, _, err := reader.LoadSchemaAndTemplateFS(ctx)
if err != nil {
return err
}
}

// Render the schema
result, err := template.RenderSchema(ctx, reader, template.RenderSchemaInput{
InputFile: inputFile,
})
if err != nil {
return err
}

_, err = fmt.Fprintln(cmd.OutOrStdout(), result.Content)
return err
}

return cmd
}
51 changes: 51 additions & 0 deletions libs/template/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ type Reader interface {
// LoadSchemaAndTemplateFS loads and returns the schema and template filesystem.
LoadSchemaAndTemplateFS(ctx context.Context) (*jsonschema.Schema, fs.FS, error)

// This may be different from the template FS returned by LoadSchemaAndTemplateFS
// when template_dir is set in the schema.
SchemaFS(ctx context.Context) (fs.FS, error)

// Cleanup releases any resources associated with the reader
// like cleaning up temporary directories.
Cleanup(ctx context.Context)
Expand Down Expand Up @@ -70,6 +74,21 @@ func (r *builtinReader) LoadSchemaAndTemplateFS(ctx context.Context) (*jsonschem
return nil, nil, fmt.Errorf("template directory %s (referenced by %s) not found", templateDirName, r.name)
}

func (r *builtinReader) SchemaFS(ctx context.Context) (fs.FS, error) {
builtin, err := builtin()
if err != nil {
return nil, err
}

for _, entry := range builtin {
if entry.Name == r.name {
return entry.FS, nil
}
}

return nil, fmt.Errorf("builtin template %s not found", r.name)
}

func (r *builtinReader) Cleanup(ctx context.Context) {}

// gitReader reads a template from a git repository.
Expand All @@ -87,6 +106,21 @@ type gitReader struct {
cloneFunc func(ctx context.Context, url, reference, targetPath string) error
}

// NewGitReader creates a new reader for a git repository template.
func NewGitReader(gitUrl, ref, templateDir string, cloneFunc func(ctx context.Context, url, reference, targetPath string) error) Reader {
return &gitReader{
gitUrl: gitUrl,
ref: ref,
templateDir: templateDir,
cloneFunc: cloneFunc,
}
}

// NewBuiltinReader creates a new reader for a built-in template.
func NewBuiltinReader(name string) Reader {
return &builtinReader{name: name}
}

// Computes the repo name from the repo URL. Treats the last non empty word
// when splitting at '/' as the repo name. For example: for url git@github.com:databricks/cli.git
// the name would be "cli.git"
Expand Down Expand Up @@ -125,6 +159,14 @@ func (r *gitReader) LoadSchemaAndTemplateFS(ctx context.Context) (*jsonschema.Sc
return loadSchemaAndResolveTemplateDir(templateDir)
}

func (r *gitReader) SchemaFS(ctx context.Context) (fs.FS, error) {
if r.tmpRepoDir == "" {
return nil, errors.New("must call LoadSchemaAndTemplateFS before SchemaFS")
}
templateDir := filepath.Join(r.tmpRepoDir, r.templateDir)
return os.DirFS(templateDir), nil
}

func (r *gitReader) Cleanup(ctx context.Context) {
if r.tmpRepoDir == "" {
return
Expand All @@ -143,10 +185,19 @@ type localReader struct {
path string
}

// NewLocalReader creates a new reader for a local template directory.
func NewLocalReader(path string) Reader {
return &localReader{path: path}
}

func (r *localReader) LoadSchemaAndTemplateFS(ctx context.Context) (*jsonschema.Schema, fs.FS, error) {
return loadSchemaAndResolveTemplateDir(r.path)
}

func (r *localReader) SchemaFS(ctx context.Context) (fs.FS, error) {
return os.DirFS(r.path), nil
}

func (r *localReader) Cleanup(ctx context.Context) {}

// loadSchemaAndResolveTemplateDir loads a schema from a local directory path
Expand Down
32 changes: 17 additions & 15 deletions libs/template/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var gitUrlPrefixes = []string{
"git@",
}

func isRepoUrl(url string) bool {
func IsRepoUrl(url string) bool {
result := false
for _, prefix := range gitUrlPrefixes {
if strings.HasPrefix(url, prefix) {
Expand All @@ -24,6 +24,19 @@ func isRepoUrl(url string) bool {
return result
}

// ResolveReader resolves a template path/URL to a Reader (built-in, git or local)
func ResolveReader(templatePathOrUrl, templateDir, ref string) (Reader, bool) {
if tmpl := GetDatabricksTemplate(TemplateName(templatePathOrUrl)); tmpl != nil {
return tmpl.Reader, false
}

if IsRepoUrl(templatePathOrUrl) {
return NewGitReader(templatePathOrUrl, ref, templateDir, git.Clone), true
}

return NewLocalReader(templatePathOrUrl), false
}

type Resolver struct {
// One of the following three:
// 1. Path to a local template directory.
Expand Down Expand Up @@ -92,26 +105,15 @@ func (r Resolver) Resolve(ctx context.Context) (*Template, error) {
//
// We resolve the appropriate reader according to the reference provided by the user.
if tmpl == nil {
reader, _ := ResolveReader(r.TemplatePathOrUrl, r.TemplateDir, ref)
tmpl = &Template{
name: Custom,
name: Custom,
Reader: reader,
// We use a writer that does not log verbose telemetry for custom templates.
// This is important because template definitions can contain PII that we
// do not want to centralize.
Writer: &defaultWriter{name: Custom},
}

if isRepoUrl(r.TemplatePathOrUrl) {
tmpl.Reader = &gitReader{
gitUrl: r.TemplatePathOrUrl,
ref: ref,
templateDir: r.TemplateDir,
cloneFunc: git.Clone,
}
} else {
tmpl.Reader = &localReader{
path: r.TemplatePathOrUrl,
}
}
}
err = tmpl.Writer.Configure(ctx, r.ConfigFile, r.OutputDir)
if err != nil {
Expand Down
31 changes: 27 additions & 4 deletions libs/template/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,32 @@ func TestTemplateResolverForCustomPath(t *testing.T) {
}

func TestBundleInitIsRepoUrl(t *testing.T) {
assert.True(t, isRepoUrl("git@github.com:databricks/cli.git"))
assert.True(t, isRepoUrl("https://github.com/databricks/cli.git"))
assert.True(t, IsRepoUrl("git@github.com:databricks/cli.git"))
assert.True(t, IsRepoUrl("https://github.com/databricks/cli.git"))

assert.False(t, isRepoUrl("./local"))
assert.False(t, isRepoUrl("foo"))
assert.False(t, IsRepoUrl("./local"))
assert.False(t, IsRepoUrl("foo"))
}

func TestResolveReader(t *testing.T) {
t.Run("builtin template", func(t *testing.T) {
reader, isGit := ResolveReader("default-python", "", "")
assert.False(t, isGit)
assert.Equal(t, &builtinReader{name: "default-python"}, reader)
})

t.Run("git URL", func(t *testing.T) {
reader, isGit := ResolveReader("https://github.com/example/repo", "/template", "v1.0")
assert.True(t, isGit)
gitReader := reader.(*gitReader)
assert.Equal(t, "https://github.com/example/repo", gitReader.gitUrl)
assert.Equal(t, "/template", gitReader.templateDir)
assert.Equal(t, "v1.0", gitReader.ref)
})

t.Run("local path", func(t *testing.T) {
reader, isGit := ResolveReader("/local/path", "", "")
assert.False(t, isGit)
assert.Equal(t, "/local/path", reader.(*localReader).path)
})
}
Loading
Loading