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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .agents/rules/style-guide-go.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ return fieldPaths

### Encoding

**RULE: When mutating an API response by round-tripping JSON, decode with `json.Decoder` + `UseNumber()`.** The naive `json.Marshal` → `map[string]any` → `Marshal` path corrupts any int64 larger than 2^53 (it degrades to a float64 mantissa — e.g. a real `spark_context_id`) and alphabetizes object keys. `libs/dyn/jsonloader` preserves key order but also lacks `UseNumber`, so it shares the int64 hazard.
**RULE: When mutating an API response by round-tripping JSON, decode with `json.Decoder` + `UseNumber()`.** The naive `json.Marshal` → `map[string]any` → `Marshal` path corrupts any int64 larger than 2^53 (it degrades to a float64 mantissa — e.g. a real `spark_context_id`) and alphabetizes object keys.

**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.

Expand Down Expand Up @@ -191,7 +191,7 @@ func NewService(deps ServiceDeps) *Service { ... }

### Configuration patterns

- Bundle config uses `dyn.Value` for dynamic typing
- Bundle config is typed structs; `libs/structs/structvar` carries `${...}` references and source locations
- Config loading supports includes, variable interpolation, and target overrides
- Schema generation is automated from Go struct tags

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/python_push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
acceptance/bundle/python/ \
go.mod \
go.sum \
libs/dyn/dynvar/testdata/reference_vectors.json \
libs/structs/structvar/testdata/reference_vectors.json \
python/ \
tools/task/ \
)
Expand Down
1 change: 1 addition & 0 deletions .nextchanges/bundles/6954.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Speed up bundle commands on large bundles: configuration is kept as typed structs instead of being converted to and from a dynamic tree (`bundle validate` and `bundle plan` with 1000 jobs are about 10x faster and use about 3.5x less memory). ([#6954](https://github.com/databricks/cli/pull/6954))
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This is the Databricks CLI, a command-line interface for interacting with Databr

**RULE: Keep each PR focused on one change.** If you notice an unrelated cleanup, bug fix, or refactor while making your primary change, leave it alone or put it in a separate PR. Reviewers consistently ask to split mixed PRs, especially when a dependency bump or schema diff rides along with a feature change.

**RULE: Before adding a new helper, search the codebase for an existing one.** Common homes: `libs/` (shared utilities), `libs/databrickscfg/` (config), `libs/git/`, `libs/filer/`, `libs/cmdio/` (CLI I/O, spinners, prompts), `libs/env/` (env vars), `libs/testserver/`, `libs/structpath/` and `libs/dyn/` (path / dynamic values), `acceptance/bin/` (acceptance test helpers), `internal/mocks/` (generated mocks). A function that duplicates an existing name and signature in the same package is a compile error waiting to happen; grep before you name.
**RULE: Before adding a new helper, search the codebase for an existing one.** Common homes: `libs/` (shared utilities), `libs/databrickscfg/` (config), `libs/git/`, `libs/filer/`, `libs/cmdio/` (CLI I/O, spinners, prompts), `libs/env/` (env vars), `libs/testserver/`, `libs/structs/structpath/` and `libs/structs/structvar/` (paths / typed config values), `acceptance/bin/` (acceptance test helpers), `internal/mocks/` (generated mocks). A function that duplicates an existing name and signature in the same package is a compile error waiting to happen; grep before you name.

# Development Commands

Expand Down Expand Up @@ -73,7 +73,7 @@ GIT_EDITOR=true GIT_SEQUENCE_EDITOR=true VISUAL=true GIT_PAGER=cat git rebase or
- `bundle/phases/` - High-level deployment phases

**libs/** - Shared libraries and utilities
- `libs/dyn/` - Dynamic configuration value manipulation
- `libs/structs/structvar/` - Typed configuration values with references and locations
- `libs/filer/` - File system abstraction (local, DBFS, workspace)
- `libs/auth/` - Databricks authentication handling
- `libs/sync/` - File synchronization between local and remote
Expand Down
13 changes: 13 additions & 0 deletions acceptance/bundle/run/job-ref-job-id/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
bundle:
name: test-bundle

resources:
jobs:
bar:
name: job bar
foo:
name: job foo
tasks:
- task_key: job_task
run_job_task:
job_id: ${resources.jobs.bar.id}
2 changes: 2 additions & 0 deletions acceptance/bundle/run/job-ref-job-id/out.test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = false
EnvMatrix.DMS = ["", "true"]
14 changes: 14 additions & 0 deletions acceptance/bundle/run/job-ref-job-id/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files...
Created jobs.bar
Created jobs.foo
Files: 1 uploaded, 0 deleted
Resources: 2 created, 0 changed, 0 deleted, 0 unchanged

=== run resolves the referenced job id into the int job_id field
>>> [CLI] bundle run foo
Run URL: [DATABRICKS_URL]/jobs/[FOO_ID]/runs/[RUN_ID]?o=[WORKSPACE_ID]

[TIMESTAMP] "job foo" RUNNING
[TIMESTAMP] "job foo" TERMINATED SUCCESS
5 changes: 5 additions & 0 deletions acceptance/bundle/run/job-ref-job-id/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
trace $CLI bundle deploy
replace_ids.py

title "run resolves the referenced job id into the int job_id field"
trace $CLI bundle run foo
3 changes: 3 additions & 0 deletions acceptance/bundle/run/job-ref-job-id/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[[Repls]]
Old = '/runs/\d+'
New = '/runs/[RUN_ID]'
32 changes: 11 additions & 21 deletions acceptance/invariant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import (
"testing"

"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/libs/dyn"
"github.com/databricks/cli/libs/dyn/yamlloader"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3"
)

const invariantConfigsDir = "bundle/invariant/configs"
Expand Down Expand Up @@ -88,31 +87,22 @@ func scanInvariantConfigs(t *testing.T) (present, withPermissions, withGrants ma
contents, err := os.ReadFile(path)
require.NoError(t, err)

v, err := yamlloader.LoadYAML(path, strings.NewReader(string(contents)))
require.NoError(t, err, "failed to parse %s", path)

resources := v.Get("resources")
if resources.Kind() != dyn.KindMap {
// Some configs (e.g. PyDABs) declare resources outside of YAML.
continue
var doc struct {
Resources map[string]any `yaml:"resources"`
}
require.NoError(t, yaml.Unmarshal(contents, &doc), "failed to parse %s", path)

for _, group := range resources.MustMap().Pairs() {
groupName := group.Key.MustString()
// Some configs (e.g. PyDABs) declare resources outside of YAML.
for groupName, group := range doc.Resources {
present[groupName] = true

if group.Value.Kind() != dyn.KindMap {
continue
}
for _, resource := range group.Value.MustMap().Pairs() {
cfg := resource.Value
if cfg.Kind() != dyn.KindMap {
continue
}
if cfg.Get("permissions").Kind() != dyn.KindInvalid {
resources, _ := group.(map[string]any)
for _, resource := range resources {
cfg, _ := resource.(map[string]any)
if _, ok := cfg["permissions"]; ok {
withPermissions[groupName] = true
}
if cfg.Get("grants").Kind() != dyn.KindInvalid {
if _, ok := cfg["grants"]; ok {
withGrants[groupName] = true
}
}
Expand Down
33 changes: 18 additions & 15 deletions bundle/apps/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config/resources"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/dyn"
"github.com/databricks/cli/libs/structs/structpath"
"github.com/databricks/cli/libs/structs/structvar"
"github.com/databricks/databricks-sdk-go/service/apps"
)

Expand Down Expand Up @@ -94,32 +94,26 @@ type appResourceReference struct {

// hasPermissions checks if a bundle resource at the given dyn path has a non-empty permissions list.
func hasPermissions(b *bundle.Bundle, resourcePath string) bool {
pv, err := dyn.Get(b.Config.Value(), resourcePath+".permissions")
pv, err := valueAt(b, resourcePath+".permissions")
if err != nil {
return false
}
s, ok := pv.AsSequence()
return ok && len(s) > 0
for range pv.Sequence() {
return true
}
return false
}

// hasAppSPInPermissions checks if any permission entry for the given resource
// references the app's service principal via variable interpolation.
func hasAppSPInPermissions(b *bundle.Bundle, resourcePath, appKey string) bool {
appSPRef := fmt.Sprintf("${resources.apps.%s.service_principal_client_id}", appKey)
pv, err := dyn.Get(b.Config.Value(), resourcePath+".permissions")
pv, err := valueAt(b, resourcePath+".permissions")
if err != nil {
return false
}
s, ok := pv.AsSequence()
if !ok {
return false
}
for _, entry := range s {
spn, err := dyn.Get(entry, "service_principal_name")
if err != nil {
continue
}
if str, ok := spn.AsString(); ok && str == appSPRef {
for _, entry := range pv.Sequence() {
if str, ok := entry.Get("service_principal_name").AsString(); ok && str == appSPRef {
return true
}
}
Expand Down Expand Up @@ -188,3 +182,12 @@ func (v *validate) Name() string {
func Validate() bundle.Mutator {
return &validate{}
}

// valueAt returns the view of the configuration value at path.
func valueAt(b *bundle.Bundle, path string) (structvar.View, error) {
p, err := structpath.ParsePath(path)
if err != nil {
return structvar.View{}, err
}
return b.Config.View().Lookup(p), nil
}
7 changes: 3 additions & 4 deletions bundle/apps/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/databricks/cli/bundle/internal/bundletest"
"github.com/databricks/cli/internal/testutil"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/dyn"
"github.com/databricks/cli/libs/vfs"
"github.com/databricks/databricks-sdk-go/service/apps"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -48,7 +47,7 @@ func TestAppsValidateSameSourcePath(t *testing.T) {
},
}

bundletest.SetLocation(b, ".", []dyn.Location{{File: filepath.Join(tmpDir, "databricks.yml")}})
bundletest.SetLocation(b, ".", []diag.Location{{File: filepath.Join(tmpDir, "databricks.yml")}})

diags := bundle.ApplySeq(t.Context(), b, mutator.TranslatePaths(), Validate())
require.Len(t, diags, 1)
Expand Down Expand Up @@ -205,7 +204,7 @@ func TestAppsValidateResourcePermissionsWarning(t *testing.T) {
},
}

bundletest.SetLocation(b, ".", []dyn.Location{{File: filepath.Join(tmpDir, "databricks.yml")}})
bundletest.SetLocation(b, ".", []diag.Location{{File: filepath.Join(tmpDir, "databricks.yml")}})

diags := bundle.ApplySeq(t.Context(), b, Validate())
warnings := diags.Filter(diag.Warning)
Expand Down Expand Up @@ -249,7 +248,7 @@ func TestAppsValidateBothSourceCodePathAndGitSource(t *testing.T) {
},
}

bundletest.SetLocation(b, ".", []dyn.Location{{File: filepath.Join(tmpDir, "databricks.yml")}})
bundletest.SetLocation(b, ".", []diag.Location{{File: filepath.Join(tmpDir, "databricks.yml")}})

diags := bundle.ApplySeq(t.Context(), b, mutator.TranslatePaths(), Validate())
require.Len(t, diags, 1)
Expand Down
Loading
Loading