Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .agents/rules/style-guide-go.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ return fieldPaths

**RULE: Be careful with `encoding/csv` `Writer.UseCRLF = true`.** It rewrites both record terminators AND embedded newlines inside quoted fields to `\r\n`, so tests for quoted multiline fields must expect `\r\n`, not just the line endings between rows.

### Structpath

**RULE: Build computed structpath paths and patterns from parts (`structpath.NewPath`, `NewPathSlice`, `NewPattern`); never format or concatenate a string to parse it (for location lookups pass the node to `GetLocationsOf`).** `MustParsePath`/`MustParsePattern`/`MustParsePaths` are for string literals and tests; `ParsePath` with the error handled is for user input. The `NoComputedStructpathParse` ruleguard rule enforces this.

### Environment variables

**RULE: In library and product code, use `github.com/databricks/cli/libs/env` for reading environment variables, not `os.Getenv`.** `env.Get(ctx, name)` and `env.Lookup(ctx, name)` can be overridden per-context in tests, so you don't have to mutate process-wide state to exercise a code path. `os.Getenv` is still fine in `main`, tests, and acceptance/integration harnesses where no `ctx` is available and overrides aren't needed.
Expand Down
12 changes: 6 additions & 6 deletions bundle/apps/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (v *validate) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
Severity: diag.Error,
Summary: "Missing app source code path or git source",
Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field", key),
Locations: b.Config.GetLocations("resources.apps." + key),
Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "resources", "apps", key)),
})
continue
}
Expand All @@ -38,7 +38,7 @@ func (v *validate) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
Severity: diag.Error,
Summary: "Both source_code_path and git_source fields are set",
Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field, not both", key),
Locations: b.Config.GetLocations("resources.apps." + key),
Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "resources", "apps", key)),
})
continue
}
Expand All @@ -48,7 +48,7 @@ func (v *validate) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
Severity: diag.Error,
Summary: "Duplicate app source code path",
Detail: fmt.Sprintf("app resource '%s' has the same source code path as app resource '%s', this will lead to the app configuration being overridden by each other", key, usedSourceCodePaths[app.SourceCodePath]),
Locations: b.Config.GetLocations(fmt.Sprintf("resources.apps.%s.source_code_path", key)),
Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "resources", "apps", key, "source_code_path")),
})
}
usedSourceCodePaths[app.SourceCodePath] = key
Expand Down Expand Up @@ -149,7 +149,7 @@ func warnForAppResourcePermissions(b *bundle.Bundle, appKey string, app *resourc
continue
}

appPath := "resources.apps." + appKey
appPath := structpath.NewPath(nil, "resources", "apps", appKey)
diags = append(diags, diag.Diagnostic{
Severity: diag.Warning,
Summary: fmt.Sprintf("app %q references %s %q which has permissions set. To prevent permission override after deploying the app, please add the app service principal to the %s permissions", appKey, refType, resourceKey, refType),
Expand All @@ -167,8 +167,8 @@ func warnForAppResourcePermissions(b *bundle.Bundle, appKey string, app *resourc
ref.permission,
appKey,
),
Paths: structpath.NewPathSlice("resources", "apps", appKey),
Locations: b.Config.GetLocations(appPath),
Paths: []*structpath.PathNode{appPath},
Locations: b.Config.GetLocationsOf(appPath),
})
}

Expand Down
5 changes: 3 additions & 2 deletions bundle/artifacts/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/databricks/cli/libs/log"
"github.com/databricks/cli/libs/logdiag"
"github.com/databricks/cli/libs/python"
"github.com/databricks/cli/libs/structs/structpath"
)

