databricks-cli/libs/template/template.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

141 lines
3.8 KiB
Go
Raw Normal View History

Refactor `bundle init` (#2074) ## Summary of changes This PR introduces three new abstractions: 1. `Resolver`: Resolves which reader and writer to use for a template. 2. `Writer`: Writes a template project to disk. Prompts the user if necessary. 3. `Reader`: Reads a template specification from disk, built into the CLI or from GitHub. Introducing these abstractions helps decouple reading a template from writing it. When I tried adding telemetry for the `bundle init` command, I noticed that the code in `cmd/init.go` was getting convoluted and hard to test. A future change could have accidentally logged PII when a user initialised a custom template. Hedging against that risk is important here because we use a generic untyped `map<string, string>` representation in the backend to log telemetry for the `databricks bundle init`. Otherwise, we risk accidentally breaking our compliance with our centralization requirements. ### Details After this PR there are two classes of templates that can be initialized: 1. A `databricks` template: This could be a builtin template or a template outside the CLI like mlops-stacks, which is still owned and managed by Databricks. These templates log their telemetry arguments and template name. 2. A `custom` template: These are templates created by and managed by the end user. In these templates we do not log the template name and args. Instead a generic placeholder string of "custom" is logged in our telemetry system. NOTE: The functionality of the `databricks bundle init` command remains the same after this PR. Only the internal abstractions used are changed. ## Tests New unit tests. Existing golden and unit tests. Also a fair bit of manual testing.
2025-01-20 12:09:28 +00:00
package template
import (
"context"
"errors"
"fmt"
"slices"
"strings"
"github.com/databricks/cli/libs/cmdio"
"github.com/databricks/cli/libs/git"
)
type Template struct {
Reader Reader
Writer Writer
name TemplateName
description string
aliases []string
hidden bool
}
type TemplateName string
const (
DefaultPython TemplateName = "default-python"
DefaultSql TemplateName = "default-sql"
DbtSql TemplateName = "dbt-sql"
MlopsStacks TemplateName = "mlops-stacks"
DefaultPydabs TemplateName = "default-pydabs"
Custom TemplateName = "custom"
ExperimentalJobsAsCode TemplateName = "experimental-jobs-as-code"
)
var databricksTemplates = []Template{
{
name: DefaultPython,
description: "The default Python template for Notebooks / Delta Live Tables / Workflows",
Reader: &builtinReader{name: string(DefaultPython)},
Writer: &writerWithFullTelemetry{},
},
{
name: DefaultSql,
description: "The default SQL template for .sql files that run with Databricks SQL",
Reader: &builtinReader{name: string(DefaultSql)},
Writer: &writerWithFullTelemetry{},
},
{
name: DbtSql,
description: "The dbt SQL template (databricks.com/blog/delivering-cost-effective-data-real-time-dbt-and-databricks)",
Reader: &builtinReader{name: string(DbtSql)},
Writer: &writerWithFullTelemetry{},
},
{
name: MlopsStacks,
description: "The Databricks MLOps Stacks template (github.com/databricks/mlops-stacks)",
aliases: []string{"mlops-stack"},
Reader: &gitReader{gitUrl: "https://github.com/databricks/mlops-stacks", cloneFunc: git.Clone},
Writer: &writerWithFullTelemetry{},
},
{
name: DefaultPydabs,
hidden: true,
description: "The default PyDABs template",
Reader: &gitReader{gitUrl: "https://databricks.github.io/workflows-authoring-toolkit/pydabs-template.git", cloneFunc: git.Clone},
Writer: &writerWithFullTelemetry{},
},
{
name: ExperimentalJobsAsCode,
hidden: true,
description: "Jobs as code template (experimental)",
Reader: &builtinReader{name: string(ExperimentalJobsAsCode)},
Writer: &writerWithFullTelemetry{},
},
}
func HelpDescriptions() string {
var lines []string
for _, template := range databricksTemplates {
if template.name != Custom && !template.hidden {
lines = append(lines, fmt.Sprintf("- %s: %s", template.name, template.description))
}
}
return strings.Join(lines, "\n")
}
var customTemplateDescription = "Bring your own template"
func options() []cmdio.Tuple {
names := make([]cmdio.Tuple, 0, len(databricksTemplates))
for _, template := range databricksTemplates {
if template.hidden {
continue
}
tuple := cmdio.Tuple{
Name: string(template.name),
Id: template.description,
}
names = append(names, tuple)
}
names = append(names, cmdio.Tuple{
Name: "custom...",
Id: customTemplateDescription,
})
return names
}
func SelectTemplate(ctx context.Context) (TemplateName, error) {
if !cmdio.IsPromptSupported(ctx) {
return "", errors.New("prompting is not supported. Please specify the path, name or URL of the template to use")
}
description, err := cmdio.SelectOrdered(ctx, options(), "Template to use")
if err != nil {
return "", err
}
if description == customTemplateDescription {
return TemplateName(""), ErrCustomSelected
}
for _, template := range databricksTemplates {
if template.description == description {
return template.name, nil
}
}
return "", fmt.Errorf("template with description %s not found", description)
}
func GetDatabricksTemplate(name TemplateName) *Template {
for _, template := range databricksTemplates {
if template.name == name || slices.Contains(template.aliases, string(name)) {
return &template
}
}
return nil
}