Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the output of the databricks bundle init command #795

Merged
merged 15 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
17 changes: 14 additions & 3 deletions cmd/bundle/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,28 @@ func repoName(url string) string {
func newInitCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "init [TEMPLATE_PATH]",
Short: "Initialize Template",
Args: cobra.MaximumNArgs(1),
lennartkats-db marked this conversation as resolved.
Show resolved Hide resolved
Short: "Initialize a new bundle from a template",
lennartkats-db marked this conversation as resolved.
Show resolved Hide resolved
Long: `Initialize a new bundle from a template.

TEMPLATE_PATH optionally specifies which template to use. It can be one of the following:
- 'default-python' for the default Python template
- a local file system path with a template directory
- a Git repository URL, e.g. https://github.com/my/repository

See https://docs.databricks.com//dev-tools/bundles/templates.html for more information on templates.`,
}

var configFile string
var outputDir string
var templateDir string
cmd.Flags().StringVar(&configFile, "config-file", "", "File containing input parameters for template initialization.")
cmd.Flags().StringVar(&templateDir, "template-dir", "", "Directory within repository that holds the template specification.")
cmd.Flags().StringVar(&templateDir, "template-dir", "", "Directory path within a Git repository containing the template.")
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 {
ctx := cmd.Context()

var templatePath string
if len(args) > 0 {
templatePath = args[0]
Expand All @@ -69,6 +77,9 @@ func newInitCommand() *cobra.Command {
}

if !isRepoUrl(templatePath) {
if templateDir != "" {
return errors.New("--template-dir can only be used with a Git repository URL")
}
// skip downloading the repo because input arg is not a URL. We assume
// it's a path on the local file system in that case
return template.Materialize(ctx, configFile, templatePath, outputDir)
Expand Down
4 changes: 4 additions & 0 deletions libs/jsonschema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ type Schema struct {
// The values are the schema for the type of the field
Properties map[string]*Schema `json:"properties,omitempty"`

// The message to print after the template is successfully initalized
// In line with our ymls, we use snake_case here.
SuccessMessage string `json:"success_message,omitempty"`
lennartkats-db marked this conversation as resolved.
Show resolved Hide resolved

// The schema for all values of an array
Items *Schema `json:"items,omitempty"`

Expand Down
39 changes: 30 additions & 9 deletions libs/template/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (c *config) assignValuesFromFile(path string) error {
}

// Assigns default values from schema to input config map
func (c *config) assignDefaultValues() error {
func (c *config) assignDefaultValues(r *renderer) error {
for name, property := range c.schema.Properties {
// Config already has a value assigned
if _, ok := c.values[name]; ok {
Expand All @@ -75,13 +75,25 @@ func (c *config) assignDefaultValues() error {
if property.Default == nil {
continue
}
c.values[name] = property.Default
defaultVal, err := jsonschema.ToString(property.Default, property.Type)
if err != nil {
return err
}
defaultVal, err = r.executeTemplate(defaultVal)
if err != nil {
return err
}
defaultValTyped, err := jsonschema.FromString(defaultVal, property.Type)
if err != nil {
return err
}
c.values[name] = defaultValTyped
}
return nil
}

// Prompts user for values for properties that do not have a value set yet
func (c *config) promptForValues() error {
func (c *config) promptForValues(r *renderer) error {
for _, p := range c.schema.OrderedProperties() {
name := p.Name
property := p.Schema
Expand All @@ -95,12 +107,21 @@ func (c *config) promptForValues() error {
var defaultVal string
var err error
if property.Default != nil {
defaultVal, err = jsonschema.ToString(property.Default, property.Type)
defaultValRaw, err := jsonschema.ToString(property.Default, property.Type)
if err != nil {
return err
}
defaultVal, err = r.executeTemplate(defaultValRaw)
if err != nil {
return err
}
}

description, err := r.executeTemplate(property.Description)
if err != nil {
return err
}

// Get user input by running the prompt
var userInput string
if property.Enum != nil {
Expand All @@ -109,12 +130,12 @@ func (c *config) promptForValues() error {
if err != nil {
return err
}
userInput, err = cmdio.AskSelect(c.ctx, property.Description, enums)
userInput, err = cmdio.AskSelect(c.ctx, description, enums)
if err != nil {
return err
}
} else {
userInput, err = cmdio.Ask(c.ctx, property.Description, defaultVal)
userInput, err = cmdio.Ask(c.ctx, description, defaultVal)
if err != nil {
return err
}
Expand All @@ -132,11 +153,11 @@ func (c *config) promptForValues() error {

// Prompt user for any missing config values. Assign default values if
// terminal is not TTY
func (c *config) promptOrAssignDefaultValues() error {
func (c *config) promptOrAssignDefaultValues(r *renderer) error {
if cmdio.IsOutTTY(c.ctx) && cmdio.IsInTTY(c.ctx) {
return c.promptForValues()
return c.promptForValues(r)
}
return c.assignDefaultValues()
return c.assignDefaultValues(r)
}

// Validates the configuration. If passes, the configuration is ready to be used
Expand Down
25 changes: 23 additions & 2 deletions libs/template/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"testing"

"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -52,11 +53,17 @@ func TestTemplateConfigAssignValuesFromFileDoesNotOverwriteExistingConfigs(t *te
func TestTemplateConfigAssignDefaultValues(t *testing.T) {
c := testConfig(t)

err := c.assignDefaultValues()
ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, nil)
helpers := loadHelpers(ctx)
r, err := newRenderer(ctx, nil, helpers, "./testdata/template-in-path/template", "./testdata/template-in-path/library", t.TempDir())
require.NoError(t, err)

err = c.assignDefaultValues(r)
assert.NoError(t, err)

assert.Len(t, c.values, 2)
assert.Equal(t, "abc", c.values["string_val"])
assert.Equal(t, "my_file", c.values["string_val"])
assert.Equal(t, int64(123), c.values["int_val"])
}

Expand Down Expand Up @@ -169,3 +176,17 @@ func TestTemplateEnumValidation(t *testing.T) {
}
assert.NoError(t, c.validate())
}

func TestAssignDefaultValuesWithTemplatedDefaults(t *testing.T) {
lennartkats-db marked this conversation as resolved.
Show resolved Hide resolved
c := testConfig(t)
ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, nil)
helpers := loadHelpers(ctx)
r, err := newRenderer(ctx, nil, helpers, "./testdata/template-in-path/template", "./testdata/template-in-path/library", t.TempDir())
require.NoError(t, err)

err = c.assignDefaultValues(r)
require.NoError(t, err)
assert.NoError(t, err)
lennartkats-db marked this conversation as resolved.
Show resolved Hide resolved
assert.Equal(t, "my_file", c.values["string_val"])
}
8 changes: 6 additions & 2 deletions libs/template/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/url"
"os"
"regexp"
"text/template"

Expand Down Expand Up @@ -65,7 +66,7 @@ func loadHelpers(ctx context.Context) template.FuncMap {
// 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'")
return "", errors.New("cannot determine target workspace, please first setup a configuration profile using 'databricks configure'")
}
if w.Config.IsAzure() {
return "Standard_D3_v2", nil
Expand All @@ -74,9 +75,12 @@ func loadHelpers(ctx context.Context) template.FuncMap {
}
return "i3.xlarge", nil
},
"path_separator": func() string {
return string(os.PathSeparator)
},
"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 "", errors.New("cannot determine target workspace, please first setup a configuration profile using 'databricks configure'")
}
return w.Config.Host, nil
},
Expand Down
24 changes: 17 additions & 7 deletions libs/template/materialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,23 @@ func Materialize(ctx context.Context, configFilePath, templateRoot, outputDir st
}
}

r, err := newRenderer(ctx, config.values, helpers, templatePath, libraryPath, outputDir)
if err != nil {
return err
}

// Prompt user for any missing config values. Assign default values if
// terminal is not TTY
err = config.promptOrAssignDefaultValues()
err = config.promptOrAssignDefaultValues(r)
if err != nil {
return err
}

err = config.validate()
if err != nil {
return err
}

// Walk and render the template, since input configuration is complete
r, err := newRenderer(ctx, config.values, helpers, templatePath, libraryPath, outputDir)
lennartkats-db marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
err = r.walk()
if err != nil {
return err
Expand All @@ -82,7 +82,17 @@ func Materialize(ctx context.Context, configFilePath, templateRoot, outputDir st
if err != nil {
return err
}
cmdio.LogString(ctx, "✨ Successfully initialized template")

success := config.schema.SuccessMessage
if success == "" {
cmdio.LogString(ctx, "✨ Successfully initialized template")
} else {
success, err = r.executeTemplate(success)
if err != nil {
return err
}
cmdio.LogString(ctx, success)
}
return nil
}

Expand Down
2 changes: 1 addition & 1 deletion libs/template/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ func (r *renderer) persistToDisk() error {
path := file.DstPath().absPath()
_, err := os.Stat(path)
if err == nil {
return fmt.Errorf("failed to persist to disk, conflict with existing file: %s", path)
return fmt.Errorf("failed initialize template, one or more files already exist: %s", path)
lennartkats-db marked this conversation as resolved.
Show resolved Hide resolved
}
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error while verifying file %s does not already exist: %w", path, err)
Expand Down
2 changes: 1 addition & 1 deletion libs/template/renderer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ func TestRendererErrorOnConflictingFile(t *testing.T) {
},
}
err = r.persistToDisk()
assert.EqualError(t, err, fmt.Sprintf("failed to persist to disk, conflict with existing file: %s", filepath.Join(tmpDir, "a")))
assert.EqualError(t, err, fmt.Sprintf("failed initialize template, one or more files already exist: %s", filepath.Join(tmpDir, "a")))
}

func TestRendererNoErrorOnConflictingFileIfSkipped(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,24 @@
"type": "string",
"default": "yes",
"enum": ["yes", "no"],
"description": "Include a stub (sample) notebook in 'my_project/src'",
"description": "Include a stub (sample) notebook in '{{.project_name}}{{path_separator}}src'",
"order": 2
},
"include_dlt": {
"type": "string",
"default": "yes",
"enum": ["yes", "no"],
"description": "Include a stub (sample) DLT pipeline in 'my_project/src'",
"description": "Include a stub (sample) Delta Live Tables pipeline in '{{.project_name}}{{path_separator}}src'",
"order": 3
},
"include_python": {
"type": "string",
"default": "yes",
"enum": ["yes", "no"],
"description": "Include a stub (sample) Python package 'my_project/src'",
"comment": "If the selected they don't want a notebook we provide an extra hint that the stub Python package can be used to author their DLT pipeline",
"description": "Include a stub (sample) Python package in '{{.project_name}}{{path_separator}}src'",
"order": 4
}
}
},
"success_message": "\n✨ Your new project has been created in the '{{.project_name}}' directory!\n\nPlease refer to the README.md of your project for further instructions on getting started.\nOr read the documentation on Databricks Asset Bundles at https://docs.databricks.com/dev-tools/bundles/index.html."
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"string_val": {
"type": "string",
"default": "abc"
"default": "{{template \"file_name\"}}"
}
}
}