func Prepare() bundle.Mutator {
Expand All @@ -38,7 +39,7 @@ func (m *prepare) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
for _, artifactName := range slices.Sorted(maps.Keys(b.Config.Artifacts)) {
artifact := b.Config.Artifacts[artifactName]
if artifact == nil {
l := b.Config.GetLocation("artifacts." + artifactName)
l := b.Config.GetLocationOf(structpath.NewPath(nil, "artifacts", artifactName))
logdiag.LogDiag(ctx, diag.Diagnostic{
Severity: diag.Error,
Summary: "Artifact not properly configured",
Expand All @@ -61,7 +62,7 @@ func (m *prepare) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
logdiag.LogError(ctx, fmt.Errorf("artifact %q: a tgz artifact needs a `files` entry naming the output path", artifactName))
}

l := b.Config.DefinitionLocation("artifacts." + artifactName)
l := b.Config.DefinitionLocationOf(structpath.NewPath(nil, "artifacts", artifactName))
dirPath := filepath.Dir(l.File)

// Check if source paths are absolute, if not, make them absolute
Expand Down
3 changes: 2 additions & 1 deletion bundle/config/loader/process_root_includes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/logdiag"
"github.com/databricks/cli/libs/structs/structpath"
)

type processRootIncludes struct{}
Expand Down Expand Up @@ -112,7 +113,7 @@ func (m *processRootIncludes) Apply(ctx context.Context, b *bundle.Bundle) diag.
Summary: "Files in the 'include' configuration section must be YAML or JSON files.",
Detail: fmt.Sprintf("The file %s in the 'include' configuration section is not a YAML or JSON file, and only such files are supported. To include files to sync, specify them in the 'sync.include' configuration section instead.", rel),
// The match's index within the glob is unrelated to the entry's position in the include list.
Locations: b.Config.GetLocations(fmt.Sprintf("include[%d]", entryIndex)),
Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "include", entryIndex)),
})
continue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (m *applySourceLinkedDeploymentPreset) Apply(ctx context.Context, b *bundle
Summary: "workspace.file_path setting will be ignored in source-linked deployment mode",
Detail: "In source-linked deployment files are not copied to the destination and resources use source files instead",
Paths: []*structpath.PathNode{path},
Locations: b.Config.GetLocations(path.String()),
Locations: b.Config.GetLocationsOf(path),
},
)
}
Expand Down
4 changes: 2 additions & 2 deletions bundle/config/mutator/paths/artifact_paths_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ type artifactRewritePattern struct {

func artifactRewritePatterns() []artifactRewritePattern {
// Base pattern to match all artifacts.
base := "artifacts.*"
base := structpath.NewPattern(nil, "artifacts", structpath.AnyKey)

// Compile list of configuration paths to rewrite.
return []artifactRewritePattern{
{
pattern: structpath.MustParsePattern(base + ".path"),
pattern: structpath.NewPattern(base, "path"),
mode: TranslateModeLocalAbsoluteDirectory,
},
}
Expand Down
10 changes: 5 additions & 5 deletions bundle/config/mutator/paths/job_libraries_paths_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import (
"github.com/databricks/cli/libs/structs/structvar"
)

func jobTaskLibrariesRewritePatterns(base string) []jobRewritePattern {
func jobTaskLibrariesRewritePatterns(base *structpath.PatternNode) []jobRewritePattern {
return []jobRewritePattern{
{
structpath.MustParsePattern(base + ".libraries[*].whl"),
structpath.NewPattern(base, "libraries", structpath.AnyIndex, "whl"),
TranslateModeLocalRelative,
noSkipRewrite,
},
{
structpath.MustParsePattern(base + ".libraries[*].jar"),
structpath.NewPattern(base, "libraries", structpath.AnyIndex, "jar"),
TranslateModeLocalRelative,
noSkipRewrite,
},
Expand All @@ -23,7 +23,7 @@ func jobTaskLibrariesRewritePatterns(base string) []jobRewritePattern {

func jobLibrariesRewritePatterns() []jobRewritePattern {
// Base pattern to match all tasks in all jobs.
base := "resources.jobs.*.tasks[*]"
base := jobTasksPattern

// Compile list of patterns and their respective rewrite functions.
jobEnvironmentsPatterns := []jobRewritePattern{
Expand All @@ -48,7 +48,7 @@ func jobLibrariesRewritePatterns() []jobRewritePattern {
}

taskPatterns := jobTaskLibrariesRewritePatterns(base)
forEachPatterns := jobTaskLibrariesRewritePatterns(base + ".for_each_task.task")
forEachPatterns := jobTaskLibrariesRewritePatterns(structpath.NewPattern(base, "for_each_task", "task"))
allPatterns := append(taskPatterns, jobEnvironmentsPatterns...)
allPatterns = append(allPatterns, jobEnvironmentsWithRequirementsPatterns...)
allPatterns = append(allPatterns, forEachPatterns...)
Expand Down
23 changes: 13 additions & 10 deletions bundle/config/mutator/paths/job_paths_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,50 @@ type jobRewritePattern struct {
skipRewrite func(string) bool
}

// jobTasksPattern matches all tasks in all jobs.
var jobTasksPattern = structpath.NewPattern(nil, "resources", "jobs", structpath.AnyKey, "tasks", structpath.AnyIndex)

func noSkipRewrite(string) bool {
return false
}

func jobTaskRewritePatterns(base string) []jobRewritePattern {
func jobTaskRewritePatterns(base *structpath.PatternNode) []jobRewritePattern {
return []jobRewritePattern{
{
structpath.MustParsePattern(base + ".notebook_task.notebook_path"),
structpath.NewPattern(base, "notebook_task", "notebook_path"),
TranslateModeNotebook,
noSkipRewrite,
},
{
structpath.MustParsePattern(base + ".spark_python_task.python_file"),
structpath.NewPattern(base, "spark_python_task", "python_file"),
TranslateModeFile,
noSkipRewrite,
},
{
structpath.MustParsePattern(base + ".dbt_task.project_directory"),
structpath.NewPattern(base, "dbt_task", "project_directory"),
TranslateModeDirectory,
noSkipRewrite,
},
{
structpath.MustParsePattern(base + ".sql_task.file.path"),
structpath.NewPattern(base, "sql_task", "file", "path"),
TranslateModeFile,
noSkipRewrite,
},
{
structpath.MustParsePattern(base + ".alert_task.workspace_path"),
structpath.NewPattern(base, "alert_task", "workspace_path"),
TranslateModeFile,
noSkipRewrite,
},
{
structpath.MustParsePattern(base + ".libraries[*].requirements"),
structpath.NewPattern(base, "libraries", structpath.AnyIndex, "requirements"),
TranslateModeFile,
noSkipRewrite,
},
{
// The AI Runtime task runs this bash script on each node; the backend
// reads it as a workspace file, so translate the local path to its
// remote (or immutable-snapshot) location like any other file.
structpath.MustParsePattern(base + ".ai_runtime_task.deployments[*].command_path"),
structpath.NewPattern(base, "ai_runtime_task", "deployments", structpath.AnyIndex, "command_path"),
TranslateModeFile,
noSkipRewrite,
},
Expand All @@ -60,10 +63,10 @@ func jobTaskRewritePatterns(base string) []jobRewritePattern {

func jobRewritePatterns() []jobRewritePattern {
// Base pattern to match all tasks in all jobs.
base := "resources.jobs.*.tasks[*]"
base := jobTasksPattern

taskPatterns := jobTaskRewritePatterns(base)
forEachPatterns := jobTaskRewritePatterns(base + ".for_each_task.task")
forEachPatterns := jobTaskRewritePatterns(structpath.NewPattern(base, "for_each_task", "task"))
return append(taskPatterns, forEachPatterns...)
}

Expand Down
10 changes: 5 additions & 5 deletions bundle/config/mutator/paths/pipeline_paths_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,28 @@ type pipelineRewritePattern struct {
}

// Base pattern to match all libraries in all pipelines.
var base = "resources.pipelines.*"
var base = structpath.NewPattern(nil, "resources", "pipelines", structpath.AnyKey)

func pipelineRewritePatterns() []pipelineRewritePattern {
// Compile list of configuration paths to rewrite.
allPatterns := []pipelineRewritePattern{
{
pattern: structpath.MustParsePattern(base + ".libraries[*].notebook.path"),
pattern: structpath.NewPattern(base, "libraries", structpath.AnyIndex, "notebook", "path"),
mode: TranslateModeNotebook,
skipRewrite: noSkipRewrite,
},
{
pattern: structpath.MustParsePattern(base + ".libraries[*].file.path"),
pattern: structpath.NewPattern(base, "libraries", structpath.AnyIndex, "file", "path"),
mode: TranslateModeFile,
skipRewrite: noSkipRewrite,
},
{
pattern: structpath.MustParsePattern(base + ".libraries[*].glob.include"),
pattern: structpath.NewPattern(base, "libraries", structpath.AnyIndex, "glob", "include"),
mode: TranslateModeGlob,
skipRewrite: noSkipRewrite,
},
{
pattern: structpath.MustParsePattern(base + ".root_path"),
pattern: structpath.NewPattern(base, "root_path"),
mode: TranslateModeDirectory,
skipRewrite: noSkipRewrite,
},
Expand Down
21 changes: 11 additions & 10 deletions bundle/config/mutator/resolve_job_run_file_triggers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config/resources"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/structs/structpath"
libsync "github.com/databricks/cli/libs/sync"
)

Expand Down Expand Up @@ -65,7 +66,7 @@ func (*resolveJobRunFileTriggers) Apply(ctx context.Context, b *bundle.Bundle) d
if t.OnFileChange == nil {
continue
}
path := fmt.Sprintf("resources.job_runs.%s.lifecycle.triggers[%d].on_file_change", name, i)
path := structpath.NewPath(nil, "resources", "job_runs", name, "lifecycle", "triggers", i, "on_file_change")
pattern, fingerprint, d := resolveFileTrigger(b, path, *t.OnFileChange, syncable)
diags = diags.Extend(d)
if !d.HasError() {
Expand Down Expand Up @@ -105,7 +106,7 @@ func listSyncableRelPaths(ctx context.Context, b *bundle.Bundle) ([]string, erro
return out, nil
}

func resolveFileTrigger(b *bundle.Bundle, loc, pattern string, syncable []string) (string, string, diag.Diagnostics) {
func resolveFileTrigger(b *bundle.Bundle, loc *structpath.PathNode, pattern string, syncable []string) (string, string, diag.Diagnostics) {
relPattern, diags := validateFileTriggerPattern(b, loc, pattern)
if diags.HasError() {
return "", "", diags
Expand All @@ -119,7 +120,7 @@ func resolveFileTrigger(b *bundle.Bundle, loc, pattern string, syncable []string
diags = diags.Append(diag.Diagnostic{
Severity: diag.Error,
Summary: fileTriggerPrefix + fmt.Sprintf("invalid pattern %q: %s", pattern, err),
Locations: b.Config.GetLocations(loc),
Locations: b.Config.GetLocationsOf(loc),
})
continue
}
Expand All @@ -131,7 +132,7 @@ func resolveFileTrigger(b *bundle.Bundle, loc, pattern string, syncable []string
diags = diags.Append(diag.Diagnostic{
Severity: diag.Error,
Summary: fileTriggerPrefix + fmt.Sprintf("hash %q: %s", rel, err),
Locations: b.Config.GetLocations(loc),
Locations: b.Config.GetLocationsOf(loc),
})
continue
}
Expand All @@ -145,20 +146,20 @@ func resolveFileTrigger(b *bundle.Bundle, loc, pattern string, syncable []string
diags = diags.Append(diag.Diagnostic{
Severity: diag.Warning,
Summary: fileTriggerPrefix + fmt.Sprintf("no synced files match %q", pattern),
Locations: b.Config.GetLocations(loc),
Locations: b.Config.GetLocationsOf(loc),
})
}
return relPattern, hex.EncodeToString(h.Sum(nil)), diags
}

func validateFileTriggerPattern(b *bundle.Bundle, loc, pattern string) (string, diag.Diagnostics) {
func validateFileTriggerPattern(b *bundle.Bundle, loc *structpath.PathNode, pattern string) (string, diag.Diagnostics) {
var diags diag.Diagnostics
// A double star looks recursive but path.Match treats it as two ordinary stars.
if strings.Contains(pattern, "**") {
return "", diags.Append(diag.Diagnostic{
Severity: diag.Error,
Summary: fileTriggerPrefix + fmt.Sprintf("** in %q is not supported; use * for a single directory level", pattern),
Locations: b.Config.GetLocations(loc),
Locations: b.Config.GetLocationsOf(loc),
})
}
// Reject a genuinely absolute path; Join would otherwise silently reinterpret it
Expand All @@ -170,7 +171,7 @@ func validateFileTriggerPattern(b *bundle.Bundle, loc, pattern string) (string,
return "", diags.Append(diag.Diagnostic{
Severity: diag.Error,
Summary: fileTriggerPrefix + fmt.Sprintf("pattern %q must be relative to the defining YAML file", pattern),
Locations: b.Config.GetLocations(loc),
Locations: b.Config.GetLocationsOf(loc),
})
}
// NormalizePaths has already rewritten YAML-relative globs to be bundle-root
Expand All @@ -182,7 +183,7 @@ func validateFileTriggerPattern(b *bundle.Bundle, loc, pattern string) (string,
return "", diags.Append(diag.Diagnostic{
Severity: diag.Error,
Summary: fileTriggerPrefix + fmt.Sprintf("pattern %q is not under the sync root", pattern),
Locations: b.Config.GetLocations(loc),
Locations: b.Config.GetLocationsOf(loc),
})
}
relPattern = filepath.ToSlash(relPattern)
Expand All @@ -191,7 +192,7 @@ func validateFileTriggerPattern(b *bundle.Bundle, loc, pattern string) (string,
return "", diags.Append(diag.Diagnostic{
Severity: diag.Error,
Summary: fileTriggerPrefix + fmt.Sprintf("invalid pattern %q: %s", pattern, err),
Locations: b.Config.GetLocations(loc),
Locations: b.Config.GetLocationsOf(loc),
})
}
return relPattern, diags
Expand Down
3 changes: 2 additions & 1 deletion bundle/config/mutator/resolve_variable_references.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var defaultPrefixes = []string{
var artifactPath = structpath.MustParsePath("artifacts")

type resolveVariableReferences struct {
// prefixes are top-level config keys.
prefixes []string
pattern *structpath.PatternNode
lookupFn func(structvar.View, *structpath.PathNode, *bundle.Bundle) (structvar.View, error)
Expand Down Expand Up @@ -169,7 +170,7 @@ func (m *resolveVariableReferences) Validate(ctx context.Context, b *bundle.Bund
func (m *resolveVariableReferences) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
prefixes := make([]*structpath.PathNode, len(m.prefixes))
for i, prefix := range m.prefixes {
prefixes[i] = structpath.MustParsePath(prefix)
prefixes[i] = structpath.NewPath(nil, prefix)
}

// The path ${var.foo} is a shorthand for ${variables.foo.value}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func (m *bundlePermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Di
slices.Sort(keys)

for _, key := range keys {
pattern := structpath.MustParsePattern("resources." + key + ".*")
pattern := structpath.NewPattern(nil, "resources", key, structpath.AnyKey)

err = structvar.ForEach(b.Config.View(), pattern, func(p *structpath.PathNode, v structvar.View) error {
had := v.Get("permissions").IsValid()
Expand Down
Loading