From d1c3445e25847277c62d7a9558a9152e828bff0e Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 1 Jun 2026 10:29:57 +0200 Subject: [PATCH 01/25] bundle: add --select flag to validate/plan/summary/deploy Filter deployment to a subset of resources using --select (e.g. --select my_job or --select jobs.my_job). Unqualified names that match exactly one resource type are resolved automatically; ambiguous names require a qualified form and produce an actionable error. Co-authored-by: Isaac --- acceptance/bundle/plan/select/databricks.yml | 32 ++++++++ acceptance/bundle/plan/select/my_script.py | 1 + acceptance/bundle/plan/select/out.test.toml | 3 + acceptance/bundle/plan/select/output.txt | 18 +++++ acceptance/bundle/plan/select/script | 11 +++ acceptance/bundle/plan/select/test.toml | 2 + bundle/bundle.go | 4 + bundle/config/mutator/select_resources.go | 70 +++++++++++++++++ .../config/mutator/select_resources_test.go | 75 +++++++++++++++++++ bundle/config/resources.go | 25 +++++++ bundle/phases/initialize.go | 5 ++ cmd/bundle/deploy.go | 3 + cmd/bundle/plan.go | 20 ++--- cmd/bundle/summary.go | 5 ++ cmd/bundle/validate.go | 5 ++ 15 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 acceptance/bundle/plan/select/databricks.yml create mode 100644 acceptance/bundle/plan/select/my_script.py create mode 100644 acceptance/bundle/plan/select/out.test.toml create mode 100644 acceptance/bundle/plan/select/output.txt create mode 100644 acceptance/bundle/plan/select/script create mode 100644 acceptance/bundle/plan/select/test.toml create mode 100644 bundle/config/mutator/select_resources.go create mode 100644 bundle/config/mutator/select_resources_test.go diff --git a/acceptance/bundle/plan/select/databricks.yml b/acceptance/bundle/plan/select/databricks.yml new file mode 100644 index 00000000000..f25f36ad2f0 --- /dev/null +++ b/acceptance/bundle/plan/select/databricks.yml @@ -0,0 +1,32 @@ +bundle: + name: plan-select + +resources: + jobs: + job_a: + name: job-a + tasks: + - task_key: task1 + spark_python_task: + python_file: "./my_script.py" + environment_key: "env" + environments: + - environment_key: "env" + spec: + client: "1" + + job_b: + name: job-b + tasks: + - task_key: task1 + spark_python_task: + python_file: "./my_script.py" + environment_key: "env" + environments: + - environment_key: "env" + spec: + client: "1" + + pipelines: + my_pipeline: + name: my-pipeline diff --git a/acceptance/bundle/plan/select/my_script.py b/acceptance/bundle/plan/select/my_script.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/plan/select/my_script.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/plan/select/out.test.toml b/acceptance/bundle/plan/select/out.test.toml new file mode 100644 index 00000000000..f784a183258 --- /dev/null +++ b/acceptance/bundle/plan/select/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/plan/select/output.txt b/acceptance/bundle/plan/select/output.txt new file mode 100644 index 00000000000..783db33baf7 --- /dev/null +++ b/acceptance/bundle/plan/select/output.txt @@ -0,0 +1,18 @@ + +>>> [CLI] bundle plan --select no_such_resource +Error: no such resource: no_such_resource + + +>>> [CLI] bundle plan --select jobs.no_such_job +Error: no such resource: jobs.no_such_job + + +>>> [CLI] bundle plan --select job_b +create jobs.job_b + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select jobs.job_a +create jobs.job_a + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select/script b/acceptance/bundle/plan/select/script new file mode 100644 index 00000000000..4e88d15e4d6 --- /dev/null +++ b/acceptance/bundle/plan/select/script @@ -0,0 +1,11 @@ +# Non-existent resource +trace $CLI bundle plan --select no_such_resource || true + +# Qualified name for non-existent resource +trace $CLI bundle plan --select jobs.no_such_job || true + +# Select a unique resource by unqualified name +trace $CLI bundle plan --select job_b + +# Select a resource by qualified name +trace $CLI bundle plan --select jobs.job_a \ No newline at end of file diff --git a/acceptance/bundle/plan/select/test.toml b/acceptance/bundle/plan/select/test.toml new file mode 100644 index 00000000000..7d36fb9dc18 --- /dev/null +++ b/acceptance/bundle/plan/select/test.toml @@ -0,0 +1,2 @@ +Local = true +Cloud = false diff --git a/bundle/bundle.go b/bundle/bundle.go index e7eef14b907..868510b15e6 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -145,6 +145,10 @@ type Bundle struct { // files AutoApprove bool + // Select contains resource selectors passed via --select flag. + // When non-empty, only the specified resources are included in deployment. + Select []string + // SkipLocalFileValidation makes path translation tolerant of missing local files. // When set, TranslatePaths computes workspace paths without verifying files exist. // Used by config-remote-sync: a user may modify resource paths remotely (e.g., diff --git a/bundle/config/mutator/select_resources.go b/bundle/config/mutator/select_resources.go new file mode 100644 index 00000000000..7a55ce0bebb --- /dev/null +++ b/bundle/config/mutator/select_resources.go @@ -0,0 +1,70 @@ +package mutator + +import ( + "context" + "strings" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" +) + +type selectResources struct{} + +// SelectResources returns a mutator that filters bundle resources to those listed in b.Select. +// Selectors may be "type.name" (e.g. "jobs.myjob") or just "name" if unique across all resource types. +// If b.Select is empty, this is a no-op. +func SelectResources() bundle.Mutator { + return &selectResources{} +} + +func (m *selectResources) Name() string { + return "SelectResources" +} + +func (m *selectResources) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { + if len(b.Select) == 0 { + return nil + } + + // Build reverse index: unqualified name → []"type.name" matches + byName := map[string][]string{} + for _, group := range b.Config.Resources.AllResources() { + typeName := group.Description.PluralName + for name := range group.Resources { + byName[name] = append(byName[name], typeName+"."+name) + } + } + + keep := map[string]struct{}{} + for _, selector := range b.Select { + if strings.Contains(selector, ".") { + typeName, name, _ := strings.Cut(selector, ".") + found := false + for _, group := range b.Config.Resources.AllResources() { + if group.Description.PluralName == typeName { + if _, ok := group.Resources[name]; ok { + found = true + } + break + } + } + if !found { + return diag.Errorf("no such resource: %s", selector) + } + keep[selector] = struct{}{} + } else { + matches := byName[selector] + switch len(matches) { + case 0: + return diag.Errorf("no such resource: %s", selector) + case 1: + keep[matches[0]] = struct{}{} + default: + return diag.Errorf("ambiguous resource: %s (can resolve to %s); use a qualified name to disambiguate", selector, strings.Join(matches, ", ")) + } + } + } + + b.Config.Resources.FilterResources(keep) + return nil +} diff --git a/bundle/config/mutator/select_resources_test.go b/bundle/config/mutator/select_resources_test.go new file mode 100644 index 00000000000..667085f0af1 --- /dev/null +++ b/bundle/config/mutator/select_resources_test.go @@ -0,0 +1,75 @@ +package mutator_test + +import ( + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/mutator" + "github.com/databricks/cli/bundle/config/resources" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func bundleWithJobsAndPipelines() *bundle.Bundle { + return &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Jobs: map[string]*resources.Job{"my_job": {}}, + Pipelines: map[string]*resources.Pipeline{"my_pipeline": {}}, + }, + }, + } +} + +func TestSelectResources_NoOp(t *testing.T) { + b := bundleWithJobsAndPipelines() + diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) + require.NoError(t, diags.Error()) + assert.Len(t, b.Config.Resources.Jobs, 1) + assert.Len(t, b.Config.Resources.Pipelines, 1) +} + +func TestSelectResources_UnqualifiedUnique(t *testing.T) { + b := bundleWithJobsAndPipelines() + b.Select = []string{"my_job"} + diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) + require.NoError(t, diags.Error()) + assert.Len(t, b.Config.Resources.Jobs, 1) + assert.Empty(t, b.Config.Resources.Pipelines) +} + +func TestSelectResources_QualifiedName(t *testing.T) { + b := bundleWithJobsAndPipelines() + b.Select = []string{"pipelines.my_pipeline"} + diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) + require.NoError(t, diags.Error()) + assert.Empty(t, b.Config.Resources.Jobs) + assert.Len(t, b.Config.Resources.Pipelines, 1) +} + +func TestSelectResources_NotFound(t *testing.T) { + b := bundleWithJobsAndPipelines() + b.Select = []string{"nonexistent"} + diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) + require.Error(t, diags.Error()) + assert.ErrorContains(t, diags.Error(), "no such resource: nonexistent") +} + +func TestSelectResources_QualifiedNotFound(t *testing.T) { + b := bundleWithJobsAndPipelines() + b.Select = []string{"jobs.nonexistent"} + diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) + require.Error(t, diags.Error()) + assert.ErrorContains(t, diags.Error(), "no such resource: jobs.nonexistent") +} + +func TestSelectResources_Ambiguous(t *testing.T) { + b := bundleWithJobsAndPipelines() + b.Config.Resources.Pipelines["my_job"] = &resources.Pipeline{} + b.Select = []string{"my_job"} + diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) + require.Error(t, diags.Error()) + assert.ErrorContains(t, diags.Error(), "ambiguous resource: my_job") + assert.ErrorContains(t, diags.Error(), "use a qualified name to disambiguate") +} diff --git a/bundle/config/resources.go b/bundle/config/resources.go index 2e840a5ef80..8d2e89e2410 100644 --- a/bundle/config/resources.go +++ b/bundle/config/resources.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "net/url" + "reflect" + "strings" "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/databricks-sdk-go" @@ -122,6 +124,29 @@ func (r *Resources) AllResources() []ResourceGroup { } } +// FilterResources removes all resources whose "typePlural.name" key is not in keep. +// It uses reflection to handle all resource types without manual enumeration. +func (r *Resources) FilterResources(keep map[string]struct{}) { + rv := reflect.ValueOf(r).Elem() + rt := rv.Type() + for i := range rt.NumField() { + field := rt.Field(i) + typeName, _, _ := strings.Cut(field.Tag.Get("json"), ",") + if typeName == "" || typeName == "-" { + continue + } + fv := rv.Field(i) + if fv.Kind() != reflect.Map || fv.IsNil() { + continue + } + for _, k := range fv.MapKeys() { + if _, ok := keep[typeName+"."+k.String()]; !ok { + fv.SetMapIndex(k, reflect.Value{}) + } + } + } +} + // FindResourceByConfigKey searches all resource maps for a resource with the given key. func (r *Resources) FindResourceByConfigKey(key string) (ConfigResource, error) { var found []ConfigResource diff --git a/bundle/phases/initialize.go b/bundle/phases/initialize.go index 440bf6c6652..d5e836f88f1 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -142,6 +142,11 @@ func Initialize(ctx context.Context, b *bundle.Bundle) { // After PythonMutator, mutators must not change bundle resources, or such changes are not // going to be visible in Python code. + // Filter resources to only those selected via --select, if any. + // Runs after all resource mutations so that dynamically added resources are visible, + // and before validation so that only selected resources are validated. + mutator.SelectResources(), + // Validate all required fields are set. This is run after variable interpolation and PyDABs mutators // since they can also set and modify resources. validate.Required(), diff --git a/cmd/bundle/deploy.go b/cmd/bundle/deploy.go index 31ffe7090d8..c73e820d917 100644 --- a/cmd/bundle/deploy.go +++ b/cmd/bundle/deploy.go @@ -31,6 +31,7 @@ See https://docs.databricks.com/en/dev-tools/bundles/index.html for more informa var autoApprove bool var verbose bool var readPlanPath string + var selectResources []string cmd.Flags().BoolVar(&force, "force", false, "Force-override Git branch validation.") cmd.Flags().BoolVar(&forceLock, "force-lock", false, "Force acquisition of deployment lock.") cmd.Flags().BoolVar(&failOnActiveRuns, "fail-on-active-runs", false, "Fail if there are running jobs or pipelines in the deployment.") @@ -40,6 +41,7 @@ See https://docs.databricks.com/en/dev-tools/bundles/index.html for more informa cmd.Flags().MarkDeprecated("compute-id", "use --cluster-id instead") cmd.Flags().BoolVar(&verbose, "verbose", false, "Enable verbose output.") cmd.Flags().StringVar(&readPlanPath, "plan", "", "Path to a JSON plan file to apply instead of planning (direct engine only).") + cmd.Flags().StringArrayVar(&selectResources, "select", nil, "Deploy only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated.") // Verbose flag currently only affects file sync output, it's used by the vscode extension cmd.Flags().MarkHidden("verbose") @@ -49,6 +51,7 @@ See https://docs.databricks.com/en/dev-tools/bundles/index.html for more informa b.Config.Bundle.Force = force b.Config.Bundle.Deployment.Lock.Force = forceLock b.AutoApprove = autoApprove + b.Select = selectResources if cmd.Flag("compute-id").Changed { b.Config.Bundle.ClusterId = clusterId diff --git a/cmd/bundle/plan.go b/cmd/bundle/plan.go index e3dd63929ed..e5b5e5eb505 100644 --- a/cmd/bundle/plan.go +++ b/cmd/bundle/plan.go @@ -28,10 +28,12 @@ It is useful for previewing changes before running 'bundle deploy'.`, var force bool var clusterId string + var selectResources []string cmd.Flags().BoolVar(&force, "force", false, "Force-override Git branch validation.") cmd.Flags().StringVar(&clusterId, "compute-id", "", "Override cluster in the deployment with the given compute ID.") cmd.Flags().StringVarP(&clusterId, "cluster-id", "c", "", "Override cluster in the deployment with the given cluster ID.") cmd.Flags().MarkDeprecated("compute-id", "use --cluster-id instead") + cmd.Flags().StringArrayVar(&selectResources, "select", nil, "Plan only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated.") cmd.RunE = func(cmd *cobra.Command, args []string) error { opts := utils.ProcessOptions{ @@ -41,18 +43,16 @@ It is useful for previewing changes before running 'bundle deploy'.`, PreDeployChecks: true, } - // Only add InitFunc if we need to set force or cluster ID - if force || cmd.Flag("compute-id").Changed || cmd.Flag("cluster-id").Changed { - opts.InitFunc = func(b *bundle.Bundle) { - b.Config.Bundle.Force = force + opts.InitFunc = func(b *bundle.Bundle) { + b.Config.Bundle.Force = force + b.Select = selectResources - if cmd.Flag("compute-id").Changed { - b.Config.Bundle.ClusterId = clusterId - } + if cmd.Flag("compute-id").Changed { + b.Config.Bundle.ClusterId = clusterId + } - if cmd.Flag("cluster-id").Changed { - b.Config.Bundle.ClusterId = clusterId - } + if cmd.Flag("cluster-id").Changed { + b.Config.Bundle.ClusterId = clusterId } } diff --git a/cmd/bundle/summary.go b/cmd/bundle/summary.go index b3a55a607cc..6b1b14e332e 100644 --- a/cmd/bundle/summary.go +++ b/cmd/bundle/summary.go @@ -21,9 +21,11 @@ Useful after deployment to see what was created and where to find it.`, var forcePull bool var includeLocations bool + var selectResources []string cmd.Flags().BoolVar(&forcePull, "force-pull", false, "Skip local cache and load the state from the remote workspace") cmd.Flags().BoolVar(&includeLocations, "include-locations", false, "Include location information in the output") cmd.Flags().MarkHidden("include-locations") + cmd.Flags().StringArrayVar(&selectResources, "select", nil, "Show only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated.") cmd.RunE = func(cmd *cobra.Command, args []string) error { b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ @@ -31,6 +33,9 @@ Useful after deployment to see what was created and where to find it.`, AlwaysPull: forcePull, IncludeLocations: includeLocations, InitIDs: true, + InitFunc: func(b *bundle.Bundle) { + b.Select = selectResources + }, }) if err != nil { return err diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index a2ec31f721b..116e2df5b09 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -46,14 +46,19 @@ Please run this command before deploying to ensure configuration quality.`, var includeLocations bool var strict bool + var selectResources []string cmd.Flags().BoolVar(&includeLocations, "include-locations", false, "Include location information in the output") cmd.Flags().MarkHidden("include-locations") cmd.Flags().BoolVar(&strict, "strict", false, "Treat warnings as errors") + cmd.Flags().StringArrayVar(&selectResources, "select", nil, "Validate only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated.") cmd.RunE = func(cmd *cobra.Command, args []string) error { b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ Validate: true, IncludeLocations: includeLocations, + InitFunc: func(b *bundle.Bundle) { + b.Select = selectResources + }, }) ctx := cmd.Context() From 5cb793e3a50212893b1800d3f2a4b2c0470ca5cf Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 1 Jun 2026 16:14:11 +0200 Subject: [PATCH 02/25] bundle: add tests for repeated --select and resource dependency - Unit tests: multiple selectors (both kept), dependency not auto-included - Acceptance: --select A --select B keeps both resources - Acceptance: selecting a resource that depends on another fails at plan time with an engine-specific error; output split per engine Co-authored-by: Isaac --- .../bundle/plan/select-dep/databricks.yml | 14 +++++++++++ .../select-dep/out.plan-single.direct.txt | 1 + .../select-dep/out.plan-single.terraform.txt | 9 +++++++ .../bundle/plan/select-dep/out.test.toml | 3 +++ acceptance/bundle/plan/select-dep/output.txt | 11 +++++++++ acceptance/bundle/plan/select-dep/script | 10 ++++++++ acceptance/bundle/plan/select-dep/test.toml | 2 ++ acceptance/bundle/plan/select/output.txt | 6 +++++ acceptance/bundle/plan/select/script | 5 +++- .../config/mutator/select_resources_test.go | 24 +++++++++++++++++++ 10 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 acceptance/bundle/plan/select-dep/databricks.yml create mode 100644 acceptance/bundle/plan/select-dep/out.plan-single.direct.txt create mode 100644 acceptance/bundle/plan/select-dep/out.plan-single.terraform.txt create mode 100644 acceptance/bundle/plan/select-dep/out.test.toml create mode 100644 acceptance/bundle/plan/select-dep/output.txt create mode 100644 acceptance/bundle/plan/select-dep/script create mode 100644 acceptance/bundle/plan/select-dep/test.toml diff --git a/acceptance/bundle/plan/select-dep/databricks.yml b/acceptance/bundle/plan/select-dep/databricks.yml new file mode 100644 index 00000000000..f77d9557684 --- /dev/null +++ b/acceptance/bundle/plan/select-dep/databricks.yml @@ -0,0 +1,14 @@ +bundle: + name: plan-select-dep + +resources: + jobs: + bar: + name: job-bar + + foo: + name: job-foo + tasks: + - task_key: run_bar + run_job_task: + job_id: ${resources.jobs.bar.id} diff --git a/acceptance/bundle/plan/select-dep/out.plan-single.direct.txt b/acceptance/bundle/plan/select-dep/out.plan-single.direct.txt new file mode 100644 index 00000000000..4cb68a39b5a --- /dev/null +++ b/acceptance/bundle/plan/select-dep/out.plan-single.direct.txt @@ -0,0 +1 @@ +Error: invalid dependency "${resources.jobs.bar.id}", no such node "resources.jobs.bar" diff --git a/acceptance/bundle/plan/select-dep/out.plan-single.terraform.txt b/acceptance/bundle/plan/select-dep/out.plan-single.terraform.txt new file mode 100644 index 00000000000..a86e90982c2 --- /dev/null +++ b/acceptance/bundle/plan/select-dep/out.plan-single.terraform.txt @@ -0,0 +1,9 @@ +Error: exit status 1 + +Error: Reference to undeclared resource + + on bundle.tf.json line 30, in resource.databricks_job.foo.task[0].run_job_task: + 30: "job_id": "${databricks_job.bar.id}" + +A managed resource "databricks_job" "bar" has not been declared in the root +module. diff --git a/acceptance/bundle/plan/select-dep/out.test.toml b/acceptance/bundle/plan/select-dep/out.test.toml new file mode 100644 index 00000000000..f784a183258 --- /dev/null +++ b/acceptance/bundle/plan/select-dep/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/plan/select-dep/output.txt b/acceptance/bundle/plan/select-dep/output.txt new file mode 100644 index 00000000000..f252435129f --- /dev/null +++ b/acceptance/bundle/plan/select-dep/output.txt @@ -0,0 +1,11 @@ + +>>> [CLI] bundle plan --select jobs.foo --select jobs.bar +create jobs.bar +create jobs.foo + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select jobs.bar +create jobs.bar + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select-dep/script b/acceptance/bundle/plan/select-dep/script new file mode 100644 index 00000000000..dea317f507e --- /dev/null +++ b/acceptance/bundle/plan/select-dep/script @@ -0,0 +1,10 @@ +# Both resources selected: foo can reference bar's id at deploy time. +trace $CLI bundle plan --select jobs.foo --select jobs.bar + +# Select the dependency alone: bar has no outgoing references. +trace $CLI bundle plan --select jobs.bar + +# Select only foo: the plan fails because foo depends on bar which is not in the plan. +# Error message differs per engine, so written to a per-engine file. +musterr $CLI bundle plan --select jobs.foo &> out.plan-single.$DATABRICKS_BUNDLE_ENGINE.txt +perl -0777 -i -pe 's/\n+$/\n/' out.plan-single.$DATABRICKS_BUNDLE_ENGINE.txt \ No newline at end of file diff --git a/acceptance/bundle/plan/select-dep/test.toml b/acceptance/bundle/plan/select-dep/test.toml new file mode 100644 index 00000000000..7d36fb9dc18 --- /dev/null +++ b/acceptance/bundle/plan/select-dep/test.toml @@ -0,0 +1,2 @@ +Local = true +Cloud = false diff --git a/acceptance/bundle/plan/select/output.txt b/acceptance/bundle/plan/select/output.txt index 783db33baf7..6261c3ab052 100644 --- a/acceptance/bundle/plan/select/output.txt +++ b/acceptance/bundle/plan/select/output.txt @@ -16,3 +16,9 @@ Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged create jobs.job_a Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select job_a --select job_b +create jobs.job_a +create jobs.job_b + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select/script b/acceptance/bundle/plan/select/script index 4e88d15e4d6..633dc216bc0 100644 --- a/acceptance/bundle/plan/select/script +++ b/acceptance/bundle/plan/select/script @@ -8,4 +8,7 @@ trace $CLI bundle plan --select jobs.no_such_job || true trace $CLI bundle plan --select job_b # Select a resource by qualified name -trace $CLI bundle plan --select jobs.job_a \ No newline at end of file +trace $CLI bundle plan --select jobs.job_a + +# Repeated --select: both resources are included +trace $CLI bundle plan --select job_a --select job_b \ No newline at end of file diff --git a/bundle/config/mutator/select_resources_test.go b/bundle/config/mutator/select_resources_test.go index 667085f0af1..23a19165f31 100644 --- a/bundle/config/mutator/select_resources_test.go +++ b/bundle/config/mutator/select_resources_test.go @@ -73,3 +73,27 @@ func TestSelectResources_Ambiguous(t *testing.T) { assert.ErrorContains(t, diags.Error(), "ambiguous resource: my_job") assert.ErrorContains(t, diags.Error(), "use a qualified name to disambiguate") } + +func TestSelectResources_MultipleSelectors(t *testing.T) { + b := bundleWithJobsAndPipelines() + b.Config.Resources.Jobs["other_job"] = &resources.Job{} + b.Select = []string{"my_job", "my_pipeline"} + diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) + require.NoError(t, diags.Error()) + assert.Len(t, b.Config.Resources.Jobs, 1) + assert.Contains(t, b.Config.Resources.Jobs, "my_job") + assert.Len(t, b.Config.Resources.Pipelines, 1) +} + +// TestSelectResources_DependencyNotAutoIncluded verifies that the SelectResources mutator +// itself does not auto-include resources referenced by selected ones. Downstream plan/deploy +// phases are responsible for catching unresolvable references. +func TestSelectResources_DependencyNotAutoIncluded(t *testing.T) { + b := bundleWithJobsAndPipelines() + b.Select = []string{"my_job"} + diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) + require.NoError(t, diags.Error()) + assert.Len(t, b.Config.Resources.Jobs, 1) + // my_pipeline was not selected and is not auto-included even if my_job references it. + assert.Empty(t, b.Config.Resources.Pipelines) +} From 76fdaca12e9b1541e771c990c41465fc614ba93d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 1 Jun 2026 16:31:31 +0200 Subject: [PATCH 03/25] bundle: auto-include transitive dependencies in --select MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a selected resource references another via an unresolved \${resources...*} variable, that dependency is automatically added to the selection. The expansion is transitive: A → B → C selects all three when only A is given. Co-authored-by: Isaac --- .../select-dep/out.plan-single.direct.txt | 1 - .../select-dep/out.plan-single.terraform.txt | 9 --- acceptance/bundle/plan/select-dep/output.txt | 6 ++ acceptance/bundle/plan/select-dep/script | 8 +-- bundle/config/mutator/select_resources.go | 58 +++++++++++++++++++ .../config/mutator/select_resources_test.go | 45 +++++++++++--- 6 files changed, 103 insertions(+), 24 deletions(-) delete mode 100644 acceptance/bundle/plan/select-dep/out.plan-single.direct.txt delete mode 100644 acceptance/bundle/plan/select-dep/out.plan-single.terraform.txt diff --git a/acceptance/bundle/plan/select-dep/out.plan-single.direct.txt b/acceptance/bundle/plan/select-dep/out.plan-single.direct.txt deleted file mode 100644 index 4cb68a39b5a..00000000000 --- a/acceptance/bundle/plan/select-dep/out.plan-single.direct.txt +++ /dev/null @@ -1 +0,0 @@ -Error: invalid dependency "${resources.jobs.bar.id}", no such node "resources.jobs.bar" diff --git a/acceptance/bundle/plan/select-dep/out.plan-single.terraform.txt b/acceptance/bundle/plan/select-dep/out.plan-single.terraform.txt deleted file mode 100644 index a86e90982c2..00000000000 --- a/acceptance/bundle/plan/select-dep/out.plan-single.terraform.txt +++ /dev/null @@ -1,9 +0,0 @@ -Error: exit status 1 - -Error: Reference to undeclared resource - - on bundle.tf.json line 30, in resource.databricks_job.foo.task[0].run_job_task: - 30: "job_id": "${databricks_job.bar.id}" - -A managed resource "databricks_job" "bar" has not been declared in the root -module. diff --git a/acceptance/bundle/plan/select-dep/output.txt b/acceptance/bundle/plan/select-dep/output.txt index f252435129f..148d316ffa5 100644 --- a/acceptance/bundle/plan/select-dep/output.txt +++ b/acceptance/bundle/plan/select-dep/output.txt @@ -9,3 +9,9 @@ Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged create jobs.bar Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select jobs.foo +create jobs.bar +create jobs.foo + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select-dep/script b/acceptance/bundle/plan/select-dep/script index dea317f507e..932953fa889 100644 --- a/acceptance/bundle/plan/select-dep/script +++ b/acceptance/bundle/plan/select-dep/script @@ -1,10 +1,8 @@ -# Both resources selected: foo can reference bar's id at deploy time. +# Both resources selected explicitly. trace $CLI bundle plan --select jobs.foo --select jobs.bar # Select the dependency alone: bar has no outgoing references. trace $CLI bundle plan --select jobs.bar -# Select only foo: the plan fails because foo depends on bar which is not in the plan. -# Error message differs per engine, so written to a per-engine file. -musterr $CLI bundle plan --select jobs.foo &> out.plan-single.$DATABRICKS_BUNDLE_ENGINE.txt -perl -0777 -i -pe 's/\n+$/\n/' out.plan-single.$DATABRICKS_BUNDLE_ENGINE.txt \ No newline at end of file +# Select only foo: bar is auto-included because foo references ${resources.jobs.bar.id}. +trace $CLI bundle plan --select jobs.foo \ No newline at end of file diff --git a/bundle/config/mutator/select_resources.go b/bundle/config/mutator/select_resources.go index 7a55ce0bebb..2ef497e221d 100644 --- a/bundle/config/mutator/select_resources.go +++ b/bundle/config/mutator/select_resources.go @@ -6,12 +6,15 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/dyn/dynvar" ) type selectResources struct{} // SelectResources returns a mutator that filters bundle resources to those listed in b.Select. // Selectors may be "type.name" (e.g. "jobs.myjob") or just "name" if unique across all resource types. +// Dependencies referenced via ${resources.*.*.*} are included transitively. // If b.Select is empty, this is a no-op. func SelectResources() bundle.Mutator { return &selectResources{} @@ -65,6 +68,61 @@ func (m *selectResources) Apply(_ context.Context, b *bundle.Bundle) diag.Diagno } } + // Expand keep set transitively: for each kept resource, find resources it + // references via unresolved ${resources...*} variables and add + // them too. Repeat until no new resources are discovered. + configVal := b.Config.Value() + queue := make([]string, 0, len(keep)) + for key := range keep { + queue = append(queue, key) + } + for len(queue) > 0 { + key := queue[0] + queue = queue[1:] + for _, dep := range resourceDeps(configVal, key) { + if _, ok := keep[dep]; !ok { + keep[dep] = struct{}{} + queue = append(queue, dep) + } + } + } + b.Config.Resources.FilterResources(keep) return nil } + +// resourceDeps returns the "type.name" keys of resources referenced by unresolved +// ${resources...*} variables inside the given resource's config subtree. +func resourceDeps(root dyn.Value, key string) []string { + path, err := dyn.NewPathFromString("resources." + key) + if err != nil { + return nil + } + val, err := dyn.GetByPath(root, path) + if err != nil { + return nil + } + + seen := map[string]bool{} + var deps []string + _ = dyn.WalkReadOnly(val, func(_ dyn.Path, v dyn.Value) error { + ref, ok := dynvar.NewRef(v) + if !ok { + return nil + } + for _, pathStr := range ref.References() { + // pathStr is like "resources.jobs.bar.id"; extract "jobs.bar" + parts := strings.SplitN(pathStr, ".", 4) + if len(parts) < 3 || parts[0] != "resources" { + continue + } + dep := parts[1] + "." + parts[2] + if !seen[dep] { + seen[dep] = true + deps = append(deps, dep) + } + } + return nil + }) + return deps +} diff --git a/bundle/config/mutator/select_resources_test.go b/bundle/config/mutator/select_resources_test.go index 23a19165f31..b187ff93d6d 100644 --- a/bundle/config/mutator/select_resources_test.go +++ b/bundle/config/mutator/select_resources_test.go @@ -9,6 +9,7 @@ import ( "github.com/databricks/cli/bundle/config/resources" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/databricks/databricks-sdk-go/service/jobs" ) func bundleWithJobsAndPipelines() *bundle.Bundle { @@ -85,15 +86,41 @@ func TestSelectResources_MultipleSelectors(t *testing.T) { assert.Len(t, b.Config.Resources.Pipelines, 1) } -// TestSelectResources_DependencyNotAutoIncluded verifies that the SelectResources mutator -// itself does not auto-include resources referenced by selected ones. Downstream plan/deploy -// phases are responsible for catching unresolvable references. -func TestSelectResources_DependencyNotAutoIncluded(t *testing.T) { - b := bundleWithJobsAndPipelines() - b.Select = []string{"my_job"} +func TestSelectResources_DependencyAutoIncluded(t *testing.T) { + // foo references bar via ${resources.jobs.bar.id}; selecting foo alone + // should automatically include bar (and transitively its deps). + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Jobs: map[string]*resources.Job{ + "foo": {JobSettings: jobs.JobSettings{Description: "${resources.jobs.bar.id}"}}, + "bar": {}, + }, + }, + }, + } + b.Select = []string{"jobs.foo"} diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) require.NoError(t, diags.Error()) - assert.Len(t, b.Config.Resources.Jobs, 1) - // my_pipeline was not selected and is not auto-included even if my_job references it. - assert.Empty(t, b.Config.Resources.Pipelines) + assert.Contains(t, b.Config.Resources.Jobs, "foo") + assert.Contains(t, b.Config.Resources.Jobs, "bar") +} + +func TestSelectResources_TransitiveDependency(t *testing.T) { + // foo → bar → baz; selecting foo alone should include all three. + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Jobs: map[string]*resources.Job{ + "foo": {JobSettings: jobs.JobSettings{Description: "${resources.jobs.bar.id}"}}, + "bar": {JobSettings: jobs.JobSettings{Description: "${resources.jobs.baz.id}"}}, + "baz": {}, + }, + }, + }, + } + b.Select = []string{"jobs.foo"} + diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) + require.NoError(t, diags.Error()) + assert.Len(t, b.Config.Resources.Jobs, 3) } From 6506b1cb4259113a08c27f165749dc526b97ec53 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 1 Jun 2026 17:00:58 +0200 Subject: [PATCH 04/25] bundle: use plan DependsOn graph for --select dependency expansion Remove the dyn.Value BFS from the SelectResources mutator. Dependency auto-inclusion now uses the DependsOn entries that CalculatePlan already computes: - Direct engine: Plan.FilterToSelected BFS-expands the selection using DependsOn, then RunPlan returns the filtered plan. - Terraform engine: config is filtered to the exact selection before writing the terraform JSON (no dep tracking). - Validate/summary: simple config filter applied post-Initialize. New deployplan.Plan.FilterToSelected method and unit tests. Co-authored-by: Isaac --- .../select-dep/out.select-single.direct.txt | 5 ++ .../out.select-single.terraform.txt | 12 +++ acceptance/bundle/plan/select-dep/output.txt | 6 -- acceptance/bundle/plan/select-dep/script | 9 +- acceptance/bundle/plan/select-dep/test.toml | 1 + bundle/config/mutator/select_resources.go | 89 ++++++------------- .../config/mutator/select_resources_test.go | 55 +++--------- bundle/deployplan/plan.go | 36 ++++++++ bundle/deployplan/plan_filter_test.go | 48 ++++++++++ bundle/phases/deploy.go | 9 ++ cmd/bundle/utils/process.go | 12 +++ 11 files changed, 171 insertions(+), 111 deletions(-) create mode 100644 acceptance/bundle/plan/select-dep/out.select-single.direct.txt create mode 100644 acceptance/bundle/plan/select-dep/out.select-single.terraform.txt create mode 100644 bundle/deployplan/plan_filter_test.go diff --git a/acceptance/bundle/plan/select-dep/out.select-single.direct.txt b/acceptance/bundle/plan/select-dep/out.select-single.direct.txt new file mode 100644 index 00000000000..71f2d9c3818 --- /dev/null +++ b/acceptance/bundle/plan/select-dep/out.select-single.direct.txt @@ -0,0 +1,5 @@ +create jobs.bar +create jobs.foo + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged +Exit code: 0 diff --git a/acceptance/bundle/plan/select-dep/out.select-single.terraform.txt b/acceptance/bundle/plan/select-dep/out.select-single.terraform.txt new file mode 100644 index 00000000000..2168444bf36 --- /dev/null +++ b/acceptance/bundle/plan/select-dep/out.select-single.terraform.txt @@ -0,0 +1,12 @@ +Error: exit status 1 + +Error: Reference to undeclared resource + + on bundle.tf.json line 30, in resource.databricks_job.foo.task[0].run_job_task: + 30: "job_id": "${databricks_job.bar.id}" + +A managed resource "databricks_job" "bar" has not been declared in the root +module. + + +Exit code: 1 diff --git a/acceptance/bundle/plan/select-dep/output.txt b/acceptance/bundle/plan/select-dep/output.txt index 148d316ffa5..f252435129f 100644 --- a/acceptance/bundle/plan/select-dep/output.txt +++ b/acceptance/bundle/plan/select-dep/output.txt @@ -9,9 +9,3 @@ Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged create jobs.bar Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle plan --select jobs.foo -create jobs.bar -create jobs.foo - -Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select-dep/script b/acceptance/bundle/plan/select-dep/script index 932953fa889..aa6bda85068 100644 --- a/acceptance/bundle/plan/select-dep/script +++ b/acceptance/bundle/plan/select-dep/script @@ -4,5 +4,10 @@ trace $CLI bundle plan --select jobs.foo --select jobs.bar # Select the dependency alone: bar has no outgoing references. trace $CLI bundle plan --select jobs.bar -# Select only foo: bar is auto-included because foo references ${resources.jobs.bar.id}. -trace $CLI bundle plan --select jobs.foo \ No newline at end of file +# Select only foo. For the direct engine bar is auto-included via the DependsOn +# graph; for terraform deps are not tracked at this stage so the output differs. +set +e +$CLI bundle plan --select jobs.foo > out.select-single.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 +echo "Exit code: $?" >> out.select-single.$DATABRICKS_BUNDLE_ENGINE.txt +set -e +perl -0777 -i -pe 's/\n+$/\n/' out.select-single.$DATABRICKS_BUNDLE_ENGINE.txt \ No newline at end of file diff --git a/acceptance/bundle/plan/select-dep/test.toml b/acceptance/bundle/plan/select-dep/test.toml index 7d36fb9dc18..85e02532c93 100644 --- a/acceptance/bundle/plan/select-dep/test.toml +++ b/acceptance/bundle/plan/select-dep/test.toml @@ -1,2 +1,3 @@ Local = true Cloud = false +Ignore = [".databricks"] diff --git a/bundle/config/mutator/select_resources.go b/bundle/config/mutator/select_resources.go index 2ef497e221d..20715028ffc 100644 --- a/bundle/config/mutator/select_resources.go +++ b/bundle/config/mutator/select_resources.go @@ -6,15 +6,14 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/dyn" - "github.com/databricks/cli/libs/dyn/dynvar" ) type selectResources struct{} -// SelectResources returns a mutator that filters bundle resources to those listed in b.Select. -// Selectors may be "type.name" (e.g. "jobs.myjob") or just "name" if unique across all resource types. -// Dependencies referenced via ${resources.*.*.*} are included transitively. +// SelectResources returns a mutator that resolves and validates the selectors in +// b.Select. Selectors may be "type.name" (e.g. "jobs.myjob") or just "name" if +// unique across all resource types. The mutator does not filter the config; callers +// are responsible for filtering (via the plan graph or a direct config filter). // If b.Select is empty, this is a no-op. func SelectResources() bundle.Mutator { return &selectResources{} @@ -29,7 +28,7 @@ func (m *selectResources) Apply(_ context.Context, b *bundle.Bundle) diag.Diagno return nil } - // Build reverse index: unqualified name → []"type.name" matches + // Build reverse index: unqualified name → []"type.name" matches. byName := map[string][]string{} for _, group := range b.Config.Resources.AllResources() { typeName := group.Description.PluralName @@ -38,7 +37,7 @@ func (m *selectResources) Apply(_ context.Context, b *bundle.Bundle) diag.Diagno } } - keep := map[string]struct{}{} + resolved := make([]string, 0, len(b.Select)) for _, selector := range b.Select { if strings.Contains(selector, ".") { typeName, name, _ := strings.Cut(selector, ".") @@ -54,75 +53,45 @@ func (m *selectResources) Apply(_ context.Context, b *bundle.Bundle) diag.Diagno if !found { return diag.Errorf("no such resource: %s", selector) } - keep[selector] = struct{}{} + resolved = append(resolved, selector) } else { matches := byName[selector] switch len(matches) { case 0: return diag.Errorf("no such resource: %s", selector) case 1: - keep[matches[0]] = struct{}{} + resolved = append(resolved, matches[0]) default: return diag.Errorf("ambiguous resource: %s (can resolve to %s); use a qualified name to disambiguate", selector, strings.Join(matches, ", ")) } } } - // Expand keep set transitively: for each kept resource, find resources it - // references via unresolved ${resources...*} variables and add - // them too. Repeat until no new resources are discovered. - configVal := b.Config.Value() - queue := make([]string, 0, len(keep)) - for key := range keep { - queue = append(queue, key) - } - for len(queue) > 0 { - key := queue[0] - queue = queue[1:] - for _, dep := range resourceDeps(configVal, key) { - if _, ok := keep[dep]; !ok { - keep[dep] = struct{}{} - queue = append(queue, dep) - } - } - } - - b.Config.Resources.FilterResources(keep) + b.Select = resolved return nil } -// resourceDeps returns the "type.name" keys of resources referenced by unresolved -// ${resources...*} variables inside the given resource's config subtree. -func resourceDeps(root dyn.Value, key string) []string { - path, err := dyn.NewPathFromString("resources." + key) - if err != nil { +// FilterSelectedResources filters b.Config.Resources to only the resources in +// b.Select (exact match, no dependency expansion). Used for commands that don't +// compute a deployment plan (validate, summary, terraform). +func FilterSelectedResources() bundle.Mutator { + return &filterSelectedResources{} +} + +type filterSelectedResources struct{} + +func (m *filterSelectedResources) Name() string { + return "FilterSelectedResources" +} + +func (m *filterSelectedResources) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { + if len(b.Select) == 0 { return nil } - val, err := dyn.GetByPath(root, path) - if err != nil { - return nil + keep := make(map[string]struct{}, len(b.Select)) + for _, key := range b.Select { + keep[key] = struct{}{} } - - seen := map[string]bool{} - var deps []string - _ = dyn.WalkReadOnly(val, func(_ dyn.Path, v dyn.Value) error { - ref, ok := dynvar.NewRef(v) - if !ok { - return nil - } - for _, pathStr := range ref.References() { - // pathStr is like "resources.jobs.bar.id"; extract "jobs.bar" - parts := strings.SplitN(pathStr, ".", 4) - if len(parts) < 3 || parts[0] != "resources" { - continue - } - dep := parts[1] + "." + parts[2] - if !seen[dep] { - seen[dep] = true - deps = append(deps, dep) - } - } - return nil - }) - return deps + b.Config.Resources.FilterResources(keep) + return nil } diff --git a/bundle/config/mutator/select_resources_test.go b/bundle/config/mutator/select_resources_test.go index b187ff93d6d..68bdecbfca4 100644 --- a/bundle/config/mutator/select_resources_test.go +++ b/bundle/config/mutator/select_resources_test.go @@ -9,7 +9,6 @@ import ( "github.com/databricks/cli/bundle/config/resources" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/databricks/databricks-sdk-go/service/jobs" ) func bundleWithJobsAndPipelines() *bundle.Bundle { @@ -27,6 +26,7 @@ func TestSelectResources_NoOp(t *testing.T) { b := bundleWithJobsAndPipelines() diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) require.NoError(t, diags.Error()) + // Mutator does not filter config — both resources remain. assert.Len(t, b.Config.Resources.Jobs, 1) assert.Len(t, b.Config.Resources.Pipelines, 1) } @@ -36,8 +36,10 @@ func TestSelectResources_UnqualifiedUnique(t *testing.T) { b.Select = []string{"my_job"} diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) require.NoError(t, diags.Error()) + // Selector resolved to qualified form; config not filtered. + assert.Equal(t, []string{"jobs.my_job"}, b.Select) assert.Len(t, b.Config.Resources.Jobs, 1) - assert.Empty(t, b.Config.Resources.Pipelines) + assert.Len(t, b.Config.Resources.Pipelines, 1) } func TestSelectResources_QualifiedName(t *testing.T) { @@ -45,8 +47,7 @@ func TestSelectResources_QualifiedName(t *testing.T) { b.Select = []string{"pipelines.my_pipeline"} diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) require.NoError(t, diags.Error()) - assert.Empty(t, b.Config.Resources.Jobs) - assert.Len(t, b.Config.Resources.Pipelines, 1) + assert.Equal(t, []string{"pipelines.my_pipeline"}, b.Select) } func TestSelectResources_NotFound(t *testing.T) { @@ -81,46 +82,14 @@ func TestSelectResources_MultipleSelectors(t *testing.T) { b.Select = []string{"my_job", "my_pipeline"} diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) require.NoError(t, diags.Error()) - assert.Len(t, b.Config.Resources.Jobs, 1) - assert.Contains(t, b.Config.Resources.Jobs, "my_job") - assert.Len(t, b.Config.Resources.Pipelines, 1) + assert.Equal(t, []string{"jobs.my_job", "pipelines.my_pipeline"}, b.Select) } -func TestSelectResources_DependencyAutoIncluded(t *testing.T) { - // foo references bar via ${resources.jobs.bar.id}; selecting foo alone - // should automatically include bar (and transitively its deps). - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Jobs: map[string]*resources.Job{ - "foo": {JobSettings: jobs.JobSettings{Description: "${resources.jobs.bar.id}"}}, - "bar": {}, - }, - }, - }, - } - b.Select = []string{"jobs.foo"} - diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) - require.NoError(t, diags.Error()) - assert.Contains(t, b.Config.Resources.Jobs, "foo") - assert.Contains(t, b.Config.Resources.Jobs, "bar") -} - -func TestSelectResources_TransitiveDependency(t *testing.T) { - // foo → bar → baz; selecting foo alone should include all three. - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Jobs: map[string]*resources.Job{ - "foo": {JobSettings: jobs.JobSettings{Description: "${resources.jobs.bar.id}"}}, - "bar": {JobSettings: jobs.JobSettings{Description: "${resources.jobs.baz.id}"}}, - "baz": {}, - }, - }, - }, - } - b.Select = []string{"jobs.foo"} - diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) +func TestFilterSelectedResources(t *testing.T) { + b := bundleWithJobsAndPipelines() + b.Select = []string{"jobs.my_job"} + diags := bundle.Apply(t.Context(), b, mutator.FilterSelectedResources()) require.NoError(t, diags.Error()) - assert.Len(t, b.Config.Resources.Jobs, 3) + assert.Len(t, b.Config.Resources.Jobs, 1) + assert.Empty(t, b.Config.Resources.Pipelines) } diff --git a/bundle/deployplan/plan.go b/bundle/deployplan/plan.go index 2d06b5fdd25..2fb5d38c806 100644 --- a/bundle/deployplan/plan.go +++ b/bundle/deployplan/plan.go @@ -202,6 +202,42 @@ func (p *Plan) RemoveEntry(resourceKey string) { delete(p.Plan, resourceKey) } +// FilterToSelected reduces the plan to the nodes in selected (format "type.name", +// e.g. "jobs.my_job") plus their transitive dependencies as recorded in each +// entry's DependsOn field. Nodes not reachable from the selected set are removed. +func (p *Plan) FilterToSelected(selected []string) { + // Convert "type.name" → "resources.type.name" (plan key format). + queue := make([]string, 0, len(selected)) + reachable := make(map[string]struct{}, len(selected)) + for _, s := range selected { + key := "resources." + s + if _, ok := p.Plan[key]; ok { + reachable[key] = struct{}{} + queue = append(queue, key) + } + } + + // BFS following DependsOn edges to include transitive dependencies. + for len(queue) > 0 { + key := queue[0] + queue = queue[1:] + for _, dep := range p.Plan[key].DependsOn { + if _, seen := reachable[dep.Node]; !seen { + if _, ok := p.Plan[dep.Node]; ok { + reachable[dep.Node] = struct{}{} + queue = append(queue, dep.Node) + } + } + } + } + + for key := range p.Plan { + if _, ok := reachable[key]; !ok { + delete(p.Plan, key) + } + } +} + type lockmap struct { state map[string]int } diff --git a/bundle/deployplan/plan_filter_test.go b/bundle/deployplan/plan_filter_test.go new file mode 100644 index 00000000000..b16d78eb43d --- /dev/null +++ b/bundle/deployplan/plan_filter_test.go @@ -0,0 +1,48 @@ +package deployplan_test + +import ( + "testing" + + "github.com/databricks/cli/bundle/deployplan" + "github.com/stretchr/testify/assert" +) + +func planWithDeps() *deployplan.Plan { + p := deployplan.NewPlanDirect() + p.Plan["resources.jobs.foo"] = &deployplan.PlanEntry{ + DependsOn: []deployplan.DependsOnEntry{{Node: "resources.jobs.bar"}}, + } + p.Plan["resources.jobs.bar"] = &deployplan.PlanEntry{ + DependsOn: []deployplan.DependsOnEntry{{Node: "resources.jobs.baz"}}, + } + p.Plan["resources.jobs.baz"] = &deployplan.PlanEntry{} + p.Plan["resources.jobs.independent"] = &deployplan.PlanEntry{} + return p +} + +func TestFilterToSelected_Direct(t *testing.T) { + p := planWithDeps() + p.FilterToSelected([]string{"jobs.foo"}) + assert.Contains(t, p.Plan, "resources.jobs.foo") + assert.Contains(t, p.Plan, "resources.jobs.bar") + assert.Contains(t, p.Plan, "resources.jobs.baz") + assert.NotContains(t, p.Plan, "resources.jobs.independent") +} + +func TestFilterToSelected_NoDeps(t *testing.T) { + p := planWithDeps() + p.FilterToSelected([]string{"jobs.baz"}) + assert.Contains(t, p.Plan, "resources.jobs.baz") + assert.NotContains(t, p.Plan, "resources.jobs.foo") + assert.NotContains(t, p.Plan, "resources.jobs.bar") + assert.NotContains(t, p.Plan, "resources.jobs.independent") +} + +func TestFilterToSelected_Multiple(t *testing.T) { + p := planWithDeps() + p.FilterToSelected([]string{"jobs.baz", "jobs.independent"}) + assert.Contains(t, p.Plan, "resources.jobs.baz") + assert.Contains(t, p.Plan, "resources.jobs.independent") + assert.NotContains(t, p.Plan, "resources.jobs.foo") + assert.NotContains(t, p.Plan, "resources.jobs.bar") +} diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index 15546880b9a..d3d024e2feb 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -8,6 +8,7 @@ import ( "github.com/databricks/cli/bundle/artifacts" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/engine" + "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/bundle/deploy" "github.com/databricks/cli/bundle/deploy/files" "github.com/databricks/cli/bundle/deploy/lock" @@ -212,9 +213,17 @@ func RunPlan(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) *d logdiag.LogError(ctx, err) return nil } + if len(b.Select) > 0 { + plan.FilterToSelected(b.Select) + } return plan } + // Terraform engine: filter config to the selected set before writing terraform JSON. + if len(b.Select) > 0 { + bundle.ApplyContext(ctx, b, mutator.FilterSelectedResources()) + } + bundle.ApplySeqContext(ctx, b, terraform.Interpolate(), terraform.Write(), diff --git a/cmd/bundle/utils/process.go b/cmd/bundle/utils/process.go index 5f43cff6acd..37bd62e9f3f 100644 --- a/cmd/bundle/utils/process.go +++ b/cmd/bundle/utils/process.go @@ -170,6 +170,18 @@ func ProcessBundleRet(cmd *cobra.Command, opts ProcessOptions) (b *bundle.Bundle } } + // For commands that don't compute a deployment plan (validate, summary), + // apply a simple exact-match config filter now. For plan/deploy with the + // direct engine, RunPlan calls plan.FilterToSelected which uses the + // DependsOn graph to auto-include transitive dependencies; the terraform + // engine is filtered inside RunPlan before writing the terraform JSON. + if len(b.Select) > 0 && !opts.PreDeployChecks && !opts.Deploy && opts.ReadPlanPath == "" { + bundle.ApplyContext(ctx, b, mutator.FilterSelectedResources()) + if logdiag.HasError(ctx) { + return b, nil, root.ErrAlreadyPrinted + } + } + shouldReadState := opts.ReadState || opts.AlwaysPull || opts.InitIDs || opts.ErrorOnEmptyState || opts.PreDeployChecks || opts.Deploy || opts.ReadPlanPath != "" if shouldReadState { From 699d77896d426ea7335ab8d783dc534a6fe31531 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 1 Jun 2026 17:29:37 +0200 Subject: [PATCH 05/25] update outputs + newlines --- acceptance/bundle/help/bundle-deploy/output.txt | 1 + acceptance/bundle/help/bundle-summary/output.txt | 5 +++-- acceptance/bundle/help/bundle-validate/output.txt | 5 +++-- acceptance/bundle/plan/select-dep/script | 2 +- acceptance/bundle/plan/select/script | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/acceptance/bundle/help/bundle-deploy/output.txt b/acceptance/bundle/help/bundle-deploy/output.txt index d9c5b1f14c2..9ff5924a4e2 100644 --- a/acceptance/bundle/help/bundle-deploy/output.txt +++ b/acceptance/bundle/help/bundle-deploy/output.txt @@ -20,6 +20,7 @@ Flags: --force-lock Force acquisition of deployment lock. -h, --help help for deploy --plan string Path to a JSON plan file to apply instead of planning (direct engine only). + --select stringArray Deploy only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated. Global Flags: --debug enable debug logging diff --git a/acceptance/bundle/help/bundle-summary/output.txt b/acceptance/bundle/help/bundle-summary/output.txt index d3c0c961edb..97237358725 100644 --- a/acceptance/bundle/help/bundle-summary/output.txt +++ b/acceptance/bundle/help/bundle-summary/output.txt @@ -7,8 +7,9 @@ Usage: databricks bundle summary [flags] Flags: - --force-pull Skip local cache and load the state from the remote workspace - -h, --help help for summary + --force-pull Skip local cache and load the state from the remote workspace + -h, --help help for summary + --select stringArray Show only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated. Global Flags: --debug enable debug logging diff --git a/acceptance/bundle/help/bundle-validate/output.txt b/acceptance/bundle/help/bundle-validate/output.txt index dbb15761b31..31d81c9308d 100644 --- a/acceptance/bundle/help/bundle-validate/output.txt +++ b/acceptance/bundle/help/bundle-validate/output.txt @@ -15,8 +15,9 @@ Usage: databricks bundle validate [flags] Flags: - -h, --help help for validate - --strict Treat warnings as errors + -h, --help help for validate + --select stringArray Validate only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated. + --strict Treat warnings as errors Global Flags: --debug enable debug logging diff --git a/acceptance/bundle/plan/select-dep/script b/acceptance/bundle/plan/select-dep/script index aa6bda85068..cb4739f43fc 100644 --- a/acceptance/bundle/plan/select-dep/script +++ b/acceptance/bundle/plan/select-dep/script @@ -10,4 +10,4 @@ set +e $CLI bundle plan --select jobs.foo > out.select-single.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 echo "Exit code: $?" >> out.select-single.$DATABRICKS_BUNDLE_ENGINE.txt set -e -perl -0777 -i -pe 's/\n+$/\n/' out.select-single.$DATABRICKS_BUNDLE_ENGINE.txt \ No newline at end of file +perl -0777 -i -pe 's/\n+$/\n/' out.select-single.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/plan/select/script b/acceptance/bundle/plan/select/script index 633dc216bc0..59aa4d9e9ea 100644 --- a/acceptance/bundle/plan/select/script +++ b/acceptance/bundle/plan/select/script @@ -11,4 +11,4 @@ trace $CLI bundle plan --select job_b trace $CLI bundle plan --select jobs.job_a # Repeated --select: both resources are included -trace $CLI bundle plan --select job_a --select job_b \ No newline at end of file +trace $CLI bundle plan --select job_a --select job_b From 34a70dbe1f7b7031149c4f62e0741ffac4223213 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 1 Jun 2026 17:32:05 +0200 Subject: [PATCH 06/25] bundle: use StringSlice for --select to match --var convention StringArrayVar rendered an ugly "stringArray" type in --help and did not split comma-separated values. Switch to StringSliceVar, matching the existing --var flag: cleaner "strings" help label and support for --select a,b in addition to repeated --select. Co-authored-by: Isaac --- acceptance/bundle/plan/select/output.txt | 6 ++++++ acceptance/bundle/plan/select/script | 3 +++ cmd/bundle/deploy.go | 2 +- cmd/bundle/plan.go | 2 +- cmd/bundle/summary.go | 2 +- cmd/bundle/validate.go | 2 +- 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/acceptance/bundle/plan/select/output.txt b/acceptance/bundle/plan/select/output.txt index 6261c3ab052..6993b7303d4 100644 --- a/acceptance/bundle/plan/select/output.txt +++ b/acceptance/bundle/plan/select/output.txt @@ -22,3 +22,9 @@ create jobs.job_a create jobs.job_b Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select job_a,job_b +create jobs.job_a +create jobs.job_b + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select/script b/acceptance/bundle/plan/select/script index 59aa4d9e9ea..9351613fc48 100644 --- a/acceptance/bundle/plan/select/script +++ b/acceptance/bundle/plan/select/script @@ -12,3 +12,6 @@ trace $CLI bundle plan --select jobs.job_a # Repeated --select: both resources are included trace $CLI bundle plan --select job_a --select job_b + +# Comma-separated --select: both resources are included +trace $CLI bundle plan --select job_a,job_b diff --git a/cmd/bundle/deploy.go b/cmd/bundle/deploy.go index c73e820d917..95776eb25f4 100644 --- a/cmd/bundle/deploy.go +++ b/cmd/bundle/deploy.go @@ -41,7 +41,7 @@ See https://docs.databricks.com/en/dev-tools/bundles/index.html for more informa cmd.Flags().MarkDeprecated("compute-id", "use --cluster-id instead") cmd.Flags().BoolVar(&verbose, "verbose", false, "Enable verbose output.") cmd.Flags().StringVar(&readPlanPath, "plan", "", "Path to a JSON plan file to apply instead of planning (direct engine only).") - cmd.Flags().StringArrayVar(&selectResources, "select", nil, "Deploy only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated.") + cmd.Flags().StringSliceVar(&selectResources, "select", nil, "Deploy only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated or comma-separated.") // Verbose flag currently only affects file sync output, it's used by the vscode extension cmd.Flags().MarkHidden("verbose") diff --git a/cmd/bundle/plan.go b/cmd/bundle/plan.go index e5b5e5eb505..9c7f22365d0 100644 --- a/cmd/bundle/plan.go +++ b/cmd/bundle/plan.go @@ -33,7 +33,7 @@ It is useful for previewing changes before running 'bundle deploy'.`, cmd.Flags().StringVar(&clusterId, "compute-id", "", "Override cluster in the deployment with the given compute ID.") cmd.Flags().StringVarP(&clusterId, "cluster-id", "c", "", "Override cluster in the deployment with the given cluster ID.") cmd.Flags().MarkDeprecated("compute-id", "use --cluster-id instead") - cmd.Flags().StringArrayVar(&selectResources, "select", nil, "Plan only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated.") + cmd.Flags().StringSliceVar(&selectResources, "select", nil, "Plan only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated or comma-separated.") cmd.RunE = func(cmd *cobra.Command, args []string) error { opts := utils.ProcessOptions{ diff --git a/cmd/bundle/summary.go b/cmd/bundle/summary.go index 6b1b14e332e..aa310aa0284 100644 --- a/cmd/bundle/summary.go +++ b/cmd/bundle/summary.go @@ -25,7 +25,7 @@ Useful after deployment to see what was created and where to find it.`, cmd.Flags().BoolVar(&forcePull, "force-pull", false, "Skip local cache and load the state from the remote workspace") cmd.Flags().BoolVar(&includeLocations, "include-locations", false, "Include location information in the output") cmd.Flags().MarkHidden("include-locations") - cmd.Flags().StringArrayVar(&selectResources, "select", nil, "Show only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated.") + cmd.Flags().StringSliceVar(&selectResources, "select", nil, "Show only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated or comma-separated.") cmd.RunE = func(cmd *cobra.Command, args []string) error { b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index 116e2df5b09..8d1e8d0c5a5 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -50,7 +50,7 @@ Please run this command before deploying to ensure configuration quality.`, cmd.Flags().BoolVar(&includeLocations, "include-locations", false, "Include location information in the output") cmd.Flags().MarkHidden("include-locations") cmd.Flags().BoolVar(&strict, "strict", false, "Treat warnings as errors") - cmd.Flags().StringArrayVar(&selectResources, "select", nil, "Validate only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated.") + cmd.Flags().StringSliceVar(&selectResources, "select", nil, "Validate only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated or comma-separated.") cmd.RunE = func(cmd *cobra.Command, args []string) error { b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ From 47eb09a7d08e140a120fe89cf0df64db66785ab8 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 12:57:50 +0200 Subject: [PATCH 07/25] update --- acceptance/bundle/help/bundle-deploy/output.txt | 2 +- acceptance/bundle/help/bundle-summary/output.txt | 6 +++--- acceptance/bundle/help/bundle-validate/output.txt | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/acceptance/bundle/help/bundle-deploy/output.txt b/acceptance/bundle/help/bundle-deploy/output.txt index 9ff5924a4e2..7e9e3ff34dd 100644 --- a/acceptance/bundle/help/bundle-deploy/output.txt +++ b/acceptance/bundle/help/bundle-deploy/output.txt @@ -20,7 +20,7 @@ Flags: --force-lock Force acquisition of deployment lock. -h, --help help for deploy --plan string Path to a JSON plan file to apply instead of planning (direct engine only). - --select stringArray Deploy only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated. + --select strings Deploy only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated or comma-separated. Global Flags: --debug enable debug logging diff --git a/acceptance/bundle/help/bundle-summary/output.txt b/acceptance/bundle/help/bundle-summary/output.txt index 97237358725..0f842b1d77a 100644 --- a/acceptance/bundle/help/bundle-summary/output.txt +++ b/acceptance/bundle/help/bundle-summary/output.txt @@ -7,9 +7,9 @@ Usage: databricks bundle summary [flags] Flags: - --force-pull Skip local cache and load the state from the remote workspace - -h, --help help for summary - --select stringArray Show only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated. + --force-pull Skip local cache and load the state from the remote workspace + -h, --help help for summary + --select strings Show only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated or comma-separated. Global Flags: --debug enable debug logging diff --git a/acceptance/bundle/help/bundle-validate/output.txt b/acceptance/bundle/help/bundle-validate/output.txt index 31d81c9308d..5566287616c 100644 --- a/acceptance/bundle/help/bundle-validate/output.txt +++ b/acceptance/bundle/help/bundle-validate/output.txt @@ -15,9 +15,9 @@ Usage: databricks bundle validate [flags] Flags: - -h, --help help for validate - --select stringArray Validate only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated. - --strict Treat warnings as errors + -h, --help help for validate + --select strings Validate only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated or comma-separated. + --strict Treat warnings as errors Global Flags: --debug enable debug logging From 52e84b933019d15f50c24625269cc2e1982d487f Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 13:08:03 +0200 Subject: [PATCH 08/25] bundle: reject --select on terraform engine with actionable error --select relies on the direct engine's plan dependency graph to expand transitive dependencies. On the terraform engine that graph isn't available, so rather than silently planning/deploying every resource, ProcessBundleRet now errors early with a hint to switch to the direct engine. Pin the select/select-dep acceptance tests to the direct engine and add select-terraform-error covering the rejection on terraform. Co-authored-by: Isaac --- .../select-dep/out.select-single.direct.txt | 5 ---- .../out.select-single.terraform.txt | 12 -------- .../bundle/plan/select-dep/out.test.toml | 2 +- acceptance/bundle/plan/select-dep/output.txt | 6 ++++ acceptance/bundle/plan/select-dep/script | 9 ++---- acceptance/bundle/plan/select-dep/test.toml | 3 +- .../select-terraform-error/databricks.yml | 7 +++++ .../plan/select-terraform-error/out.test.toml | 3 ++ .../plan/select-terraform-error/output.txt | 6 ++++ .../bundle/plan/select-terraform-error/script | 2 ++ .../plan/select-terraform-error/test.toml | 4 +++ acceptance/bundle/plan/select/out.test.toml | 2 +- acceptance/bundle/plan/select/test.toml | 2 ++ bundle/phases/deploy.go | 7 ++--- cmd/bundle/utils/process.go | 29 ++++++++++++++----- 15 files changed, 59 insertions(+), 40 deletions(-) delete mode 100644 acceptance/bundle/plan/select-dep/out.select-single.direct.txt delete mode 100644 acceptance/bundle/plan/select-dep/out.select-single.terraform.txt create mode 100644 acceptance/bundle/plan/select-terraform-error/databricks.yml create mode 100644 acceptance/bundle/plan/select-terraform-error/out.test.toml create mode 100644 acceptance/bundle/plan/select-terraform-error/output.txt create mode 100644 acceptance/bundle/plan/select-terraform-error/script create mode 100644 acceptance/bundle/plan/select-terraform-error/test.toml diff --git a/acceptance/bundle/plan/select-dep/out.select-single.direct.txt b/acceptance/bundle/plan/select-dep/out.select-single.direct.txt deleted file mode 100644 index 71f2d9c3818..00000000000 --- a/acceptance/bundle/plan/select-dep/out.select-single.direct.txt +++ /dev/null @@ -1,5 +0,0 @@ -create jobs.bar -create jobs.foo - -Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged -Exit code: 0 diff --git a/acceptance/bundle/plan/select-dep/out.select-single.terraform.txt b/acceptance/bundle/plan/select-dep/out.select-single.terraform.txt deleted file mode 100644 index 2168444bf36..00000000000 --- a/acceptance/bundle/plan/select-dep/out.select-single.terraform.txt +++ /dev/null @@ -1,12 +0,0 @@ -Error: exit status 1 - -Error: Reference to undeclared resource - - on bundle.tf.json line 30, in resource.databricks_job.foo.task[0].run_job_task: - 30: "job_id": "${databricks_job.bar.id}" - -A managed resource "databricks_job" "bar" has not been declared in the root -module. - - -Exit code: 1 diff --git a/acceptance/bundle/plan/select-dep/out.test.toml b/acceptance/bundle/plan/select-dep/out.test.toml index f784a183258..e90b6d5d1ba 100644 --- a/acceptance/bundle/plan/select-dep/out.test.toml +++ b/acceptance/bundle/plan/select-dep/out.test.toml @@ -1,3 +1,3 @@ Local = true Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/plan/select-dep/output.txt b/acceptance/bundle/plan/select-dep/output.txt index f252435129f..148d316ffa5 100644 --- a/acceptance/bundle/plan/select-dep/output.txt +++ b/acceptance/bundle/plan/select-dep/output.txt @@ -9,3 +9,9 @@ Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged create jobs.bar Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select jobs.foo +create jobs.bar +create jobs.foo + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select-dep/script b/acceptance/bundle/plan/select-dep/script index cb4739f43fc..c49bf6b1515 100644 --- a/acceptance/bundle/plan/select-dep/script +++ b/acceptance/bundle/plan/select-dep/script @@ -4,10 +4,5 @@ trace $CLI bundle plan --select jobs.foo --select jobs.bar # Select the dependency alone: bar has no outgoing references. trace $CLI bundle plan --select jobs.bar -# Select only foo. For the direct engine bar is auto-included via the DependsOn -# graph; for terraform deps are not tracked at this stage so the output differs. -set +e -$CLI bundle plan --select jobs.foo > out.select-single.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 -echo "Exit code: $?" >> out.select-single.$DATABRICKS_BUNDLE_ENGINE.txt -set -e -perl -0777 -i -pe 's/\n+$/\n/' out.select-single.$DATABRICKS_BUNDLE_ENGINE.txt +# Select only foo: bar is auto-included because foo references ${resources.jobs.bar.id}. +trace $CLI bundle plan --select jobs.foo diff --git a/acceptance/bundle/plan/select-dep/test.toml b/acceptance/bundle/plan/select-dep/test.toml index 85e02532c93..23af315b941 100644 --- a/acceptance/bundle/plan/select-dep/test.toml +++ b/acceptance/bundle/plan/select-dep/test.toml @@ -1,3 +1,4 @@ Local = true Cloud = false -Ignore = [".databricks"] +# --select is only supported by the direct engine (terraform errors out, tested separately). +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/plan/select-terraform-error/databricks.yml b/acceptance/bundle/plan/select-terraform-error/databricks.yml new file mode 100644 index 00000000000..64f1cbc4496 --- /dev/null +++ b/acceptance/bundle/plan/select-terraform-error/databricks.yml @@ -0,0 +1,7 @@ +bundle: + name: plan-select-terraform-error + +resources: + jobs: + my_job: + name: my-job diff --git a/acceptance/bundle/plan/select-terraform-error/out.test.toml b/acceptance/bundle/plan/select-terraform-error/out.test.toml new file mode 100644 index 00000000000..65156e0457c --- /dev/null +++ b/acceptance/bundle/plan/select-terraform-error/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] diff --git a/acceptance/bundle/plan/select-terraform-error/output.txt b/acceptance/bundle/plan/select-terraform-error/output.txt new file mode 100644 index 00000000000..584e0b0e0a5 --- /dev/null +++ b/acceptance/bundle/plan/select-terraform-error/output.txt @@ -0,0 +1,6 @@ + +>>> [CLI] bundle plan --select my_job +Error: --select is only supported with the direct engine (set bundle.engine to "direct" or DATABRICKS_BUNDLE_ENGINE=direct) + + +Exit code: 1 diff --git a/acceptance/bundle/plan/select-terraform-error/script b/acceptance/bundle/plan/select-terraform-error/script new file mode 100644 index 00000000000..5df07c8840c --- /dev/null +++ b/acceptance/bundle/plan/select-terraform-error/script @@ -0,0 +1,2 @@ +# --select is only supported by the direct engine; terraform must error out. +trace $CLI bundle plan --select my_job diff --git a/acceptance/bundle/plan/select-terraform-error/test.toml b/acceptance/bundle/plan/select-terraform-error/test.toml new file mode 100644 index 00000000000..8f260b04a71 --- /dev/null +++ b/acceptance/bundle/plan/select-terraform-error/test.toml @@ -0,0 +1,4 @@ +Local = true +Cloud = false +# --select is rejected on the terraform engine with an actionable error. +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] diff --git a/acceptance/bundle/plan/select/out.test.toml b/acceptance/bundle/plan/select/out.test.toml index f784a183258..e90b6d5d1ba 100644 --- a/acceptance/bundle/plan/select/out.test.toml +++ b/acceptance/bundle/plan/select/out.test.toml @@ -1,3 +1,3 @@ Local = true Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/plan/select/test.toml b/acceptance/bundle/plan/select/test.toml index 7d36fb9dc18..23af315b941 100644 --- a/acceptance/bundle/plan/select/test.toml +++ b/acceptance/bundle/plan/select/test.toml @@ -1,2 +1,4 @@ Local = true Cloud = false +# --select is only supported by the direct engine (terraform errors out, tested separately). +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index d3d024e2feb..3cac322f9e3 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -8,7 +8,6 @@ import ( "github.com/databricks/cli/bundle/artifacts" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/engine" - "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/bundle/deploy" "github.com/databricks/cli/bundle/deploy/files" "github.com/databricks/cli/bundle/deploy/lock" @@ -219,10 +218,8 @@ func RunPlan(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) *d return plan } - // Terraform engine: filter config to the selected set before writing terraform JSON. - if len(b.Select) > 0 { - bundle.ApplyContext(ctx, b, mutator.FilterSelectedResources()) - } + // b.Select is rejected for the terraform engine in ProcessBundleRet, so it is + // never set here. bundle.ApplySeqContext(ctx, b, terraform.Interpolate(), diff --git a/cmd/bundle/utils/process.go b/cmd/bundle/utils/process.go index 37bd62e9f3f..25a02ae0af8 100644 --- a/cmd/bundle/utils/process.go +++ b/cmd/bundle/utils/process.go @@ -170,16 +170,29 @@ func ProcessBundleRet(cmd *cobra.Command, opts ProcessOptions) (b *bundle.Bundle } } - // For commands that don't compute a deployment plan (validate, summary), - // apply a simple exact-match config filter now. For plan/deploy with the - // direct engine, RunPlan calls plan.FilterToSelected which uses the - // DependsOn graph to auto-include transitive dependencies; the terraform - // engine is filtered inside RunPlan before writing the terraform JSON. - if len(b.Select) > 0 && !opts.PreDeployChecks && !opts.Deploy && opts.ReadPlanPath == "" { - bundle.ApplyContext(ctx, b, mutator.FilterSelectedResources()) - if logdiag.HasError(ctx) { + if len(b.Select) > 0 { + // --select is only supported by the direct engine, which tracks resource + // dependencies in the plan graph. Reject it on terraform with an actionable + // error rather than silently planning/deploying every resource. + engineSetting, err := ResolveEngineSetting(ctx, b) + if err != nil { + return b, nil, err + } + if !engineSetting.Type.IsDirect() { + logdiag.LogError(ctx, errors.New(`--select is only supported with the direct engine (set bundle.engine to "direct" or DATABRICKS_BUNDLE_ENGINE=direct)`)) return b, nil, root.ErrAlreadyPrinted } + + // For commands that don't compute a deployment plan (validate, summary), + // apply a simple exact-match config filter now. For plan/deploy, RunPlan + // calls plan.FilterToSelected which uses the DependsOn graph to auto-include + // transitive dependencies. + if !opts.PreDeployChecks && !opts.Deploy && opts.ReadPlanPath == "" { + bundle.ApplyContext(ctx, b, mutator.FilterSelectedResources()) + if logdiag.HasError(ctx) { + return b, nil, root.ErrAlreadyPrinted + } + } } shouldReadState := opts.ReadState || opts.AlwaysPull || opts.InitIDs || opts.ErrorOnEmptyState || opts.PreDeployChecks || opts.Deploy || opts.ReadPlanPath != "" From dc519fa4419bd41737cab23830a469f3216a2a62 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 13:26:31 +0200 Subject: [PATCH 09/25] bundle: limit --select to plan/deploy only Drop the --select flag from validate and summary; the feature is only meaningful when a deployment plan is computed. This removes the now-dead FilterSelectedResources mutator and Resources.FilterResources helper, and simplifies ProcessBundleRet to just the terraform-engine rejection check (filtering happens in RunPlan via plan.FilterToSelected). Co-authored-by: Isaac --- .../bundle/help/bundle-summary/output.txt | 5 ++-- .../bundle/help/bundle-validate/output.txt | 5 ++-- bundle/config/mutator/select_resources.go | 25 ------------------- .../config/mutator/select_resources_test.go | 9 ------- bundle/config/resources.go | 25 ------------------- cmd/bundle/summary.go | 5 ---- cmd/bundle/utils/process.go | 18 +++---------- cmd/bundle/validate.go | 5 ---- 8 files changed, 8 insertions(+), 89 deletions(-) diff --git a/acceptance/bundle/help/bundle-summary/output.txt b/acceptance/bundle/help/bundle-summary/output.txt index 0f842b1d77a..d3c0c961edb 100644 --- a/acceptance/bundle/help/bundle-summary/output.txt +++ b/acceptance/bundle/help/bundle-summary/output.txt @@ -7,9 +7,8 @@ Usage: databricks bundle summary [flags] Flags: - --force-pull Skip local cache and load the state from the remote workspace - -h, --help help for summary - --select strings Show only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated or comma-separated. + --force-pull Skip local cache and load the state from the remote workspace + -h, --help help for summary Global Flags: --debug enable debug logging diff --git a/acceptance/bundle/help/bundle-validate/output.txt b/acceptance/bundle/help/bundle-validate/output.txt index 5566287616c..dbb15761b31 100644 --- a/acceptance/bundle/help/bundle-validate/output.txt +++ b/acceptance/bundle/help/bundle-validate/output.txt @@ -15,9 +15,8 @@ Usage: databricks bundle validate [flags] Flags: - -h, --help help for validate - --select strings Validate only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated or comma-separated. - --strict Treat warnings as errors + -h, --help help for validate + --strict Treat warnings as errors Global Flags: --debug enable debug logging diff --git a/bundle/config/mutator/select_resources.go b/bundle/config/mutator/select_resources.go index 20715028ffc..455fec7c0fb 100644 --- a/bundle/config/mutator/select_resources.go +++ b/bundle/config/mutator/select_resources.go @@ -70,28 +70,3 @@ func (m *selectResources) Apply(_ context.Context, b *bundle.Bundle) diag.Diagno b.Select = resolved return nil } - -// FilterSelectedResources filters b.Config.Resources to only the resources in -// b.Select (exact match, no dependency expansion). Used for commands that don't -// compute a deployment plan (validate, summary, terraform). -func FilterSelectedResources() bundle.Mutator { - return &filterSelectedResources{} -} - -type filterSelectedResources struct{} - -func (m *filterSelectedResources) Name() string { - return "FilterSelectedResources" -} - -func (m *filterSelectedResources) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { - if len(b.Select) == 0 { - return nil - } - keep := make(map[string]struct{}, len(b.Select)) - for _, key := range b.Select { - keep[key] = struct{}{} - } - b.Config.Resources.FilterResources(keep) - return nil -} diff --git a/bundle/config/mutator/select_resources_test.go b/bundle/config/mutator/select_resources_test.go index 68bdecbfca4..0b4e182a20f 100644 --- a/bundle/config/mutator/select_resources_test.go +++ b/bundle/config/mutator/select_resources_test.go @@ -84,12 +84,3 @@ func TestSelectResources_MultipleSelectors(t *testing.T) { require.NoError(t, diags.Error()) assert.Equal(t, []string{"jobs.my_job", "pipelines.my_pipeline"}, b.Select) } - -func TestFilterSelectedResources(t *testing.T) { - b := bundleWithJobsAndPipelines() - b.Select = []string{"jobs.my_job"} - diags := bundle.Apply(t.Context(), b, mutator.FilterSelectedResources()) - require.NoError(t, diags.Error()) - assert.Len(t, b.Config.Resources.Jobs, 1) - assert.Empty(t, b.Config.Resources.Pipelines) -} diff --git a/bundle/config/resources.go b/bundle/config/resources.go index 8d2e89e2410..2e840a5ef80 100644 --- a/bundle/config/resources.go +++ b/bundle/config/resources.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "net/url" - "reflect" - "strings" "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/databricks-sdk-go" @@ -124,29 +122,6 @@ func (r *Resources) AllResources() []ResourceGroup { } } -// FilterResources removes all resources whose "typePlural.name" key is not in keep. -// It uses reflection to handle all resource types without manual enumeration. -func (r *Resources) FilterResources(keep map[string]struct{}) { - rv := reflect.ValueOf(r).Elem() - rt := rv.Type() - for i := range rt.NumField() { - field := rt.Field(i) - typeName, _, _ := strings.Cut(field.Tag.Get("json"), ",") - if typeName == "" || typeName == "-" { - continue - } - fv := rv.Field(i) - if fv.Kind() != reflect.Map || fv.IsNil() { - continue - } - for _, k := range fv.MapKeys() { - if _, ok := keep[typeName+"."+k.String()]; !ok { - fv.SetMapIndex(k, reflect.Value{}) - } - } - } -} - // FindResourceByConfigKey searches all resource maps for a resource with the given key. func (r *Resources) FindResourceByConfigKey(key string) (ConfigResource, error) { var found []ConfigResource diff --git a/cmd/bundle/summary.go b/cmd/bundle/summary.go index aa310aa0284..b3a55a607cc 100644 --- a/cmd/bundle/summary.go +++ b/cmd/bundle/summary.go @@ -21,11 +21,9 @@ Useful after deployment to see what was created and where to find it.`, var forcePull bool var includeLocations bool - var selectResources []string cmd.Flags().BoolVar(&forcePull, "force-pull", false, "Skip local cache and load the state from the remote workspace") cmd.Flags().BoolVar(&includeLocations, "include-locations", false, "Include location information in the output") cmd.Flags().MarkHidden("include-locations") - cmd.Flags().StringSliceVar(&selectResources, "select", nil, "Show only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated or comma-separated.") cmd.RunE = func(cmd *cobra.Command, args []string) error { b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ @@ -33,9 +31,6 @@ Useful after deployment to see what was created and where to find it.`, AlwaysPull: forcePull, IncludeLocations: includeLocations, InitIDs: true, - InitFunc: func(b *bundle.Bundle) { - b.Select = selectResources - }, }) if err != nil { return err diff --git a/cmd/bundle/utils/process.go b/cmd/bundle/utils/process.go index 25a02ae0af8..d8fcfc16787 100644 --- a/cmd/bundle/utils/process.go +++ b/cmd/bundle/utils/process.go @@ -171,9 +171,10 @@ func ProcessBundleRet(cmd *cobra.Command, opts ProcessOptions) (b *bundle.Bundle } if len(b.Select) > 0 { - // --select is only supported by the direct engine, which tracks resource - // dependencies in the plan graph. Reject it on terraform with an actionable - // error rather than silently planning/deploying every resource. + // --select (plan/deploy only) is only supported by the direct engine, which + // tracks resource dependencies in the plan graph. Reject it on terraform with + // an actionable error rather than silently planning/deploying every resource. + // The actual filtering happens in RunPlan via plan.FilterToSelected. engineSetting, err := ResolveEngineSetting(ctx, b) if err != nil { return b, nil, err @@ -182,17 +183,6 @@ func ProcessBundleRet(cmd *cobra.Command, opts ProcessOptions) (b *bundle.Bundle logdiag.LogError(ctx, errors.New(`--select is only supported with the direct engine (set bundle.engine to "direct" or DATABRICKS_BUNDLE_ENGINE=direct)`)) return b, nil, root.ErrAlreadyPrinted } - - // For commands that don't compute a deployment plan (validate, summary), - // apply a simple exact-match config filter now. For plan/deploy, RunPlan - // calls plan.FilterToSelected which uses the DependsOn graph to auto-include - // transitive dependencies. - if !opts.PreDeployChecks && !opts.Deploy && opts.ReadPlanPath == "" { - bundle.ApplyContext(ctx, b, mutator.FilterSelectedResources()) - if logdiag.HasError(ctx) { - return b, nil, root.ErrAlreadyPrinted - } - } } shouldReadState := opts.ReadState || opts.AlwaysPull || opts.InitIDs || opts.ErrorOnEmptyState || opts.PreDeployChecks || opts.Deploy || opts.ReadPlanPath != "" diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index 8d1e8d0c5a5..a2ec31f721b 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -46,19 +46,14 @@ Please run this command before deploying to ensure configuration quality.`, var includeLocations bool var strict bool - var selectResources []string cmd.Flags().BoolVar(&includeLocations, "include-locations", false, "Include location information in the output") cmd.Flags().MarkHidden("include-locations") cmd.Flags().BoolVar(&strict, "strict", false, "Treat warnings as errors") - cmd.Flags().StringSliceVar(&selectResources, "select", nil, "Validate only the specified resource (e.g. 'my_job' or 'jobs.my_job'). Can be repeated or comma-separated.") cmd.RunE = func(cmd *cobra.Command, args []string) error { b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ Validate: true, IncludeLocations: includeLocations, - InitFunc: func(b *bundle.Bundle) { - b.Select = selectResources - }, }) ctx := cmd.Context() From 203ea8485374fe0b2103c199f201f672a56afb64 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 13:44:43 +0200 Subject: [PATCH 10/25] bundle: run --select tests on both engines, link docs in error Run the select/select-dep acceptance tests on both engines (instead of pinning to direct) and capture output in per-engine out.plan..txt files so the terraform rejection is visible. Commands are prefixed with errcode so the script records each result without aborting midway. This makes the dedicated select-terraform-error test redundant, so it is removed. Move the engine check before Initialize so the engine mismatch is reported before per-resource validation, and point the error at the direct-engine docs instead of repeating the env/config knobs. Co-authored-by: Isaac --- .../plan/select-dep/out.plan.direct.txt | 17 +++++++++ .../plan/select-dep/out.plan.terraform.txt | 18 ++++++++++ .../bundle/plan/select-dep/out.test.toml | 2 +- acceptance/bundle/plan/select-dep/output.txt | 17 --------- acceptance/bundle/plan/select-dep/script | 10 ++++-- acceptance/bundle/plan/select-dep/test.toml | 2 -- .../select-terraform-error/databricks.yml | 7 ---- .../plan/select-terraform-error/out.test.toml | 3 -- .../plan/select-terraform-error/output.txt | 6 ---- .../bundle/plan/select-terraform-error/script | 2 -- .../plan/select-terraform-error/test.toml | 4 --- .../bundle/plan/select/out.plan.direct.txt | 34 ++++++++++++++++++ .../bundle/plan/select/out.plan.terraform.txt | 36 +++++++++++++++++++ acceptance/bundle/plan/select/out.test.toml | 2 +- acceptance/bundle/plan/select/output.txt | 30 ---------------- acceptance/bundle/plan/select/script | 16 +++++---- acceptance/bundle/plan/select/test.toml | 2 -- cmd/bundle/utils/process.go | 32 +++++++++-------- 18 files changed, 141 insertions(+), 99 deletions(-) create mode 100644 acceptance/bundle/plan/select-dep/out.plan.direct.txt create mode 100644 acceptance/bundle/plan/select-dep/out.plan.terraform.txt delete mode 100644 acceptance/bundle/plan/select-terraform-error/databricks.yml delete mode 100644 acceptance/bundle/plan/select-terraform-error/out.test.toml delete mode 100644 acceptance/bundle/plan/select-terraform-error/output.txt delete mode 100644 acceptance/bundle/plan/select-terraform-error/script delete mode 100644 acceptance/bundle/plan/select-terraform-error/test.toml create mode 100644 acceptance/bundle/plan/select/out.plan.direct.txt create mode 100644 acceptance/bundle/plan/select/out.plan.terraform.txt diff --git a/acceptance/bundle/plan/select-dep/out.plan.direct.txt b/acceptance/bundle/plan/select-dep/out.plan.direct.txt new file mode 100644 index 00000000000..148d316ffa5 --- /dev/null +++ b/acceptance/bundle/plan/select-dep/out.plan.direct.txt @@ -0,0 +1,17 @@ + +>>> [CLI] bundle plan --select jobs.foo --select jobs.bar +create jobs.bar +create jobs.foo + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select jobs.bar +create jobs.bar + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select jobs.foo +create jobs.bar +create jobs.foo + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select-dep/out.plan.terraform.txt b/acceptance/bundle/plan/select-dep/out.plan.terraform.txt new file mode 100644 index 00000000000..c8f57803b98 --- /dev/null +++ b/acceptance/bundle/plan/select-dep/out.plan.terraform.txt @@ -0,0 +1,18 @@ + +>>> [CLI] bundle plan --select jobs.foo --select jobs.bar +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 + +>>> [CLI] bundle plan --select jobs.bar +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 + +>>> [CLI] bundle plan --select jobs.foo +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 diff --git a/acceptance/bundle/plan/select-dep/out.test.toml b/acceptance/bundle/plan/select-dep/out.test.toml index e90b6d5d1ba..f784a183258 100644 --- a/acceptance/bundle/plan/select-dep/out.test.toml +++ b/acceptance/bundle/plan/select-dep/out.test.toml @@ -1,3 +1,3 @@ Local = true Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/plan/select-dep/output.txt b/acceptance/bundle/plan/select-dep/output.txt index 148d316ffa5..e69de29bb2d 100644 --- a/acceptance/bundle/plan/select-dep/output.txt +++ b/acceptance/bundle/plan/select-dep/output.txt @@ -1,17 +0,0 @@ - ->>> [CLI] bundle plan --select jobs.foo --select jobs.bar -create jobs.bar -create jobs.foo - -Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle plan --select jobs.bar -create jobs.bar - -Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle plan --select jobs.foo -create jobs.bar -create jobs.foo - -Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select-dep/script b/acceptance/bundle/plan/select-dep/script index c49bf6b1515..41036e4f7ce 100644 --- a/acceptance/bundle/plan/select-dep/script +++ b/acceptance/bundle/plan/select-dep/script @@ -1,8 +1,12 @@ +# --select is only supported by the direct engine; terraform errors out. +# Output differs per engine, so it is captured in a per-engine file. +{ # Both resources selected explicitly. -trace $CLI bundle plan --select jobs.foo --select jobs.bar +errcode trace $CLI bundle plan --select jobs.foo --select jobs.bar # Select the dependency alone: bar has no outgoing references. -trace $CLI bundle plan --select jobs.bar +errcode trace $CLI bundle plan --select jobs.bar # Select only foo: bar is auto-included because foo references ${resources.jobs.bar.id}. -trace $CLI bundle plan --select jobs.foo +errcode trace $CLI bundle plan --select jobs.foo +} &> out.plan.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/plan/select-dep/test.toml b/acceptance/bundle/plan/select-dep/test.toml index 23af315b941..7d36fb9dc18 100644 --- a/acceptance/bundle/plan/select-dep/test.toml +++ b/acceptance/bundle/plan/select-dep/test.toml @@ -1,4 +1,2 @@ Local = true Cloud = false -# --select is only supported by the direct engine (terraform errors out, tested separately). -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/plan/select-terraform-error/databricks.yml b/acceptance/bundle/plan/select-terraform-error/databricks.yml deleted file mode 100644 index 64f1cbc4496..00000000000 --- a/acceptance/bundle/plan/select-terraform-error/databricks.yml +++ /dev/null @@ -1,7 +0,0 @@ -bundle: - name: plan-select-terraform-error - -resources: - jobs: - my_job: - name: my-job diff --git a/acceptance/bundle/plan/select-terraform-error/out.test.toml b/acceptance/bundle/plan/select-terraform-error/out.test.toml deleted file mode 100644 index 65156e0457c..00000000000 --- a/acceptance/bundle/plan/select-terraform-error/out.test.toml +++ /dev/null @@ -1,3 +0,0 @@ -Local = true -Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] diff --git a/acceptance/bundle/plan/select-terraform-error/output.txt b/acceptance/bundle/plan/select-terraform-error/output.txt deleted file mode 100644 index 584e0b0e0a5..00000000000 --- a/acceptance/bundle/plan/select-terraform-error/output.txt +++ /dev/null @@ -1,6 +0,0 @@ - ->>> [CLI] bundle plan --select my_job -Error: --select is only supported with the direct engine (set bundle.engine to "direct" or DATABRICKS_BUNDLE_ENGINE=direct) - - -Exit code: 1 diff --git a/acceptance/bundle/plan/select-terraform-error/script b/acceptance/bundle/plan/select-terraform-error/script deleted file mode 100644 index 5df07c8840c..00000000000 --- a/acceptance/bundle/plan/select-terraform-error/script +++ /dev/null @@ -1,2 +0,0 @@ -# --select is only supported by the direct engine; terraform must error out. -trace $CLI bundle plan --select my_job diff --git a/acceptance/bundle/plan/select-terraform-error/test.toml b/acceptance/bundle/plan/select-terraform-error/test.toml deleted file mode 100644 index 8f260b04a71..00000000000 --- a/acceptance/bundle/plan/select-terraform-error/test.toml +++ /dev/null @@ -1,4 +0,0 @@ -Local = true -Cloud = false -# --select is rejected on the terraform engine with an actionable error. -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] diff --git a/acceptance/bundle/plan/select/out.plan.direct.txt b/acceptance/bundle/plan/select/out.plan.direct.txt new file mode 100644 index 00000000000..a95f7d37023 --- /dev/null +++ b/acceptance/bundle/plan/select/out.plan.direct.txt @@ -0,0 +1,34 @@ + +>>> [CLI] bundle plan --select no_such_resource +Error: no such resource: no_such_resource + + +Exit code: 1 + +>>> [CLI] bundle plan --select jobs.no_such_job +Error: no such resource: jobs.no_such_job + + +Exit code: 1 + +>>> [CLI] bundle plan --select job_b +create jobs.job_b + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select jobs.job_a +create jobs.job_a + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select job_a --select job_b +create jobs.job_a +create jobs.job_b + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select job_a,job_b +create jobs.job_a +create jobs.job_b + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select/out.plan.terraform.txt b/acceptance/bundle/plan/select/out.plan.terraform.txt new file mode 100644 index 00000000000..4c0c5e8c45c --- /dev/null +++ b/acceptance/bundle/plan/select/out.plan.terraform.txt @@ -0,0 +1,36 @@ + +>>> [CLI] bundle plan --select no_such_resource +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 + +>>> [CLI] bundle plan --select jobs.no_such_job +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 + +>>> [CLI] bundle plan --select job_b +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 + +>>> [CLI] bundle plan --select jobs.job_a +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 + +>>> [CLI] bundle plan --select job_a --select job_b +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 + +>>> [CLI] bundle plan --select job_a,job_b +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 diff --git a/acceptance/bundle/plan/select/out.test.toml b/acceptance/bundle/plan/select/out.test.toml index e90b6d5d1ba..f784a183258 100644 --- a/acceptance/bundle/plan/select/out.test.toml +++ b/acceptance/bundle/plan/select/out.test.toml @@ -1,3 +1,3 @@ Local = true Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/plan/select/output.txt b/acceptance/bundle/plan/select/output.txt index 6993b7303d4..e69de29bb2d 100644 --- a/acceptance/bundle/plan/select/output.txt +++ b/acceptance/bundle/plan/select/output.txt @@ -1,30 +0,0 @@ - ->>> [CLI] bundle plan --select no_such_resource -Error: no such resource: no_such_resource - - ->>> [CLI] bundle plan --select jobs.no_such_job -Error: no such resource: jobs.no_such_job - - ->>> [CLI] bundle plan --select job_b -create jobs.job_b - -Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle plan --select jobs.job_a -create jobs.job_a - -Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle plan --select job_a --select job_b -create jobs.job_a -create jobs.job_b - -Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle plan --select job_a,job_b -create jobs.job_a -create jobs.job_b - -Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select/script b/acceptance/bundle/plan/select/script index 9351613fc48..f246bea9099 100644 --- a/acceptance/bundle/plan/select/script +++ b/acceptance/bundle/plan/select/script @@ -1,17 +1,21 @@ +# --select is only supported by the direct engine; terraform errors out. +# Output differs per engine, so it is captured in a per-engine file. +{ # Non-existent resource -trace $CLI bundle plan --select no_such_resource || true +errcode trace $CLI bundle plan --select no_such_resource # Qualified name for non-existent resource -trace $CLI bundle plan --select jobs.no_such_job || true +errcode trace $CLI bundle plan --select jobs.no_such_job # Select a unique resource by unqualified name -trace $CLI bundle plan --select job_b +errcode trace $CLI bundle plan --select job_b # Select a resource by qualified name -trace $CLI bundle plan --select jobs.job_a +errcode trace $CLI bundle plan --select jobs.job_a # Repeated --select: both resources are included -trace $CLI bundle plan --select job_a --select job_b +errcode trace $CLI bundle plan --select job_a --select job_b # Comma-separated --select: both resources are included -trace $CLI bundle plan --select job_a,job_b +errcode trace $CLI bundle plan --select job_a,job_b +} &> out.plan.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/plan/select/test.toml b/acceptance/bundle/plan/select/test.toml index 23af315b941..7d36fb9dc18 100644 --- a/acceptance/bundle/plan/select/test.toml +++ b/acceptance/bundle/plan/select/test.toml @@ -1,4 +1,2 @@ Local = true Cloud = false -# --select is only supported by the direct engine (terraform errors out, tested separately). -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/cmd/bundle/utils/process.go b/cmd/bundle/utils/process.go index d8fcfc16787..e498c877fc7 100644 --- a/cmd/bundle/utils/process.go +++ b/cmd/bundle/utils/process.go @@ -139,6 +139,23 @@ func ProcessBundleRet(cmd *cobra.Command, opts ProcessOptions) (b *bundle.Bundle bundle.ApplyFuncContext(ctx, b, func(context.Context, *bundle.Bundle) { opts.InitFunc(b) }) } + if len(b.Select) > 0 { + // --select (plan/deploy only) is only supported by the direct engine, which + // tracks resource dependencies in the plan graph. Reject it on terraform with + // an actionable error rather than silently planning/deploying every resource. + // Checked before Initialize so the engine mismatch is reported before any + // per-resource validation. The actual filtering happens in RunPlan via + // plan.FilterToSelected. + engineSetting, err := ResolveEngineSetting(ctx, b) + if err != nil { + return b, nil, err + } + if !engineSetting.Type.IsDirect() { + logdiag.LogError(ctx, errors.New("--select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct")) + return b, nil, root.ErrAlreadyPrinted + } + } + if !opts.SkipInitialize { t0 := time.Now() phases.Initialize(ctx, b) @@ -170,21 +187,6 @@ func ProcessBundleRet(cmd *cobra.Command, opts ProcessOptions) (b *bundle.Bundle } } - if len(b.Select) > 0 { - // --select (plan/deploy only) is only supported by the direct engine, which - // tracks resource dependencies in the plan graph. Reject it on terraform with - // an actionable error rather than silently planning/deploying every resource. - // The actual filtering happens in RunPlan via plan.FilterToSelected. - engineSetting, err := ResolveEngineSetting(ctx, b) - if err != nil { - return b, nil, err - } - if !engineSetting.Type.IsDirect() { - logdiag.LogError(ctx, errors.New(`--select is only supported with the direct engine (set bundle.engine to "direct" or DATABRICKS_BUNDLE_ENGINE=direct)`)) - return b, nil, root.ErrAlreadyPrinted - } - } - shouldReadState := opts.ReadState || opts.AlwaysPull || opts.InitIDs || opts.ErrorOnEmptyState || opts.PreDeployChecks || opts.Deploy || opts.ReadPlanPath != "" if shouldReadState { From 124ebddceeac6d3faecf976c1f947d6cb3674a30 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 13:51:54 +0200 Subject: [PATCH 11/25] bundle: move --select tests to acceptance/bundle/select, cover deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relocate the --select acceptance tests under acceptance/bundle/select and add a deploy subtest. The deploy test selects jobs.foo (which references jobs.bar) and confirms via recorded requests that only bar and foo are created while the independent baz is left untouched — exercising deploy, not just plan. Both subtests run on terraform and direct, capturing the per-engine output (terraform rejects --select). Co-authored-by: Isaac --- .../plan/select-dep/out.plan.direct.txt | 17 ------ acceptance/bundle/plan/select-dep/script | 12 ---- acceptance/bundle/plan/select-dep/test.toml | 2 - acceptance/bundle/plan/select/test.toml | 2 - .../deploy}/databricks.yml | 5 +- .../select/deploy/out.deploy.direct.txt | 56 +++++++++++++++++++ .../deploy/out.deploy.terraform.txt} | 10 +--- .../deploy}/out.test.toml | 0 .../select-dep => select/deploy}/output.txt | 0 acceptance/bundle/select/deploy/script | 12 ++++ acceptance/bundle/select/deploy/test.toml | 1 + .../select => select/plan}/databricks.yml | 0 .../{plan/select => select/plan}/my_script.py | 0 .../plan}/out.plan.direct.txt | 0 .../plan}/out.plan.terraform.txt | 0 .../select => select/plan}/out.test.toml | 0 .../{plan/select => select/plan}/output.txt | 0 .../{plan/select => select/plan}/script | 0 acceptance/bundle/select/test.toml | 3 + 19 files changed, 79 insertions(+), 41 deletions(-) delete mode 100644 acceptance/bundle/plan/select-dep/out.plan.direct.txt delete mode 100644 acceptance/bundle/plan/select-dep/script delete mode 100644 acceptance/bundle/plan/select-dep/test.toml delete mode 100644 acceptance/bundle/plan/select/test.toml rename acceptance/bundle/{plan/select-dep => select/deploy}/databricks.yml (79%) create mode 100644 acceptance/bundle/select/deploy/out.deploy.direct.txt rename acceptance/bundle/{plan/select-dep/out.plan.terraform.txt => select/deploy/out.deploy.terraform.txt} (57%) rename acceptance/bundle/{plan/select-dep => select/deploy}/out.test.toml (100%) rename acceptance/bundle/{plan/select-dep => select/deploy}/output.txt (100%) create mode 100644 acceptance/bundle/select/deploy/script create mode 100644 acceptance/bundle/select/deploy/test.toml rename acceptance/bundle/{plan/select => select/plan}/databricks.yml (100%) rename acceptance/bundle/{plan/select => select/plan}/my_script.py (100%) rename acceptance/bundle/{plan/select => select/plan}/out.plan.direct.txt (100%) rename acceptance/bundle/{plan/select => select/plan}/out.plan.terraform.txt (100%) rename acceptance/bundle/{plan/select => select/plan}/out.test.toml (100%) rename acceptance/bundle/{plan/select => select/plan}/output.txt (100%) rename acceptance/bundle/{plan/select => select/plan}/script (100%) create mode 100644 acceptance/bundle/select/test.toml diff --git a/acceptance/bundle/plan/select-dep/out.plan.direct.txt b/acceptance/bundle/plan/select-dep/out.plan.direct.txt deleted file mode 100644 index 148d316ffa5..00000000000 --- a/acceptance/bundle/plan/select-dep/out.plan.direct.txt +++ /dev/null @@ -1,17 +0,0 @@ - ->>> [CLI] bundle plan --select jobs.foo --select jobs.bar -create jobs.bar -create jobs.foo - -Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle plan --select jobs.bar -create jobs.bar - -Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle plan --select jobs.foo -create jobs.bar -create jobs.foo - -Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/plan/select-dep/script b/acceptance/bundle/plan/select-dep/script deleted file mode 100644 index 41036e4f7ce..00000000000 --- a/acceptance/bundle/plan/select-dep/script +++ /dev/null @@ -1,12 +0,0 @@ -# --select is only supported by the direct engine; terraform errors out. -# Output differs per engine, so it is captured in a per-engine file. -{ -# Both resources selected explicitly. -errcode trace $CLI bundle plan --select jobs.foo --select jobs.bar - -# Select the dependency alone: bar has no outgoing references. -errcode trace $CLI bundle plan --select jobs.bar - -# Select only foo: bar is auto-included because foo references ${resources.jobs.bar.id}. -errcode trace $CLI bundle plan --select jobs.foo -} &> out.plan.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/plan/select-dep/test.toml b/acceptance/bundle/plan/select-dep/test.toml deleted file mode 100644 index 7d36fb9dc18..00000000000 --- a/acceptance/bundle/plan/select-dep/test.toml +++ /dev/null @@ -1,2 +0,0 @@ -Local = true -Cloud = false diff --git a/acceptance/bundle/plan/select/test.toml b/acceptance/bundle/plan/select/test.toml deleted file mode 100644 index 7d36fb9dc18..00000000000 --- a/acceptance/bundle/plan/select/test.toml +++ /dev/null @@ -1,2 +0,0 @@ -Local = true -Cloud = false diff --git a/acceptance/bundle/plan/select-dep/databricks.yml b/acceptance/bundle/select/deploy/databricks.yml similarity index 79% rename from acceptance/bundle/plan/select-dep/databricks.yml rename to acceptance/bundle/select/deploy/databricks.yml index f77d9557684..a394f46022e 100644 --- a/acceptance/bundle/plan/select-dep/databricks.yml +++ b/acceptance/bundle/select/deploy/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: plan-select-dep + name: select-deploy resources: jobs: @@ -12,3 +12,6 @@ resources: - task_key: run_bar run_job_task: job_id: ${resources.jobs.bar.id} + + baz: + name: job-baz diff --git a/acceptance/bundle/select/deploy/out.deploy.direct.txt b/acceptance/bundle/select/deploy/out.deploy.direct.txt new file mode 100644 index 00000000000..a8bea68a696 --- /dev/null +++ b/acceptance/bundle/select/deploy/out.deploy.direct.txt @@ -0,0 +1,56 @@ + +>>> [CLI] bundle plan --select jobs.foo +create jobs.bar +create jobs.foo + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle deploy --select jobs.foo +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select-deploy/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> print_requests.py --sort //jobs +{ + "method": "POST", + "path": "/api/2.2/jobs/create", + "body": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select-deploy/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "job-bar", + "queue": { + "enabled": true + } + } +} +{ + "method": "POST", + "path": "/api/2.2/jobs/create", + "body": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select-deploy/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "job-foo", + "queue": { + "enabled": true + }, + "tasks": [ + { + "run_job_task": { + "job_id": [NUMID] + }, + "task_key": "run_bar" + } + ] + } +} diff --git a/acceptance/bundle/plan/select-dep/out.plan.terraform.txt b/acceptance/bundle/select/deploy/out.deploy.terraform.txt similarity index 57% rename from acceptance/bundle/plan/select-dep/out.plan.terraform.txt rename to acceptance/bundle/select/deploy/out.deploy.terraform.txt index c8f57803b98..f778eeebb07 100644 --- a/acceptance/bundle/plan/select-dep/out.plan.terraform.txt +++ b/acceptance/bundle/select/deploy/out.deploy.terraform.txt @@ -1,18 +1,14 @@ ->>> [CLI] bundle plan --select jobs.foo --select jobs.bar +>>> [CLI] bundle plan --select jobs.foo Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct Exit code: 1 ->>> [CLI] bundle plan --select jobs.bar +>>> [CLI] bundle deploy --select jobs.foo Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct Exit code: 1 ->>> [CLI] bundle plan --select jobs.foo -Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct - - -Exit code: 1 +>>> print_requests.py --sort //jobs diff --git a/acceptance/bundle/plan/select-dep/out.test.toml b/acceptance/bundle/select/deploy/out.test.toml similarity index 100% rename from acceptance/bundle/plan/select-dep/out.test.toml rename to acceptance/bundle/select/deploy/out.test.toml diff --git a/acceptance/bundle/plan/select-dep/output.txt b/acceptance/bundle/select/deploy/output.txt similarity index 100% rename from acceptance/bundle/plan/select-dep/output.txt rename to acceptance/bundle/select/deploy/output.txt diff --git a/acceptance/bundle/select/deploy/script b/acceptance/bundle/select/deploy/script new file mode 100644 index 00000000000..6f8fb763ef2 --- /dev/null +++ b/acceptance/bundle/select/deploy/script @@ -0,0 +1,12 @@ +# --select is only supported by the direct engine; terraform errors out. +# Output differs per engine, so it is captured in a per-engine file. +{ +# Plan shows foo plus its dependency bar, but not the independent baz. +errcode trace $CLI bundle plan --select jobs.foo + +# Deploy only foo: bar is created as a dependency, baz is never deployed. +errcode trace $CLI bundle deploy --select jobs.foo + +# Confirm via recorded requests that only bar and foo were created. +errcode trace print_requests.py --sort //jobs +} &> out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/select/deploy/test.toml b/acceptance/bundle/select/deploy/test.toml new file mode 100644 index 00000000000..159efe02696 --- /dev/null +++ b/acceptance/bundle/select/deploy/test.toml @@ -0,0 +1 @@ +RecordRequests = true diff --git a/acceptance/bundle/plan/select/databricks.yml b/acceptance/bundle/select/plan/databricks.yml similarity index 100% rename from acceptance/bundle/plan/select/databricks.yml rename to acceptance/bundle/select/plan/databricks.yml diff --git a/acceptance/bundle/plan/select/my_script.py b/acceptance/bundle/select/plan/my_script.py similarity index 100% rename from acceptance/bundle/plan/select/my_script.py rename to acceptance/bundle/select/plan/my_script.py diff --git a/acceptance/bundle/plan/select/out.plan.direct.txt b/acceptance/bundle/select/plan/out.plan.direct.txt similarity index 100% rename from acceptance/bundle/plan/select/out.plan.direct.txt rename to acceptance/bundle/select/plan/out.plan.direct.txt diff --git a/acceptance/bundle/plan/select/out.plan.terraform.txt b/acceptance/bundle/select/plan/out.plan.terraform.txt similarity index 100% rename from acceptance/bundle/plan/select/out.plan.terraform.txt rename to acceptance/bundle/select/plan/out.plan.terraform.txt diff --git a/acceptance/bundle/plan/select/out.test.toml b/acceptance/bundle/select/plan/out.test.toml similarity index 100% rename from acceptance/bundle/plan/select/out.test.toml rename to acceptance/bundle/select/plan/out.test.toml diff --git a/acceptance/bundle/plan/select/output.txt b/acceptance/bundle/select/plan/output.txt similarity index 100% rename from acceptance/bundle/plan/select/output.txt rename to acceptance/bundle/select/plan/output.txt diff --git a/acceptance/bundle/plan/select/script b/acceptance/bundle/select/plan/script similarity index 100% rename from acceptance/bundle/plan/select/script rename to acceptance/bundle/select/plan/script diff --git a/acceptance/bundle/select/test.toml b/acceptance/bundle/select/test.toml new file mode 100644 index 00000000000..85ce448afd3 --- /dev/null +++ b/acceptance/bundle/select/test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +Ignore = [".databricks", ".gitignore"] From 5dc2c35631b0faca4b566323a45f636a9660f513 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 14:15:01 +0200 Subject: [PATCH 12/25] bundle: cover serialized-plan deploy variant in --select test Combine plan and deploy in the select/deploy test using readplanarg, and add EnvMatrix.READPLAN so the subset is deployed both inline and from a serialized plan (--plan). The serialized plan (out.plan.direct.json) contains only foo and its dependency bar, and the deploy creates exactly those two regardless of path. Also restore the minimal diff in cmd/bundle/plan.go: keep the existing InitFunc guard instead of making it unconditional. Co-authored-by: Isaac --- .../select/deploy/out.deploy.direct.txt | 8 +-- .../select/deploy/out.deploy.terraform.txt | 4 +- .../bundle/select/deploy/out.plan.direct.json | 59 +++++++++++++++++++ .../select/deploy/out.plan.terraform.json | 0 acceptance/bundle/select/deploy/out.test.toml | 1 + acceptance/bundle/select/deploy/script | 14 +++-- acceptance/bundle/select/deploy/test.toml | 3 + cmd/bundle/plan.go | 21 ++++--- 8 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 acceptance/bundle/select/deploy/out.plan.direct.json create mode 100644 acceptance/bundle/select/deploy/out.plan.terraform.json diff --git a/acceptance/bundle/select/deploy/out.deploy.direct.txt b/acceptance/bundle/select/deploy/out.deploy.direct.txt index a8bea68a696..c2a28501dbb 100644 --- a/acceptance/bundle/select/deploy/out.deploy.direct.txt +++ b/acceptance/bundle/select/deploy/out.deploy.direct.txt @@ -1,11 +1,7 @@ ->>> [CLI] bundle plan --select jobs.foo -create jobs.bar -create jobs.foo +>>> [CLI] bundle plan --select jobs.foo -o json -Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle deploy --select jobs.foo +=== bundle deploy --select jobs.foo Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select-deploy/default/files... Deploying resources... Updating deployment state... diff --git a/acceptance/bundle/select/deploy/out.deploy.terraform.txt b/acceptance/bundle/select/deploy/out.deploy.terraform.txt index f778eeebb07..22bd0a73b02 100644 --- a/acceptance/bundle/select/deploy/out.deploy.terraform.txt +++ b/acceptance/bundle/select/deploy/out.deploy.terraform.txt @@ -1,11 +1,11 @@ ->>> [CLI] bundle plan --select jobs.foo +>>> [CLI] bundle plan --select jobs.foo -o json Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct Exit code: 1 ->>> [CLI] bundle deploy --select jobs.foo +=== bundle deploy --select jobs.foo Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct diff --git a/acceptance/bundle/select/deploy/out.plan.direct.json b/acceptance/bundle/select/deploy/out.plan.direct.json new file mode 100644 index 00000000000..464b091cdfa --- /dev/null +++ b/acceptance/bundle/select/deploy/out.plan.direct.json @@ -0,0 +1,59 @@ +{ + "plan_version": 2, + "cli_version": "[DEV_VERSION]", + "plan": { + "resources.jobs.bar": { + "action": "create", + "new_state": { + "value": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select-deploy/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "job-bar", + "queue": { + "enabled": true + } + } + } + }, + "resources.jobs.foo": { + "depends_on": [ + { + "node": "resources.jobs.bar", + "label": "${resources.jobs.bar.id}" + } + ], + "action": "create", + "new_state": { + "value": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select-deploy/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "job-foo", + "queue": { + "enabled": true + }, + "tasks": [ + { + "run_job_task": { + "job_id": 0 + }, + "task_key": "run_bar" + } + ] + }, + "vars": { + "tasks[0].run_job_task.job_id": "${resources.jobs.bar.id}" + } + } + } + } +} diff --git a/acceptance/bundle/select/deploy/out.plan.terraform.json b/acceptance/bundle/select/deploy/out.plan.terraform.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/acceptance/bundle/select/deploy/out.test.toml b/acceptance/bundle/select/deploy/out.test.toml index f784a183258..8ffbd40f24c 100644 --- a/acceptance/bundle/select/deploy/out.test.toml +++ b/acceptance/bundle/select/deploy/out.test.toml @@ -1,3 +1,4 @@ Local = true Cloud = false EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/select/deploy/script b/acceptance/bundle/select/deploy/script index 6f8fb763ef2..beee41685a1 100644 --- a/acceptance/bundle/select/deploy/script +++ b/acceptance/bundle/select/deploy/script @@ -1,12 +1,16 @@ # --select is only supported by the direct engine; terraform errors out. # Output differs per engine, so it is captured in a per-engine file. { -# Plan shows foo plus its dependency bar, but not the independent baz. -errcode trace $CLI bundle plan --select jobs.foo +# Serialize the filtered plan: only foo and its dependency bar, never baz. +errcode trace $CLI bundle plan --select jobs.foo -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json -# Deploy only foo: bar is created as a dependency, baz is never deployed. -errcode trace $CLI bundle deploy --select jobs.foo +# Deploy only foo. Without --plan the plan is computed inline; with --plan +# (READPLAN=1, direct only) the serialized plan above is applied as-is. +# Not traced: readplanarg varies the command line between READPLAN variants, +# which must produce identical output. +title "bundle deploy --select jobs.foo\n" +errcode $CLI bundle deploy --select jobs.foo $(readplanarg out.plan.direct.json) -# Confirm via recorded requests that only bar and foo were created. +# Confirm via recorded requests that only bar and foo were created, never baz. errcode trace print_requests.py --sort //jobs } &> out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/select/deploy/test.toml b/acceptance/bundle/select/deploy/test.toml index 159efe02696..c8b9f1d8b07 100644 --- a/acceptance/bundle/select/deploy/test.toml +++ b/acceptance/bundle/select/deploy/test.toml @@ -1 +1,4 @@ RecordRequests = true +# Also run a variant that deploys a serialized (already filtered) plan via --plan. +# terraform + READPLAN=1 is excluded globally (see root test.toml). +EnvMatrix.READPLAN = ["", "1"] diff --git a/cmd/bundle/plan.go b/cmd/bundle/plan.go index 9c7f22365d0..b3a2227bcdb 100644 --- a/cmd/bundle/plan.go +++ b/cmd/bundle/plan.go @@ -43,16 +43,19 @@ It is useful for previewing changes before running 'bundle deploy'.`, PreDeployChecks: true, } - opts.InitFunc = func(b *bundle.Bundle) { - b.Config.Bundle.Force = force - b.Select = selectResources - - if cmd.Flag("compute-id").Changed { - b.Config.Bundle.ClusterId = clusterId - } + // Only add InitFunc if we need to set force, cluster ID or selection + if force || len(selectResources) > 0 || cmd.Flag("compute-id").Changed || cmd.Flag("cluster-id").Changed { + opts.InitFunc = func(b *bundle.Bundle) { + b.Config.Bundle.Force = force + b.Select = selectResources + + if cmd.Flag("compute-id").Changed { + b.Config.Bundle.ClusterId = clusterId + } - if cmd.Flag("cluster-id").Changed { - b.Config.Bundle.ClusterId = clusterId + if cmd.Flag("cluster-id").Changed { + b.Config.Bundle.ClusterId = clusterId + } } } From 21294042b1e335500e43c0966b8146c8ebc574e0 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 14:48:19 +0200 Subject: [PATCH 13/25] bundle: consolidate --select tests into a single acceptance test Merge select/plan and select/deploy into one acceptance/bundle/select test against a single config. It covers selector resolution (not-found, qualified, unqualified-unique, repeated, comma-separated, transitive dependency inclusion) and an end-to-end deploy of the selected subset, including the serialized-plan (READPLAN) variant. Co-authored-by: Isaac --- .../bundle/select/{deploy => }/databricks.yml | 2 +- .../select/deploy/out.deploy.terraform.txt | 14 ------- acceptance/bundle/select/deploy/script | 16 -------- acceptance/bundle/select/deploy/test.toml | 4 -- .../out.deploy.direct.txt => out.direct.txt} | 41 +++++++++++++++++-- .../select/{deploy => }/out.plan.direct.json | 4 +- .../{deploy => }/out.plan.terraform.json | 0 ...t.plan.terraform.txt => out.terraform.txt} | 22 ++++++++-- .../bundle/select/{deploy => }/out.test.toml | 0 .../bundle/select/{deploy => }/output.txt | 0 acceptance/bundle/select/plan/databricks.yml | 32 --------------- acceptance/bundle/select/plan/my_script.py | 1 - .../bundle/select/plan/out.plan.direct.txt | 34 --------------- acceptance/bundle/select/plan/out.test.toml | 3 -- acceptance/bundle/select/plan/output.txt | 0 acceptance/bundle/select/plan/script | 21 ---------- acceptance/bundle/select/script | 27 ++++++++++++ acceptance/bundle/select/test.toml | 4 ++ 18 files changed, 90 insertions(+), 135 deletions(-) rename acceptance/bundle/select/{deploy => }/databricks.yml (91%) delete mode 100644 acceptance/bundle/select/deploy/out.deploy.terraform.txt delete mode 100644 acceptance/bundle/select/deploy/script delete mode 100644 acceptance/bundle/select/deploy/test.toml rename acceptance/bundle/select/{deploy/out.deploy.direct.txt => out.direct.txt} (56%) rename acceptance/bundle/select/{deploy => }/out.plan.direct.json (93%) rename acceptance/bundle/select/{deploy => }/out.plan.terraform.json (100%) rename acceptance/bundle/select/{plan/out.plan.terraform.txt => out.terraform.txt} (61%) rename acceptance/bundle/select/{deploy => }/out.test.toml (100%) rename acceptance/bundle/select/{deploy => }/output.txt (100%) delete mode 100644 acceptance/bundle/select/plan/databricks.yml delete mode 100644 acceptance/bundle/select/plan/my_script.py delete mode 100644 acceptance/bundle/select/plan/out.plan.direct.txt delete mode 100644 acceptance/bundle/select/plan/out.test.toml delete mode 100644 acceptance/bundle/select/plan/output.txt delete mode 100644 acceptance/bundle/select/plan/script create mode 100644 acceptance/bundle/select/script diff --git a/acceptance/bundle/select/deploy/databricks.yml b/acceptance/bundle/select/databricks.yml similarity index 91% rename from acceptance/bundle/select/deploy/databricks.yml rename to acceptance/bundle/select/databricks.yml index a394f46022e..f9bd45dfbd4 100644 --- a/acceptance/bundle/select/deploy/databricks.yml +++ b/acceptance/bundle/select/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: select-deploy + name: select resources: jobs: diff --git a/acceptance/bundle/select/deploy/out.deploy.terraform.txt b/acceptance/bundle/select/deploy/out.deploy.terraform.txt deleted file mode 100644 index 22bd0a73b02..00000000000 --- a/acceptance/bundle/select/deploy/out.deploy.terraform.txt +++ /dev/null @@ -1,14 +0,0 @@ - ->>> [CLI] bundle plan --select jobs.foo -o json -Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct - - -Exit code: 1 - -=== bundle deploy --select jobs.foo -Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct - - -Exit code: 1 - ->>> print_requests.py --sort //jobs diff --git a/acceptance/bundle/select/deploy/script b/acceptance/bundle/select/deploy/script deleted file mode 100644 index beee41685a1..00000000000 --- a/acceptance/bundle/select/deploy/script +++ /dev/null @@ -1,16 +0,0 @@ -# --select is only supported by the direct engine; terraform errors out. -# Output differs per engine, so it is captured in a per-engine file. -{ -# Serialize the filtered plan: only foo and its dependency bar, never baz. -errcode trace $CLI bundle plan --select jobs.foo -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json - -# Deploy only foo. Without --plan the plan is computed inline; with --plan -# (READPLAN=1, direct only) the serialized plan above is applied as-is. -# Not traced: readplanarg varies the command line between READPLAN variants, -# which must produce identical output. -title "bundle deploy --select jobs.foo\n" -errcode $CLI bundle deploy --select jobs.foo $(readplanarg out.plan.direct.json) - -# Confirm via recorded requests that only bar and foo were created, never baz. -errcode trace print_requests.py --sort //jobs -} &> out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/select/deploy/test.toml b/acceptance/bundle/select/deploy/test.toml deleted file mode 100644 index c8b9f1d8b07..00000000000 --- a/acceptance/bundle/select/deploy/test.toml +++ /dev/null @@ -1,4 +0,0 @@ -RecordRequests = true -# Also run a variant that deploys a serialized (already filtered) plan via --plan. -# terraform + READPLAN=1 is excluded globally (see root test.toml). -EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/select/deploy/out.deploy.direct.txt b/acceptance/bundle/select/out.direct.txt similarity index 56% rename from acceptance/bundle/select/deploy/out.deploy.direct.txt rename to acceptance/bundle/select/out.direct.txt index c2a28501dbb..8b799726289 100644 --- a/acceptance/bundle/select/deploy/out.deploy.direct.txt +++ b/acceptance/bundle/select/out.direct.txt @@ -1,8 +1,43 @@ +>>> [CLI] bundle plan --select no_such_resource +Error: no such resource: no_such_resource + + +Exit code: 1 + +>>> [CLI] bundle plan --select jobs.no_such_job +Error: no such resource: jobs.no_such_job + + +Exit code: 1 + +>>> [CLI] bundle plan --select bar +create jobs.bar + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select jobs.bar --select jobs.baz +create jobs.bar +create jobs.baz + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select jobs.bar,jobs.baz +create jobs.bar +create jobs.baz + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan --select jobs.foo +create jobs.bar +create jobs.foo + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged + >>> [CLI] bundle plan --select jobs.foo -o json === bundle deploy --select jobs.foo -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select-deploy/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select/default/files... Deploying resources... Updating deployment state... Deployment complete! @@ -14,7 +49,7 @@ Deployment complete! "body": { "deployment": { "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select-deploy/default/state/metadata.json" + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select/default/state/metadata.json" }, "edit_mode": "UI_LOCKED", "format": "MULTI_TASK", @@ -31,7 +66,7 @@ Deployment complete! "body": { "deployment": { "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select-deploy/default/state/metadata.json" + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select/default/state/metadata.json" }, "edit_mode": "UI_LOCKED", "format": "MULTI_TASK", diff --git a/acceptance/bundle/select/deploy/out.plan.direct.json b/acceptance/bundle/select/out.plan.direct.json similarity index 93% rename from acceptance/bundle/select/deploy/out.plan.direct.json rename to acceptance/bundle/select/out.plan.direct.json index 464b091cdfa..2866ddf1d0c 100644 --- a/acceptance/bundle/select/deploy/out.plan.direct.json +++ b/acceptance/bundle/select/out.plan.direct.json @@ -8,7 +8,7 @@ "value": { "deployment": { "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select-deploy/default/state/metadata.json" + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select/default/state/metadata.json" }, "edit_mode": "UI_LOCKED", "format": "MULTI_TASK", @@ -32,7 +32,7 @@ "value": { "deployment": { "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select-deploy/default/state/metadata.json" + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select/default/state/metadata.json" }, "edit_mode": "UI_LOCKED", "format": "MULTI_TASK", diff --git a/acceptance/bundle/select/deploy/out.plan.terraform.json b/acceptance/bundle/select/out.plan.terraform.json similarity index 100% rename from acceptance/bundle/select/deploy/out.plan.terraform.json rename to acceptance/bundle/select/out.plan.terraform.json diff --git a/acceptance/bundle/select/plan/out.plan.terraform.txt b/acceptance/bundle/select/out.terraform.txt similarity index 61% rename from acceptance/bundle/select/plan/out.plan.terraform.txt rename to acceptance/bundle/select/out.terraform.txt index 4c0c5e8c45c..c643b2cd71f 100644 --- a/acceptance/bundle/select/plan/out.plan.terraform.txt +++ b/acceptance/bundle/select/out.terraform.txt @@ -11,26 +11,40 @@ Error: --select is only supported with the direct engine. See https://docs.datab Exit code: 1 ->>> [CLI] bundle plan --select job_b +>>> [CLI] bundle plan --select bar Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct Exit code: 1 ->>> [CLI] bundle plan --select jobs.job_a +>>> [CLI] bundle plan --select jobs.bar --select jobs.baz Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct Exit code: 1 ->>> [CLI] bundle plan --select job_a --select job_b +>>> [CLI] bundle plan --select jobs.bar,jobs.baz Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct Exit code: 1 ->>> [CLI] bundle plan --select job_a,job_b +>>> [CLI] bundle plan --select jobs.foo Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct Exit code: 1 + +>>> [CLI] bundle plan --select jobs.foo -o json +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 + +=== bundle deploy --select jobs.foo +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 + +>>> print_requests.py --sort //jobs diff --git a/acceptance/bundle/select/deploy/out.test.toml b/acceptance/bundle/select/out.test.toml similarity index 100% rename from acceptance/bundle/select/deploy/out.test.toml rename to acceptance/bundle/select/out.test.toml diff --git a/acceptance/bundle/select/deploy/output.txt b/acceptance/bundle/select/output.txt similarity index 100% rename from acceptance/bundle/select/deploy/output.txt rename to acceptance/bundle/select/output.txt diff --git a/acceptance/bundle/select/plan/databricks.yml b/acceptance/bundle/select/plan/databricks.yml deleted file mode 100644 index f25f36ad2f0..00000000000 --- a/acceptance/bundle/select/plan/databricks.yml +++ /dev/null @@ -1,32 +0,0 @@ -bundle: - name: plan-select - -resources: - jobs: - job_a: - name: job-a - tasks: - - task_key: task1 - spark_python_task: - python_file: "./my_script.py" - environment_key: "env" - environments: - - environment_key: "env" - spec: - client: "1" - - job_b: - name: job-b - tasks: - - task_key: task1 - spark_python_task: - python_file: "./my_script.py" - environment_key: "env" - environments: - - environment_key: "env" - spec: - client: "1" - - pipelines: - my_pipeline: - name: my-pipeline diff --git a/acceptance/bundle/select/plan/my_script.py b/acceptance/bundle/select/plan/my_script.py deleted file mode 100644 index 11b15b1a458..00000000000 --- a/acceptance/bundle/select/plan/my_script.py +++ /dev/null @@ -1 +0,0 @@ -print("hello") diff --git a/acceptance/bundle/select/plan/out.plan.direct.txt b/acceptance/bundle/select/plan/out.plan.direct.txt deleted file mode 100644 index a95f7d37023..00000000000 --- a/acceptance/bundle/select/plan/out.plan.direct.txt +++ /dev/null @@ -1,34 +0,0 @@ - ->>> [CLI] bundle plan --select no_such_resource -Error: no such resource: no_such_resource - - -Exit code: 1 - ->>> [CLI] bundle plan --select jobs.no_such_job -Error: no such resource: jobs.no_such_job - - -Exit code: 1 - ->>> [CLI] bundle plan --select job_b -create jobs.job_b - -Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle plan --select jobs.job_a -create jobs.job_a - -Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle plan --select job_a --select job_b -create jobs.job_a -create jobs.job_b - -Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle plan --select job_a,job_b -create jobs.job_a -create jobs.job_b - -Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/select/plan/out.test.toml b/acceptance/bundle/select/plan/out.test.toml deleted file mode 100644 index f784a183258..00000000000 --- a/acceptance/bundle/select/plan/out.test.toml +++ /dev/null @@ -1,3 +0,0 @@ -Local = true -Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/select/plan/output.txt b/acceptance/bundle/select/plan/output.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/acceptance/bundle/select/plan/script b/acceptance/bundle/select/plan/script deleted file mode 100644 index f246bea9099..00000000000 --- a/acceptance/bundle/select/plan/script +++ /dev/null @@ -1,21 +0,0 @@ -# --select is only supported by the direct engine; terraform errors out. -# Output differs per engine, so it is captured in a per-engine file. -{ -# Non-existent resource -errcode trace $CLI bundle plan --select no_such_resource - -# Qualified name for non-existent resource -errcode trace $CLI bundle plan --select jobs.no_such_job - -# Select a unique resource by unqualified name -errcode trace $CLI bundle plan --select job_b - -# Select a resource by qualified name -errcode trace $CLI bundle plan --select jobs.job_a - -# Repeated --select: both resources are included -errcode trace $CLI bundle plan --select job_a --select job_b - -# Comma-separated --select: both resources are included -errcode trace $CLI bundle plan --select job_a,job_b -} &> out.plan.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/select/script b/acceptance/bundle/select/script new file mode 100644 index 00000000000..a4f7e6427d3 --- /dev/null +++ b/acceptance/bundle/select/script @@ -0,0 +1,27 @@ +# --select is only supported by the direct engine; terraform errors out. +# Output differs per engine, so it is captured in a per-engine file. +{ +# --- selector resolution --- +# Non-existent resource +errcode trace $CLI bundle plan --select no_such_resource +# Qualified name for a non-existent resource +errcode trace $CLI bundle plan --select jobs.no_such_job +# Unqualified name, unique across resource types +errcode trace $CLI bundle plan --select bar +# Repeated --select +errcode trace $CLI bundle plan --select jobs.bar --select jobs.baz +# Comma-separated --select (equivalent to repeating the flag) +errcode trace $CLI bundle plan --select jobs.bar,jobs.baz +# Qualified name; foo pulls in its dependency bar but not the independent baz +errcode trace $CLI bundle plan --select jobs.foo + +# --- deploy a subset --- +# Serialize the filtered plan, then deploy it: inline, or via --plan (READPLAN=1). +errcode trace $CLI bundle plan --select jobs.foo -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json +# Not traced: readplanarg varies the command line between READPLAN variants, +# which must produce identical output. +title "bundle deploy --select jobs.foo\n" +errcode $CLI bundle deploy --select jobs.foo $(readplanarg out.plan.direct.json) +# Confirm via recorded requests that only bar and foo were created, never baz. +errcode trace print_requests.py --sort //jobs +} &> out.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/select/test.toml b/acceptance/bundle/select/test.toml index 85ce448afd3..584d2d3538f 100644 --- a/acceptance/bundle/select/test.toml +++ b/acceptance/bundle/select/test.toml @@ -1,3 +1,7 @@ Local = true Cloud = false Ignore = [".databricks", ".gitignore"] +RecordRequests = true +# Also run a variant that deploys a serialized (already filtered) plan via --plan. +# terraform + READPLAN=1 is excluded globally (see root test.toml). +EnvMatrix.READPLAN = ["", "1"] From 1b2e8daaf60e9bf0ebc0c3ddd0fdc759f738eedd Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 15:11:49 +0200 Subject: [PATCH 14/25] bundle: rename SelectResources->ResolveSelect, split error test, trim units MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mutator only resolves and normalizes b.Select (validating existence and expanding to type.name); it does not filter. Rename it to ResolveSelect to reflect that — filtering happens later via plan.FilterToSelected. Move the resolution-error cases (not-found, qualified-not-found) into a dedicated acceptance/bundle/select_errors test, pinned to direct (on terraform every --select hits the engine rejection first, covered by bundle/select). Trim the unit test to the cases acceptance can't cover: selector normalization and the ambiguous-selector error (unreachable in a loadable bundle because UniqueResourceKeys forbids duplicate keys across types). Co-authored-by: Isaac --- acceptance/bundle/select/out.direct.txt | 12 --- acceptance/bundle/select/out.terraform.txt | 12 --- acceptance/bundle/select/script | 4 - .../bundle/select_errors/databricks.yml | 7 ++ acceptance/bundle/select_errors/out.test.toml | 3 + acceptance/bundle/select_errors/output.txt | 12 +++ acceptance/bundle/select_errors/script | 9 ++ acceptance/bundle/select_errors/test.toml | 6 ++ ...{select_resources.go => resolve_select.go} | 21 ++--- bundle/config/mutator/resolve_select_test.go | 52 +++++++++++ .../config/mutator/select_resources_test.go | 86 ------------------- bundle/phases/initialize.go | 9 +- 12 files changed, 105 insertions(+), 128 deletions(-) create mode 100644 acceptance/bundle/select_errors/databricks.yml create mode 100644 acceptance/bundle/select_errors/out.test.toml create mode 100644 acceptance/bundle/select_errors/output.txt create mode 100644 acceptance/bundle/select_errors/script create mode 100644 acceptance/bundle/select_errors/test.toml rename bundle/config/mutator/{select_resources.go => resolve_select.go} (69%) create mode 100644 bundle/config/mutator/resolve_select_test.go delete mode 100644 bundle/config/mutator/select_resources_test.go diff --git a/acceptance/bundle/select/out.direct.txt b/acceptance/bundle/select/out.direct.txt index 8b799726289..1d4aea0b926 100644 --- a/acceptance/bundle/select/out.direct.txt +++ b/acceptance/bundle/select/out.direct.txt @@ -1,16 +1,4 @@ ->>> [CLI] bundle plan --select no_such_resource -Error: no such resource: no_such_resource - - -Exit code: 1 - ->>> [CLI] bundle plan --select jobs.no_such_job -Error: no such resource: jobs.no_such_job - - -Exit code: 1 - >>> [CLI] bundle plan --select bar create jobs.bar diff --git a/acceptance/bundle/select/out.terraform.txt b/acceptance/bundle/select/out.terraform.txt index c643b2cd71f..5f1c6d35fc4 100644 --- a/acceptance/bundle/select/out.terraform.txt +++ b/acceptance/bundle/select/out.terraform.txt @@ -1,16 +1,4 @@ ->>> [CLI] bundle plan --select no_such_resource -Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct - - -Exit code: 1 - ->>> [CLI] bundle plan --select jobs.no_such_job -Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct - - -Exit code: 1 - >>> [CLI] bundle plan --select bar Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct diff --git a/acceptance/bundle/select/script b/acceptance/bundle/select/script index a4f7e6427d3..465eb9762b7 100644 --- a/acceptance/bundle/select/script +++ b/acceptance/bundle/select/script @@ -2,10 +2,6 @@ # Output differs per engine, so it is captured in a per-engine file. { # --- selector resolution --- -# Non-existent resource -errcode trace $CLI bundle plan --select no_such_resource -# Qualified name for a non-existent resource -errcode trace $CLI bundle plan --select jobs.no_such_job # Unqualified name, unique across resource types errcode trace $CLI bundle plan --select bar # Repeated --select diff --git a/acceptance/bundle/select_errors/databricks.yml b/acceptance/bundle/select_errors/databricks.yml new file mode 100644 index 00000000000..e093f64dd7e --- /dev/null +++ b/acceptance/bundle/select_errors/databricks.yml @@ -0,0 +1,7 @@ +bundle: + name: select-errors + +resources: + jobs: + my_job: + name: my-job diff --git a/acceptance/bundle/select_errors/out.test.toml b/acceptance/bundle/select_errors/out.test.toml new file mode 100644 index 00000000000..e90b6d5d1ba --- /dev/null +++ b/acceptance/bundle/select_errors/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/select_errors/output.txt b/acceptance/bundle/select_errors/output.txt new file mode 100644 index 00000000000..f70c74476ba --- /dev/null +++ b/acceptance/bundle/select_errors/output.txt @@ -0,0 +1,12 @@ + +>>> [CLI] bundle plan --select no_such_resource +Error: no such resource: no_such_resource + + +Exit code: 1 + +>>> [CLI] bundle plan --select jobs.no_such_job +Error: no such resource: jobs.no_such_job + + +Exit code: 1 diff --git a/acceptance/bundle/select_errors/script b/acceptance/bundle/select_errors/script new file mode 100644 index 00000000000..6aa18a72950 --- /dev/null +++ b/acceptance/bundle/select_errors/script @@ -0,0 +1,9 @@ +# Unqualified name that matches no resource. +errcode trace $CLI bundle plan --select no_such_resource + +# Qualified name whose resource does not exist. +errcode trace $CLI bundle plan --select jobs.no_such_job + +# Note: the "ambiguous resource" error cannot be reproduced here. UniqueResourceKeys +# forbids two resources sharing a key across types, so an ambiguous selection is +# unreachable in a loadable bundle; it is covered by a unit test instead. diff --git a/acceptance/bundle/select_errors/test.toml b/acceptance/bundle/select_errors/test.toml new file mode 100644 index 00000000000..5648f5bc803 --- /dev/null +++ b/acceptance/bundle/select_errors/test.toml @@ -0,0 +1,6 @@ +Local = true +Cloud = false +Ignore = [".databricks", ".gitignore"] +# Selector resolution only happens on the direct engine; on terraform every +# --select hits the engine-rejection error first (covered by bundle/select). +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/bundle/config/mutator/select_resources.go b/bundle/config/mutator/resolve_select.go similarity index 69% rename from bundle/config/mutator/select_resources.go rename to bundle/config/mutator/resolve_select.go index 455fec7c0fb..3435050ce45 100644 --- a/bundle/config/mutator/select_resources.go +++ b/bundle/config/mutator/resolve_select.go @@ -8,22 +8,23 @@ import ( "github.com/databricks/cli/libs/diag" ) -type selectResources struct{} +type resolveSelect struct{} -// SelectResources returns a mutator that resolves and validates the selectors in -// b.Select. Selectors may be "type.name" (e.g. "jobs.myjob") or just "name" if -// unique across all resource types. The mutator does not filter the config; callers -// are responsible for filtering (via the plan graph or a direct config filter). +// ResolveSelect returns a mutator that resolves and validates the selectors in +// b.Select, normalizing each to its qualified "type.name" form. Selectors may be +// "type.name" (e.g. "jobs.myjob") or just "name" if unique across all resource +// types. The mutator does not filter the config; the direct engine selects against +// the resolved keys later via plan.FilterToSelected. // If b.Select is empty, this is a no-op. -func SelectResources() bundle.Mutator { - return &selectResources{} +func ResolveSelect() bundle.Mutator { + return &resolveSelect{} } -func (m *selectResources) Name() string { - return "SelectResources" +func (m *resolveSelect) Name() string { + return "ResolveSelect" } -func (m *selectResources) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { +func (m *resolveSelect) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { if len(b.Select) == 0 { return nil } diff --git a/bundle/config/mutator/resolve_select_test.go b/bundle/config/mutator/resolve_select_test.go new file mode 100644 index 00000000000..d7954900c8c --- /dev/null +++ b/bundle/config/mutator/resolve_select_test.go @@ -0,0 +1,52 @@ +package mutator_test + +import ( + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/mutator" + "github.com/databricks/cli/bundle/config/resources" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestResolveSelect_Normalizes checks that selectors are normalized to their +// qualified "type.name" form in place, without filtering the config. End-to-end +// selection behavior is covered by acceptance/bundle/select. +func TestResolveSelect_Normalizes(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Jobs: map[string]*resources.Job{"my_job": {}}, + Pipelines: map[string]*resources.Pipeline{"my_pipeline": {}}, + }, + }, + } + b.Select = []string{"my_job", "pipelines.my_pipeline"} + diags := bundle.Apply(t.Context(), b, mutator.ResolveSelect()) + require.NoError(t, diags.Error()) + assert.Equal(t, []string{"jobs.my_job", "pipelines.my_pipeline"}, b.Select) + // Config is left untouched; the mutator only resolves selectors. + assert.Len(t, b.Config.Resources.Jobs, 1) + assert.Len(t, b.Config.Resources.Pipelines, 1) +} + +// TestResolveSelect_Ambiguous covers the ambiguous-selector error. This cannot be +// exercised by an acceptance test: UniqueResourceKeys forbids two resources sharing +// a key across types, so an ambiguous selection is unreachable in a loadable bundle. +func TestResolveSelect_Ambiguous(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Jobs: map[string]*resources.Job{"thing": {}}, + Pipelines: map[string]*resources.Pipeline{"thing": {}}, + }, + }, + } + b.Select = []string{"thing"} + diags := bundle.Apply(t.Context(), b, mutator.ResolveSelect()) + require.Error(t, diags.Error()) + assert.ErrorContains(t, diags.Error(), "ambiguous resource: thing") + assert.ErrorContains(t, diags.Error(), "use a qualified name to disambiguate") +} diff --git a/bundle/config/mutator/select_resources_test.go b/bundle/config/mutator/select_resources_test.go deleted file mode 100644 index 0b4e182a20f..00000000000 --- a/bundle/config/mutator/select_resources_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package mutator_test - -import ( - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/mutator" - "github.com/databricks/cli/bundle/config/resources" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func bundleWithJobsAndPipelines() *bundle.Bundle { - return &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Jobs: map[string]*resources.Job{"my_job": {}}, - Pipelines: map[string]*resources.Pipeline{"my_pipeline": {}}, - }, - }, - } -} - -func TestSelectResources_NoOp(t *testing.T) { - b := bundleWithJobsAndPipelines() - diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) - require.NoError(t, diags.Error()) - // Mutator does not filter config — both resources remain. - assert.Len(t, b.Config.Resources.Jobs, 1) - assert.Len(t, b.Config.Resources.Pipelines, 1) -} - -func TestSelectResources_UnqualifiedUnique(t *testing.T) { - b := bundleWithJobsAndPipelines() - b.Select = []string{"my_job"} - diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) - require.NoError(t, diags.Error()) - // Selector resolved to qualified form; config not filtered. - assert.Equal(t, []string{"jobs.my_job"}, b.Select) - assert.Len(t, b.Config.Resources.Jobs, 1) - assert.Len(t, b.Config.Resources.Pipelines, 1) -} - -func TestSelectResources_QualifiedName(t *testing.T) { - b := bundleWithJobsAndPipelines() - b.Select = []string{"pipelines.my_pipeline"} - diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) - require.NoError(t, diags.Error()) - assert.Equal(t, []string{"pipelines.my_pipeline"}, b.Select) -} - -func TestSelectResources_NotFound(t *testing.T) { - b := bundleWithJobsAndPipelines() - b.Select = []string{"nonexistent"} - diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) - require.Error(t, diags.Error()) - assert.ErrorContains(t, diags.Error(), "no such resource: nonexistent") -} - -func TestSelectResources_QualifiedNotFound(t *testing.T) { - b := bundleWithJobsAndPipelines() - b.Select = []string{"jobs.nonexistent"} - diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) - require.Error(t, diags.Error()) - assert.ErrorContains(t, diags.Error(), "no such resource: jobs.nonexistent") -} - -func TestSelectResources_Ambiguous(t *testing.T) { - b := bundleWithJobsAndPipelines() - b.Config.Resources.Pipelines["my_job"] = &resources.Pipeline{} - b.Select = []string{"my_job"} - diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) - require.Error(t, diags.Error()) - assert.ErrorContains(t, diags.Error(), "ambiguous resource: my_job") - assert.ErrorContains(t, diags.Error(), "use a qualified name to disambiguate") -} - -func TestSelectResources_MultipleSelectors(t *testing.T) { - b := bundleWithJobsAndPipelines() - b.Config.Resources.Jobs["other_job"] = &resources.Job{} - b.Select = []string{"my_job", "my_pipeline"} - diags := bundle.Apply(t.Context(), b, mutator.SelectResources()) - require.NoError(t, diags.Error()) - assert.Equal(t, []string{"jobs.my_job", "pipelines.my_pipeline"}, b.Select) -} diff --git a/bundle/phases/initialize.go b/bundle/phases/initialize.go index d5e836f88f1..a40506ebb18 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -142,10 +142,11 @@ func Initialize(ctx context.Context, b *bundle.Bundle) { // After PythonMutator, mutators must not change bundle resources, or such changes are not // going to be visible in Python code. - // Filter resources to only those selected via --select, if any. - // Runs after all resource mutations so that dynamically added resources are visible, - // and before validation so that only selected resources are validated. - mutator.SelectResources(), + // Resolve --select selectors against the materialized resources: normalize + // each to its "type.name" form and validate it exists. Runs after all resource + // mutations so that dynamically added resources are visible. This does not + // filter resources; the direct engine selects against the resolved keys later. + mutator.ResolveSelect(), // Validate all required fields are set. This is run after variable interpolation and PyDABs mutators // since they can also set and modify resources. From 36fc9d13a55386ebf965ed3c86918c8c1641c3d0 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 15:17:58 +0200 Subject: [PATCH 15/25] bundle: fold plan InitFunc into ProcessOptions literal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Always set InitFunc (matching deploy.go) instead of guarding it behind a flag check — the guard was a micro-optimization and running the no-op mutator is harmless. Inline it in the ProcessOptions literal for clarity. Co-authored-by: Isaac --- cmd/bundle/plan.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cmd/bundle/plan.go b/cmd/bundle/plan.go index b3a2227bcdb..20df8cb5f0f 100644 --- a/cmd/bundle/plan.go +++ b/cmd/bundle/plan.go @@ -41,11 +41,7 @@ It is useful for previewing changes before running 'bundle deploy'.`, FastValidate: true, Build: true, PreDeployChecks: true, - } - - // Only add InitFunc if we need to set force, cluster ID or selection - if force || len(selectResources) > 0 || cmd.Flag("compute-id").Changed || cmd.Flag("cluster-id").Changed { - opts.InitFunc = func(b *bundle.Bundle) { + InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Force = force b.Select = selectResources @@ -56,7 +52,7 @@ It is useful for previewing changes before running 'bundle deploy'.`, if cmd.Flag("cluster-id").Changed { b.Config.Bundle.ClusterId = clusterId } - } + }, } b, stateDesc, err := utils.ProcessBundleRet(cmd, opts) From cb1e46795eb0e7287933e6970644170199937bf6 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 15:30:59 +0200 Subject: [PATCH 16/25] bundle: check --select engine after state pull; add ambiguous accept test The effective engine is only known for certain after the state is pulled (stateDesc.Engine), so move the --select direct-engine check there instead of resolving it separately before Initialize. ResolveSelect goes back to pure selector resolution. Add acceptance/bundle/select_errors/ambiguous documenting that an ambiguous selector ("thing" as both a job and a pipeline) is rejected at load time by UniqueResourceKeys, before selector resolution. Split select_errors into missing/ and ambiguous/; both run on terraform and direct (resolution errors are engine-independent now that the engine check runs later). Co-authored-by: Isaac --- .../select_errors/ambiguous/databricks.yml | 13 ++++++++++ .../select_errors/ambiguous/out.test.toml | 3 +++ .../bundle/select_errors/ambiguous/output.txt | 10 +++++++ .../bundle/select_errors/ambiguous/script | 4 +++ .../{ => missing}/databricks.yml | 0 .../select_errors/missing/out.test.toml | 3 +++ .../select_errors/{ => missing}/output.txt | 0 .../bundle/select_errors/missing/script | 8 ++++++ acceptance/bundle/select_errors/out.test.toml | 3 --- acceptance/bundle/select_errors/script | 9 ------- acceptance/bundle/select_errors/test.toml | 3 --- bundle/config/mutator/resolve_select_test.go | 7 ++--- cmd/bundle/utils/process.go | 26 +++++++------------ 13 files changed, 54 insertions(+), 35 deletions(-) create mode 100644 acceptance/bundle/select_errors/ambiguous/databricks.yml create mode 100644 acceptance/bundle/select_errors/ambiguous/out.test.toml create mode 100644 acceptance/bundle/select_errors/ambiguous/output.txt create mode 100644 acceptance/bundle/select_errors/ambiguous/script rename acceptance/bundle/select_errors/{ => missing}/databricks.yml (100%) create mode 100644 acceptance/bundle/select_errors/missing/out.test.toml rename acceptance/bundle/select_errors/{ => missing}/output.txt (100%) create mode 100644 acceptance/bundle/select_errors/missing/script delete mode 100644 acceptance/bundle/select_errors/out.test.toml delete mode 100644 acceptance/bundle/select_errors/script diff --git a/acceptance/bundle/select_errors/ambiguous/databricks.yml b/acceptance/bundle/select_errors/ambiguous/databricks.yml new file mode 100644 index 00000000000..27962d00d55 --- /dev/null +++ b/acceptance/bundle/select_errors/ambiguous/databricks.yml @@ -0,0 +1,13 @@ +bundle: + name: select-ambiguous + +# "thing" is defined as both a job and a pipeline. This is what an ambiguous +# --select selector would resolve to, but UniqueResourceKeys rejects it at load +# time, so the bundle never reaches selector resolution. +resources: + jobs: + thing: + name: job-thing + pipelines: + thing: + name: pipeline-thing diff --git a/acceptance/bundle/select_errors/ambiguous/out.test.toml b/acceptance/bundle/select_errors/ambiguous/out.test.toml new file mode 100644 index 00000000000..f784a183258 --- /dev/null +++ b/acceptance/bundle/select_errors/ambiguous/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/select_errors/ambiguous/output.txt b/acceptance/bundle/select_errors/ambiguous/output.txt new file mode 100644 index 00000000000..433b92a66b3 --- /dev/null +++ b/acceptance/bundle/select_errors/ambiguous/output.txt @@ -0,0 +1,10 @@ + +>>> [CLI] bundle plan --select thing +Error: multiple resources or scripts have been defined with the same key: thing + at resources.jobs.thing + resources.pipelines.thing + in databricks.yml:10:7 + databricks.yml:13:7 + + +Exit code: 1 diff --git a/acceptance/bundle/select_errors/ambiguous/script b/acceptance/bundle/select_errors/ambiguous/script new file mode 100644 index 00000000000..7ffea8fd5b4 --- /dev/null +++ b/acceptance/bundle/select_errors/ambiguous/script @@ -0,0 +1,4 @@ +# An ambiguous selector ("thing" is both a job and a pipeline) cannot actually be +# reached: UniqueResourceKeys rejects the duplicate key at load time, before --select +# is resolved. The ResolveSelect "ambiguous resource" branch is covered by a unit test. +errcode trace $CLI bundle plan --select thing diff --git a/acceptance/bundle/select_errors/databricks.yml b/acceptance/bundle/select_errors/missing/databricks.yml similarity index 100% rename from acceptance/bundle/select_errors/databricks.yml rename to acceptance/bundle/select_errors/missing/databricks.yml diff --git a/acceptance/bundle/select_errors/missing/out.test.toml b/acceptance/bundle/select_errors/missing/out.test.toml new file mode 100644 index 00000000000..f784a183258 --- /dev/null +++ b/acceptance/bundle/select_errors/missing/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/select_errors/output.txt b/acceptance/bundle/select_errors/missing/output.txt similarity index 100% rename from acceptance/bundle/select_errors/output.txt rename to acceptance/bundle/select_errors/missing/output.txt diff --git a/acceptance/bundle/select_errors/missing/script b/acceptance/bundle/select_errors/missing/script new file mode 100644 index 00000000000..0c3193764ef --- /dev/null +++ b/acceptance/bundle/select_errors/missing/script @@ -0,0 +1,8 @@ +# Selectors are resolved by the ResolveSelect mutator during initialize, before the +# engine is known, so these errors are identical on both engines. + +# Unqualified name that matches no resource. +errcode trace $CLI bundle plan --select no_such_resource + +# Qualified name whose resource does not exist. +errcode trace $CLI bundle plan --select jobs.no_such_job diff --git a/acceptance/bundle/select_errors/out.test.toml b/acceptance/bundle/select_errors/out.test.toml deleted file mode 100644 index e90b6d5d1ba..00000000000 --- a/acceptance/bundle/select_errors/out.test.toml +++ /dev/null @@ -1,3 +0,0 @@ -Local = true -Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/select_errors/script b/acceptance/bundle/select_errors/script deleted file mode 100644 index 6aa18a72950..00000000000 --- a/acceptance/bundle/select_errors/script +++ /dev/null @@ -1,9 +0,0 @@ -# Unqualified name that matches no resource. -errcode trace $CLI bundle plan --select no_such_resource - -# Qualified name whose resource does not exist. -errcode trace $CLI bundle plan --select jobs.no_such_job - -# Note: the "ambiguous resource" error cannot be reproduced here. UniqueResourceKeys -# forbids two resources sharing a key across types, so an ambiguous selection is -# unreachable in a loadable bundle; it is covered by a unit test instead. diff --git a/acceptance/bundle/select_errors/test.toml b/acceptance/bundle/select_errors/test.toml index 5648f5bc803..85ce448afd3 100644 --- a/acceptance/bundle/select_errors/test.toml +++ b/acceptance/bundle/select_errors/test.toml @@ -1,6 +1,3 @@ Local = true Cloud = false Ignore = [".databricks", ".gitignore"] -# Selector resolution only happens on the direct engine; on terraform every -# --select hits the engine-rejection error first (covered by bundle/select). -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/bundle/config/mutator/resolve_select_test.go b/bundle/config/mutator/resolve_select_test.go index d7954900c8c..d116d9ca601 100644 --- a/bundle/config/mutator/resolve_select_test.go +++ b/bundle/config/mutator/resolve_select_test.go @@ -32,9 +32,10 @@ func TestResolveSelect_Normalizes(t *testing.T) { assert.Len(t, b.Config.Resources.Pipelines, 1) } -// TestResolveSelect_Ambiguous covers the ambiguous-selector error. This cannot be -// exercised by an acceptance test: UniqueResourceKeys forbids two resources sharing -// a key across types, so an ambiguous selection is unreachable in a loadable bundle. +// TestResolveSelect_Ambiguous covers the ambiguous-selector error in isolation. +// In a real bundle this is unreachable — UniqueResourceKeys rejects two resources +// sharing a key across types before this mutator runs (see the acceptance test +// bundle/select_errors/ambiguous for the real, load-time error). func TestResolveSelect_Ambiguous(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ diff --git a/cmd/bundle/utils/process.go b/cmd/bundle/utils/process.go index e498c877fc7..d61c4525530 100644 --- a/cmd/bundle/utils/process.go +++ b/cmd/bundle/utils/process.go @@ -139,23 +139,6 @@ func ProcessBundleRet(cmd *cobra.Command, opts ProcessOptions) (b *bundle.Bundle bundle.ApplyFuncContext(ctx, b, func(context.Context, *bundle.Bundle) { opts.InitFunc(b) }) } - if len(b.Select) > 0 { - // --select (plan/deploy only) is only supported by the direct engine, which - // tracks resource dependencies in the plan graph. Reject it on terraform with - // an actionable error rather than silently planning/deploying every resource. - // Checked before Initialize so the engine mismatch is reported before any - // per-resource validation. The actual filtering happens in RunPlan via - // plan.FilterToSelected. - engineSetting, err := ResolveEngineSetting(ctx, b) - if err != nil { - return b, nil, err - } - if !engineSetting.Type.IsDirect() { - logdiag.LogError(ctx, errors.New("--select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct")) - return b, nil, root.ErrAlreadyPrinted - } - } - if !opts.SkipInitialize { t0 := time.Now() phases.Initialize(ctx, b) @@ -202,6 +185,15 @@ func ProcessBundleRet(cmd *cobra.Command, opts ProcessOptions) (b *bundle.Bundle } cmd.SetContext(ctx) + // --select is only supported by the direct engine, which tracks resource + // dependencies in the plan graph (used to expand the selection transitively). + // The engine is only known for certain after the state is pulled, so reject it + // here rather than silently planning/deploying every resource on terraform. + if len(b.Select) > 0 && !stateDesc.Engine.IsDirect() { + logdiag.LogError(ctx, errors.New("--select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct")) + return b, stateDesc, root.ErrAlreadyPrinted + } + // Open direct engine state once for all subsequent operations (ExportState, CalculatePlan, Apply, etc.) needDirectState := stateDesc.Engine.IsDirect() && (opts.InitIDs || opts.ErrorOnEmptyState || opts.Deploy || opts.ReadPlanPath != "" || opts.PreDeployChecks || opts.PostStateFunc != nil) if needDirectState { From cb85374bc8e06a74ca2b92446f68fac86f06d51b Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 15:33:32 +0200 Subject: [PATCH 17/25] bundle: regroup --select acceptance tests under select/{ok,missing,ambiguous} Move the happy-path test to select/ok, and fold select_errors/{missing, ambiguous} into select/. Common settings (Local/Cloud/Ignore) live in select/test.toml; the deploy-specific RecordRequests and READPLAN matrix stay in select/ok/test.toml. Co-authored-by: Isaac --- .../bundle/{select_errors => select}/ambiguous/databricks.yml | 0 .../bundle/{select_errors => select}/ambiguous/out.test.toml | 0 .../bundle/{select_errors => select}/ambiguous/output.txt | 0 acceptance/bundle/{select_errors => select}/ambiguous/script | 0 .../bundle/{select_errors => select}/missing/databricks.yml | 0 .../bundle/{select_errors => select}/missing/out.test.toml | 0 .../bundle/{select_errors => select}/missing/output.txt | 0 acceptance/bundle/{select_errors => select}/missing/script | 0 acceptance/bundle/select/{ => ok}/databricks.yml | 0 acceptance/bundle/select/{ => ok}/out.direct.txt | 0 acceptance/bundle/select/{ => ok}/out.plan.direct.json | 0 acceptance/bundle/select/{ => ok}/out.plan.terraform.json | 0 acceptance/bundle/select/{ => ok}/out.terraform.txt | 0 acceptance/bundle/select/{ => ok}/out.test.toml | 0 acceptance/bundle/select/{ => ok}/output.txt | 0 acceptance/bundle/select/{ => ok}/script | 0 acceptance/bundle/select/ok/test.toml | 4 ++++ acceptance/bundle/select/test.toml | 4 ---- acceptance/bundle/select_errors/test.toml | 3 --- 19 files changed, 4 insertions(+), 7 deletions(-) rename acceptance/bundle/{select_errors => select}/ambiguous/databricks.yml (100%) rename acceptance/bundle/{select_errors => select}/ambiguous/out.test.toml (100%) rename acceptance/bundle/{select_errors => select}/ambiguous/output.txt (100%) rename acceptance/bundle/{select_errors => select}/ambiguous/script (100%) rename acceptance/bundle/{select_errors => select}/missing/databricks.yml (100%) rename acceptance/bundle/{select_errors => select}/missing/out.test.toml (100%) rename acceptance/bundle/{select_errors => select}/missing/output.txt (100%) rename acceptance/bundle/{select_errors => select}/missing/script (100%) rename acceptance/bundle/select/{ => ok}/databricks.yml (100%) rename acceptance/bundle/select/{ => ok}/out.direct.txt (100%) rename acceptance/bundle/select/{ => ok}/out.plan.direct.json (100%) rename acceptance/bundle/select/{ => ok}/out.plan.terraform.json (100%) rename acceptance/bundle/select/{ => ok}/out.terraform.txt (100%) rename acceptance/bundle/select/{ => ok}/out.test.toml (100%) rename acceptance/bundle/select/{ => ok}/output.txt (100%) rename acceptance/bundle/select/{ => ok}/script (100%) create mode 100644 acceptance/bundle/select/ok/test.toml delete mode 100644 acceptance/bundle/select_errors/test.toml diff --git a/acceptance/bundle/select_errors/ambiguous/databricks.yml b/acceptance/bundle/select/ambiguous/databricks.yml similarity index 100% rename from acceptance/bundle/select_errors/ambiguous/databricks.yml rename to acceptance/bundle/select/ambiguous/databricks.yml diff --git a/acceptance/bundle/select_errors/ambiguous/out.test.toml b/acceptance/bundle/select/ambiguous/out.test.toml similarity index 100% rename from acceptance/bundle/select_errors/ambiguous/out.test.toml rename to acceptance/bundle/select/ambiguous/out.test.toml diff --git a/acceptance/bundle/select_errors/ambiguous/output.txt b/acceptance/bundle/select/ambiguous/output.txt similarity index 100% rename from acceptance/bundle/select_errors/ambiguous/output.txt rename to acceptance/bundle/select/ambiguous/output.txt diff --git a/acceptance/bundle/select_errors/ambiguous/script b/acceptance/bundle/select/ambiguous/script similarity index 100% rename from acceptance/bundle/select_errors/ambiguous/script rename to acceptance/bundle/select/ambiguous/script diff --git a/acceptance/bundle/select_errors/missing/databricks.yml b/acceptance/bundle/select/missing/databricks.yml similarity index 100% rename from acceptance/bundle/select_errors/missing/databricks.yml rename to acceptance/bundle/select/missing/databricks.yml diff --git a/acceptance/bundle/select_errors/missing/out.test.toml b/acceptance/bundle/select/missing/out.test.toml similarity index 100% rename from acceptance/bundle/select_errors/missing/out.test.toml rename to acceptance/bundle/select/missing/out.test.toml diff --git a/acceptance/bundle/select_errors/missing/output.txt b/acceptance/bundle/select/missing/output.txt similarity index 100% rename from acceptance/bundle/select_errors/missing/output.txt rename to acceptance/bundle/select/missing/output.txt diff --git a/acceptance/bundle/select_errors/missing/script b/acceptance/bundle/select/missing/script similarity index 100% rename from acceptance/bundle/select_errors/missing/script rename to acceptance/bundle/select/missing/script diff --git a/acceptance/bundle/select/databricks.yml b/acceptance/bundle/select/ok/databricks.yml similarity index 100% rename from acceptance/bundle/select/databricks.yml rename to acceptance/bundle/select/ok/databricks.yml diff --git a/acceptance/bundle/select/out.direct.txt b/acceptance/bundle/select/ok/out.direct.txt similarity index 100% rename from acceptance/bundle/select/out.direct.txt rename to acceptance/bundle/select/ok/out.direct.txt diff --git a/acceptance/bundle/select/out.plan.direct.json b/acceptance/bundle/select/ok/out.plan.direct.json similarity index 100% rename from acceptance/bundle/select/out.plan.direct.json rename to acceptance/bundle/select/ok/out.plan.direct.json diff --git a/acceptance/bundle/select/out.plan.terraform.json b/acceptance/bundle/select/ok/out.plan.terraform.json similarity index 100% rename from acceptance/bundle/select/out.plan.terraform.json rename to acceptance/bundle/select/ok/out.plan.terraform.json diff --git a/acceptance/bundle/select/out.terraform.txt b/acceptance/bundle/select/ok/out.terraform.txt similarity index 100% rename from acceptance/bundle/select/out.terraform.txt rename to acceptance/bundle/select/ok/out.terraform.txt diff --git a/acceptance/bundle/select/out.test.toml b/acceptance/bundle/select/ok/out.test.toml similarity index 100% rename from acceptance/bundle/select/out.test.toml rename to acceptance/bundle/select/ok/out.test.toml diff --git a/acceptance/bundle/select/output.txt b/acceptance/bundle/select/ok/output.txt similarity index 100% rename from acceptance/bundle/select/output.txt rename to acceptance/bundle/select/ok/output.txt diff --git a/acceptance/bundle/select/script b/acceptance/bundle/select/ok/script similarity index 100% rename from acceptance/bundle/select/script rename to acceptance/bundle/select/ok/script diff --git a/acceptance/bundle/select/ok/test.toml b/acceptance/bundle/select/ok/test.toml new file mode 100644 index 00000000000..c8b9f1d8b07 --- /dev/null +++ b/acceptance/bundle/select/ok/test.toml @@ -0,0 +1,4 @@ +RecordRequests = true +# Also run a variant that deploys a serialized (already filtered) plan via --plan. +# terraform + READPLAN=1 is excluded globally (see root test.toml). +EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/select/test.toml b/acceptance/bundle/select/test.toml index 584d2d3538f..85ce448afd3 100644 --- a/acceptance/bundle/select/test.toml +++ b/acceptance/bundle/select/test.toml @@ -1,7 +1,3 @@ Local = true Cloud = false Ignore = [".databricks", ".gitignore"] -RecordRequests = true -# Also run a variant that deploys a serialized (already filtered) plan via --plan. -# terraform + READPLAN=1 is excluded globally (see root test.toml). -EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/select_errors/test.toml b/acceptance/bundle/select_errors/test.toml deleted file mode 100644 index 85ce448afd3..00000000000 --- a/acceptance/bundle/select_errors/test.toml +++ /dev/null @@ -1,3 +0,0 @@ -Local = true -Cloud = false -Ignore = [".databricks", ".gitignore"] From 849814c7c7b5989874c4076990c39f29c6031d5d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 15:34:48 +0200 Subject: [PATCH 18/25] update --- acceptance/bundle/select/ambiguous/output.txt | 10 ++++++++-- acceptance/bundle/select/ambiguous/script | 6 ++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/acceptance/bundle/select/ambiguous/output.txt b/acceptance/bundle/select/ambiguous/output.txt index 433b92a66b3..7dfcae53017 100644 --- a/acceptance/bundle/select/ambiguous/output.txt +++ b/acceptance/bundle/select/ambiguous/output.txt @@ -1,5 +1,5 @@ ->>> [CLI] bundle plan --select thing +>>> musterr [CLI] bundle plan --select thing Error: multiple resources or scripts have been defined with the same key: thing at resources.jobs.thing resources.pipelines.thing @@ -7,4 +7,10 @@ Error: multiple resources or scripts have been defined with the same key: thing databricks.yml:13:7 -Exit code: 1 +>>> musterr [CLI] bundle deploy --select thing +Error: multiple resources or scripts have been defined with the same key: thing + at resources.jobs.thing + resources.pipelines.thing + in databricks.yml:10:7 + databricks.yml:13:7 + diff --git a/acceptance/bundle/select/ambiguous/script b/acceptance/bundle/select/ambiguous/script index 7ffea8fd5b4..6f5afbc33d6 100644 --- a/acceptance/bundle/select/ambiguous/script +++ b/acceptance/bundle/select/ambiguous/script @@ -1,4 +1,2 @@ -# An ambiguous selector ("thing" is both a job and a pipeline) cannot actually be -# reached: UniqueResourceKeys rejects the duplicate key at load time, before --select -# is resolved. The ResolveSelect "ambiguous resource" branch is covered by a unit test. -errcode trace $CLI bundle plan --select thing +trace musterr $CLI bundle plan --select thing +trace musterr $CLI bundle deploy --select thing From 606505a62ba0aa9ae84b6f6c67f64220cf636e58 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 15:35:46 +0200 Subject: [PATCH 19/25] update --- acceptance/bundle/select/missing/script | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/acceptance/bundle/select/missing/script b/acceptance/bundle/select/missing/script index 0c3193764ef..6125b04ef17 100644 --- a/acceptance/bundle/select/missing/script +++ b/acceptance/bundle/select/missing/script @@ -1,8 +1,2 @@ -# Selectors are resolved by the ResolveSelect mutator during initialize, before the -# engine is known, so these errors are identical on both engines. - -# Unqualified name that matches no resource. -errcode trace $CLI bundle plan --select no_such_resource - -# Qualified name whose resource does not exist. -errcode trace $CLI bundle plan --select jobs.no_such_job +trace musterr $CLI bundle plan --select no_such_resource +trace musterr $CLI bundle deploy --select jobs.no_such_job From bba43415922d750bcb6e5d508ae67677938c0bad Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 15:40:31 +0200 Subject: [PATCH 20/25] Add NEXT_CHANGELOG entry for bundle --select Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index df070a324a5..24be6b2d716 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -11,5 +11,6 @@ * Retry transient HTTP 5xx and 408 errors in direct deployment engine ([#5349](https://github.com/databricks/cli/pull/5349), [#5364](https://github.com/databricks/cli/pull/5364)). * Preserve `.designer.ipynb` suffix when translating notebook task paths so Lakeflow Designer files referenced from a `notebook_task` resolve correctly in the workspace ([#5370](https://github.com/databricks/cli/pull/5370)). * Fix script output dropping last line without trailing newline ([#4995](https://github.com/databricks/cli/pull/4995)). +* Add `--select` flag to `bundle plan` and `bundle deploy` to plan/deploy a subset of resources (e.g. `--select my_job` or `--select jobs.my_job`); resources referenced by the selection are included transitively. Direct engine only ([#5413](https://github.com/databricks/cli/pull/5413)). ### Dependency updates From c2b30d58503b49ebe02a0e93a004a8b5052d6e8d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 17:04:21 +0200 Subject: [PATCH 21/25] Fix select/missing test: script out of sync with output The committed script used `trace musterr ... bundle deploy` while output.txt was generated from `errcode trace ... bundle plan`, so CI regenerated and diverged. Restore the intended errcode/plan script that matches the output. Co-authored-by: Isaac --- acceptance/bundle/select/missing/script | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/acceptance/bundle/select/missing/script b/acceptance/bundle/select/missing/script index 6125b04ef17..0c3193764ef 100644 --- a/acceptance/bundle/select/missing/script +++ b/acceptance/bundle/select/missing/script @@ -1,2 +1,8 @@ -trace musterr $CLI bundle plan --select no_such_resource -trace musterr $CLI bundle deploy --select jobs.no_such_job +# Selectors are resolved by the ResolveSelect mutator during initialize, before the +# engine is known, so these errors are identical on both engines. + +# Unqualified name that matches no resource. +errcode trace $CLI bundle plan --select no_such_resource + +# Qualified name whose resource does not exist. +errcode trace $CLI bundle plan --select jobs.no_such_job From d718aa4d30182c6034150c403bb035e1828a7ac9 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 20:44:40 +0200 Subject: [PATCH 22/25] bundle: run select/basic on cloud; test full deploy after partial deploy Rename select/ok to select/basic and extend it: - Enable on cloud (Local+Cloud) with a $UNIQUE_NAME-templated bundle and a destroy cleanup trap, pinned to the direct engine (the only engine where --select works). The serialized plan is written to an ignored plan.json so cloud runs don't diverge on remote state. - After the partial deploy (--select jobs.foo deploys foo+bar), run a full plan/deploy with no selection: the plan shows only baz to add with foo and bar unchanged, deploy creates baz, and a final plan reports no changes. Add select/rejected (terraform, local) covering the --select rejection that select/basic no longer exercises now that it is direct-only. Co-authored-by: Isaac --- .../databricks.yml.tmpl} | 8 +-- acceptance/bundle/select/basic/out.test.toml | 4 ++ .../{ok/out.direct.txt => basic/output.txt} | 60 ++++++++++++++++--- acceptance/bundle/select/basic/script | 40 +++++++++++++ acceptance/bundle/select/basic/test.toml | 10 ++++ .../bundle/select/ok/out.plan.direct.json | 59 ------------------ .../bundle/select/ok/out.plan.terraform.json | 0 acceptance/bundle/select/ok/out.terraform.txt | 38 ------------ acceptance/bundle/select/ok/out.test.toml | 4 -- acceptance/bundle/select/ok/output.txt | 0 acceptance/bundle/select/ok/script | 23 ------- acceptance/bundle/select/ok/test.toml | 4 -- .../bundle/select/rejected/databricks.yml | 7 +++ .../bundle/select/rejected/out.test.toml | 3 + acceptance/bundle/select/rejected/output.txt | 12 ++++ acceptance/bundle/select/rejected/script | 3 + acceptance/bundle/select/rejected/test.toml | 2 + 17 files changed, 138 insertions(+), 139 deletions(-) rename acceptance/bundle/select/{ok/databricks.yml => basic/databricks.yml.tmpl} (59%) create mode 100644 acceptance/bundle/select/basic/out.test.toml rename acceptance/bundle/select/{ok/out.direct.txt => basic/output.txt} (51%) create mode 100644 acceptance/bundle/select/basic/script create mode 100644 acceptance/bundle/select/basic/test.toml delete mode 100644 acceptance/bundle/select/ok/out.plan.direct.json delete mode 100644 acceptance/bundle/select/ok/out.plan.terraform.json delete mode 100644 acceptance/bundle/select/ok/out.terraform.txt delete mode 100644 acceptance/bundle/select/ok/out.test.toml delete mode 100644 acceptance/bundle/select/ok/output.txt delete mode 100644 acceptance/bundle/select/ok/script delete mode 100644 acceptance/bundle/select/ok/test.toml create mode 100644 acceptance/bundle/select/rejected/databricks.yml create mode 100644 acceptance/bundle/select/rejected/out.test.toml create mode 100644 acceptance/bundle/select/rejected/output.txt create mode 100644 acceptance/bundle/select/rejected/script create mode 100644 acceptance/bundle/select/rejected/test.toml diff --git a/acceptance/bundle/select/ok/databricks.yml b/acceptance/bundle/select/basic/databricks.yml.tmpl similarity index 59% rename from acceptance/bundle/select/ok/databricks.yml rename to acceptance/bundle/select/basic/databricks.yml.tmpl index f9bd45dfbd4..228b695e072 100644 --- a/acceptance/bundle/select/ok/databricks.yml +++ b/acceptance/bundle/select/basic/databricks.yml.tmpl @@ -1,17 +1,17 @@ bundle: - name: select + name: select-$UNIQUE_NAME resources: jobs: bar: - name: job-bar + name: bar-$UNIQUE_NAME foo: - name: job-foo + name: foo-$UNIQUE_NAME tasks: - task_key: run_bar run_job_task: job_id: ${resources.jobs.bar.id} baz: - name: job-baz + name: baz-$UNIQUE_NAME diff --git a/acceptance/bundle/select/basic/out.test.toml b/acceptance/bundle/select/basic/out.test.toml new file mode 100644 index 00000000000..8b995e4d177 --- /dev/null +++ b/acceptance/bundle/select/basic/out.test.toml @@ -0,0 +1,4 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] +EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/select/ok/out.direct.txt b/acceptance/bundle/select/basic/output.txt similarity index 51% rename from acceptance/bundle/select/ok/out.direct.txt rename to acceptance/bundle/select/basic/output.txt index 1d4aea0b926..ac1a6a46e11 100644 --- a/acceptance/bundle/select/ok/out.direct.txt +++ b/acceptance/bundle/select/basic/output.txt @@ -22,10 +22,8 @@ create jobs.foo Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged ->>> [CLI] bundle plan --select jobs.foo -o json - === bundle deploy --select jobs.foo -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select-[UNIQUE_NAME]/default/files... Deploying resources... Updating deployment state... Deployment complete! @@ -37,12 +35,12 @@ Deployment complete! "body": { "deployment": { "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select/default/state/metadata.json" + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select-[UNIQUE_NAME]/default/state/metadata.json" }, "edit_mode": "UI_LOCKED", "format": "MULTI_TASK", "max_concurrent_runs": 1, - "name": "job-bar", + "name": "bar-[UNIQUE_NAME]", "queue": { "enabled": true } @@ -54,12 +52,12 @@ Deployment complete! "body": { "deployment": { "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select/default/state/metadata.json" + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select-[UNIQUE_NAME]/default/state/metadata.json" }, "edit_mode": "UI_LOCKED", "format": "MULTI_TASK", "max_concurrent_runs": 1, - "name": "job-foo", + "name": "foo-[UNIQUE_NAME]", "queue": { "enabled": true }, @@ -73,3 +71,51 @@ Deployment complete! ] } } + +=== Full plan after partial deploy +>>> [CLI] bundle plan +create jobs.baz + +Plan: 1 to add, 0 to change, 0 to delete, 2 unchanged + +=== Full deploy +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> print_requests.py --sort //jobs +{ + "method": "POST", + "path": "/api/2.2/jobs/create", + "body": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select-[UNIQUE_NAME]/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "baz-[UNIQUE_NAME]", + "queue": { + "enabled": true + } + } +} + +=== Full plan again +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 3 unchanged + +=== Destroy +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.jobs.bar + delete resources.jobs.baz + delete resources.jobs.foo + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/select-[UNIQUE_NAME]/default + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/select/basic/script b/acceptance/bundle/select/basic/script new file mode 100644 index 00000000000..d4737c1b13a --- /dev/null +++ b/acceptance/bundle/select/basic/script @@ -0,0 +1,40 @@ +envsubst '$UNIQUE_NAME' < databricks.yml.tmpl > databricks.yml + +cleanup() { + title "Destroy" + trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +# --- selector resolution --- +# Unqualified name, unique across resource types. +trace $CLI bundle plan --select bar +# Repeated --select. +trace $CLI bundle plan --select jobs.bar --select jobs.baz +# Comma-separated --select (equivalent to repeating the flag). +trace $CLI bundle plan --select jobs.bar,jobs.baz +# Qualified name; foo pulls in its dependency bar but not the independent baz. +trace $CLI bundle plan --select jobs.foo + +# --- partial deploy --- +# Serialize the filtered plan, then deploy it: inline, or via --plan (READPLAN=1). +# The deploy is not traced because readplanarg varies the command line between +# READPLAN variants, which must produce identical output. +$CLI bundle plan --select jobs.foo -o json > plan.json +title "bundle deploy --select jobs.foo\n" +$CLI bundle deploy --select jobs.foo $(readplanarg plan.json) +# Only bar and foo were created, never baz. +trace print_requests.py --sort //jobs + +# --- full plan/deploy after the partial deploy --- +# foo and bar are already deployed, so only baz remains to create. +title "Full plan after partial deploy" +trace $CLI bundle plan +title "Full deploy" +trace $CLI bundle deploy +# Only baz is created this time. +trace print_requests.py --sort //jobs +# Everything is deployed now: no changes. +title "Full plan again" +trace $CLI bundle plan diff --git a/acceptance/bundle/select/basic/test.toml b/acceptance/bundle/select/basic/test.toml new file mode 100644 index 00000000000..8bad1594075 --- /dev/null +++ b/acceptance/bundle/select/basic/test.toml @@ -0,0 +1,10 @@ +Local = true +Cloud = true +RecordRequests = true +# --select is only supported by the direct engine, so this happy-path test runs on +# direct only (the rejection on terraform is covered by bundle/select/rejected). +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] +# Also run a variant that deploys a serialized (already filtered) plan via --plan. +EnvMatrix.READPLAN = ["", "1"] +# databricks.yml and plan.json are generated at runtime. +Ignore = [".databricks", ".gitignore", "databricks.yml", "plan.json"] diff --git a/acceptance/bundle/select/ok/out.plan.direct.json b/acceptance/bundle/select/ok/out.plan.direct.json deleted file mode 100644 index 2866ddf1d0c..00000000000 --- a/acceptance/bundle/select/ok/out.plan.direct.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "plan_version": 2, - "cli_version": "[DEV_VERSION]", - "plan": { - "resources.jobs.bar": { - "action": "create", - "new_state": { - "value": { - "deployment": { - "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select/default/state/metadata.json" - }, - "edit_mode": "UI_LOCKED", - "format": "MULTI_TASK", - "max_concurrent_runs": 1, - "name": "job-bar", - "queue": { - "enabled": true - } - } - } - }, - "resources.jobs.foo": { - "depends_on": [ - { - "node": "resources.jobs.bar", - "label": "${resources.jobs.bar.id}" - } - ], - "action": "create", - "new_state": { - "value": { - "deployment": { - "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/select/default/state/metadata.json" - }, - "edit_mode": "UI_LOCKED", - "format": "MULTI_TASK", - "max_concurrent_runs": 1, - "name": "job-foo", - "queue": { - "enabled": true - }, - "tasks": [ - { - "run_job_task": { - "job_id": 0 - }, - "task_key": "run_bar" - } - ] - }, - "vars": { - "tasks[0].run_job_task.job_id": "${resources.jobs.bar.id}" - } - } - } - } -} diff --git a/acceptance/bundle/select/ok/out.plan.terraform.json b/acceptance/bundle/select/ok/out.plan.terraform.json deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/acceptance/bundle/select/ok/out.terraform.txt b/acceptance/bundle/select/ok/out.terraform.txt deleted file mode 100644 index 5f1c6d35fc4..00000000000 --- a/acceptance/bundle/select/ok/out.terraform.txt +++ /dev/null @@ -1,38 +0,0 @@ - ->>> [CLI] bundle plan --select bar -Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct - - -Exit code: 1 - ->>> [CLI] bundle plan --select jobs.bar --select jobs.baz -Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct - - -Exit code: 1 - ->>> [CLI] bundle plan --select jobs.bar,jobs.baz -Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct - - -Exit code: 1 - ->>> [CLI] bundle plan --select jobs.foo -Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct - - -Exit code: 1 - ->>> [CLI] bundle plan --select jobs.foo -o json -Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct - - -Exit code: 1 - -=== bundle deploy --select jobs.foo -Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct - - -Exit code: 1 - ->>> print_requests.py --sort //jobs diff --git a/acceptance/bundle/select/ok/out.test.toml b/acceptance/bundle/select/ok/out.test.toml deleted file mode 100644 index 8ffbd40f24c..00000000000 --- a/acceptance/bundle/select/ok/out.test.toml +++ /dev/null @@ -1,4 +0,0 @@ -Local = true -Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] -EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/select/ok/output.txt b/acceptance/bundle/select/ok/output.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/acceptance/bundle/select/ok/script b/acceptance/bundle/select/ok/script deleted file mode 100644 index 465eb9762b7..00000000000 --- a/acceptance/bundle/select/ok/script +++ /dev/null @@ -1,23 +0,0 @@ -# --select is only supported by the direct engine; terraform errors out. -# Output differs per engine, so it is captured in a per-engine file. -{ -# --- selector resolution --- -# Unqualified name, unique across resource types -errcode trace $CLI bundle plan --select bar -# Repeated --select -errcode trace $CLI bundle plan --select jobs.bar --select jobs.baz -# Comma-separated --select (equivalent to repeating the flag) -errcode trace $CLI bundle plan --select jobs.bar,jobs.baz -# Qualified name; foo pulls in its dependency bar but not the independent baz -errcode trace $CLI bundle plan --select jobs.foo - -# --- deploy a subset --- -# Serialize the filtered plan, then deploy it: inline, or via --plan (READPLAN=1). -errcode trace $CLI bundle plan --select jobs.foo -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json -# Not traced: readplanarg varies the command line between READPLAN variants, -# which must produce identical output. -title "bundle deploy --select jobs.foo\n" -errcode $CLI bundle deploy --select jobs.foo $(readplanarg out.plan.direct.json) -# Confirm via recorded requests that only bar and foo were created, never baz. -errcode trace print_requests.py --sort //jobs -} &> out.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/select/ok/test.toml b/acceptance/bundle/select/ok/test.toml deleted file mode 100644 index c8b9f1d8b07..00000000000 --- a/acceptance/bundle/select/ok/test.toml +++ /dev/null @@ -1,4 +0,0 @@ -RecordRequests = true -# Also run a variant that deploys a serialized (already filtered) plan via --plan. -# terraform + READPLAN=1 is excluded globally (see root test.toml). -EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/select/rejected/databricks.yml b/acceptance/bundle/select/rejected/databricks.yml new file mode 100644 index 00000000000..2c5ff850e63 --- /dev/null +++ b/acceptance/bundle/select/rejected/databricks.yml @@ -0,0 +1,7 @@ +bundle: + name: select-rejected + +resources: + jobs: + my_job: + name: my-job diff --git a/acceptance/bundle/select/rejected/out.test.toml b/acceptance/bundle/select/rejected/out.test.toml new file mode 100644 index 00000000000..65156e0457c --- /dev/null +++ b/acceptance/bundle/select/rejected/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] diff --git a/acceptance/bundle/select/rejected/output.txt b/acceptance/bundle/select/rejected/output.txt new file mode 100644 index 00000000000..9f4417461f6 --- /dev/null +++ b/acceptance/bundle/select/rejected/output.txt @@ -0,0 +1,12 @@ + +>>> [CLI] bundle plan --select my_job +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 + +>>> [CLI] bundle deploy --select my_job +Error: --select is only supported with the direct engine. See https://docs.databricks.com/aws/en/dev-tools/bundles/direct + + +Exit code: 1 diff --git a/acceptance/bundle/select/rejected/script b/acceptance/bundle/select/rejected/script new file mode 100644 index 00000000000..8e5acc82b87 --- /dev/null +++ b/acceptance/bundle/select/rejected/script @@ -0,0 +1,3 @@ +# --select is only supported by the direct engine; both plan and deploy reject it. +errcode trace $CLI bundle plan --select my_job +errcode trace $CLI bundle deploy --select my_job diff --git a/acceptance/bundle/select/rejected/test.toml b/acceptance/bundle/select/rejected/test.toml new file mode 100644 index 00000000000..7d8b7840fab --- /dev/null +++ b/acceptance/bundle/select/rejected/test.toml @@ -0,0 +1,2 @@ +# --select is rejected on the terraform engine with an actionable error. +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] From b0153abe85083741c6e6d76a0a263f2fd9588536 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 21:09:30 +0200 Subject: [PATCH 23/25] bundle: serialize full plan via readplanarg in select/basic Apply the serialized-plan path to the full deploy after the partial deploy too: write the plan JSON to a temp (ignored) file and deploy via readplanarg. The JSON plans stay out of the recorded output because they embed remote state that differs between local and cloud; the textual `bundle plan` output is what's recorded and asserted. Co-authored-by: Isaac --- acceptance/bundle/select/basic/output.txt | 1 - acceptance/bundle/select/basic/script | 12 +++++++----- acceptance/bundle/select/basic/test.toml | 5 +++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/acceptance/bundle/select/basic/output.txt b/acceptance/bundle/select/basic/output.txt index ac1a6a46e11..cd1ebeeac9e 100644 --- a/acceptance/bundle/select/basic/output.txt +++ b/acceptance/bundle/select/basic/output.txt @@ -79,7 +79,6 @@ create jobs.baz Plan: 1 to add, 0 to change, 0 to delete, 2 unchanged === Full deploy ->>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select-[UNIQUE_NAME]/default/files... Deploying resources... Updating deployment state... diff --git a/acceptance/bundle/select/basic/script b/acceptance/bundle/select/basic/script index d4737c1b13a..26a70d00357 100644 --- a/acceptance/bundle/select/basic/script +++ b/acceptance/bundle/select/basic/script @@ -18,9 +18,10 @@ trace $CLI bundle plan --select jobs.bar,jobs.baz trace $CLI bundle plan --select jobs.foo # --- partial deploy --- -# Serialize the filtered plan, then deploy it: inline, or via --plan (READPLAN=1). -# The deploy is not traced because readplanarg varies the command line between -# READPLAN variants, which must produce identical output. +# Serialize the filtered plan to a temp file (kept out of the recorded output: the +# JSON embeds remote state that differs between local and cloud), then deploy it: +# inline, or via --plan (READPLAN=1). The deploy is not traced because readplanarg +# varies the command line between READPLAN variants, which must produce identical output. $CLI bundle plan --select jobs.foo -o json > plan.json title "bundle deploy --select jobs.foo\n" $CLI bundle deploy --select jobs.foo $(readplanarg plan.json) @@ -31,8 +32,9 @@ trace print_requests.py --sort //jobs # foo and bar are already deployed, so only baz remains to create. title "Full plan after partial deploy" trace $CLI bundle plan -title "Full deploy" -trace $CLI bundle deploy +$CLI bundle plan -o json > plan-full.json +title "Full deploy\n" +$CLI bundle deploy $(readplanarg plan-full.json) # Only baz is created this time. trace print_requests.py --sort //jobs # Everything is deployed now: no changes. diff --git a/acceptance/bundle/select/basic/test.toml b/acceptance/bundle/select/basic/test.toml index 8bad1594075..93ebc572acf 100644 --- a/acceptance/bundle/select/basic/test.toml +++ b/acceptance/bundle/select/basic/test.toml @@ -6,5 +6,6 @@ RecordRequests = true EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] # Also run a variant that deploys a serialized (already filtered) plan via --plan. EnvMatrix.READPLAN = ["", "1"] -# databricks.yml and plan.json are generated at runtime. -Ignore = [".databricks", ".gitignore", "databricks.yml", "plan.json"] +# databricks.yml and the serialized plans are generated at runtime; the plan JSON +# is kept out of the recorded output because it embeds local/cloud-specific state. +Ignore = [".databricks", ".gitignore", "databricks.yml", "plan.json", "plan-full.json"] From 576ba6e4e5568c7b7a384d8bb6efd2c1a5b4600d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 21:32:43 +0200 Subject: [PATCH 24/25] bundle: emit select_used telemetry metric when --select is used Set the select_used bool metric in the ResolveSelect mutator (runs only when --select is non-empty). Add a telemetry acceptance test asserting the metric is emitted on deploy. Co-authored-by: Isaac --- .../bundle/telemetry/deploy-select/databricks.yml | 7 +++++++ .../bundle/telemetry/deploy-select/out.test.toml | 3 +++ acceptance/bundle/telemetry/deploy-select/output.txt | 12 ++++++++++++ acceptance/bundle/telemetry/deploy-select/script | 5 +++++ acceptance/bundle/telemetry/deploy-select/test.toml | 2 ++ bundle/config/mutator/resolve_select.go | 3 +++ bundle/metrics/metrics.go | 1 + 7 files changed, 33 insertions(+) create mode 100644 acceptance/bundle/telemetry/deploy-select/databricks.yml create mode 100644 acceptance/bundle/telemetry/deploy-select/out.test.toml create mode 100644 acceptance/bundle/telemetry/deploy-select/output.txt create mode 100644 acceptance/bundle/telemetry/deploy-select/script create mode 100644 acceptance/bundle/telemetry/deploy-select/test.toml diff --git a/acceptance/bundle/telemetry/deploy-select/databricks.yml b/acceptance/bundle/telemetry/deploy-select/databricks.yml new file mode 100644 index 00000000000..2437ace1bde --- /dev/null +++ b/acceptance/bundle/telemetry/deploy-select/databricks.yml @@ -0,0 +1,7 @@ +bundle: + name: telemetry-select + +resources: + jobs: + my_job: + name: my-job diff --git a/acceptance/bundle/telemetry/deploy-select/out.test.toml b/acceptance/bundle/telemetry/deploy-select/out.test.toml new file mode 100644 index 00000000000..e90b6d5d1ba --- /dev/null +++ b/acceptance/bundle/telemetry/deploy-select/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/telemetry/deploy-select/output.txt b/acceptance/bundle/telemetry/deploy-select/output.txt new file mode 100644 index 00000000000..13d91fa10d4 --- /dev/null +++ b/acceptance/bundle/telemetry/deploy-select/output.txt @@ -0,0 +1,12 @@ + +>>> [CLI] bundle deploy --select my_job +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/telemetry-select/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> cat out.requests.txt +{ + "key": "select_used", + "value": true +} diff --git a/acceptance/bundle/telemetry/deploy-select/script b/acceptance/bundle/telemetry/deploy-select/script new file mode 100644 index 00000000000..c2ea2c767a7 --- /dev/null +++ b/acceptance/bundle/telemetry/deploy-select/script @@ -0,0 +1,5 @@ +trace $CLI bundle deploy --select my_job + +trace cat out.requests.txt | jq 'select(has("path") and .path == "/telemetry-ext") | .body.protoLogs[] | fromjson | .entry.databricks_cli_log.bundle_deploy_event.experimental.bool_values[] | select(.key == "select_used")' + +rm out.requests.txt diff --git a/acceptance/bundle/telemetry/deploy-select/test.toml b/acceptance/bundle/telemetry/deploy-select/test.toml new file mode 100644 index 00000000000..9707d39a124 --- /dev/null +++ b/acceptance/bundle/telemetry/deploy-select/test.toml @@ -0,0 +1,2 @@ +# --select is only supported by the direct engine. +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/bundle/config/mutator/resolve_select.go b/bundle/config/mutator/resolve_select.go index 3435050ce45..28d9751c322 100644 --- a/bundle/config/mutator/resolve_select.go +++ b/bundle/config/mutator/resolve_select.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/metrics" "github.com/databricks/cli/libs/diag" ) @@ -29,6 +30,8 @@ func (m *resolveSelect) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnost return nil } + b.Metrics.SetBoolValue(metrics.SelectUsed, true) + // Build reverse index: unqualified name → []"type.name" matches. byName := map[string][]string{} for _, group := range b.Config.Resources.AllResources() { diff --git a/bundle/metrics/metrics.go b/bundle/metrics/metrics.go index c3c0599789c..01d7166e23c 100644 --- a/bundle/metrics/metrics.go +++ b/bundle/metrics/metrics.go @@ -8,4 +8,5 @@ const ( PresetsNamePrefixIsSet = "presets_name_prefix_is_set" AppLifecycleStarted = "app_lifecycle_started" ClusterLifecycleStarted = "cluster_lifecycle_started" + SelectUsed = "select_used" ) From 820dc0e499d0db9b90933493fbac3d4eb117af62 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 2 Jun 2026 21:38:41 +0200 Subject: [PATCH 25/25] bundle: assert select telemetry and summary in select/basic Instead of a dedicated telemetry test, capture the select_used metric in the existing select/basic test via the print_telemetry_bool_values helper after the partial deploy. Also check `bundle summary` there, confirming foo and bar are deployed while baz is not. Co-authored-by: Isaac --- acceptance/bundle/select/basic/output.txt | 21 +++++++++++++++++++ acceptance/bundle/select/basic/script | 5 +++++ .../telemetry/deploy-select/databricks.yml | 7 ------- .../telemetry/deploy-select/out.test.toml | 3 --- .../bundle/telemetry/deploy-select/output.txt | 12 ----------- .../bundle/telemetry/deploy-select/script | 5 ----- .../bundle/telemetry/deploy-select/test.toml | 2 -- 7 files changed, 26 insertions(+), 29 deletions(-) delete mode 100644 acceptance/bundle/telemetry/deploy-select/databricks.yml delete mode 100644 acceptance/bundle/telemetry/deploy-select/out.test.toml delete mode 100644 acceptance/bundle/telemetry/deploy-select/output.txt delete mode 100644 acceptance/bundle/telemetry/deploy-select/script delete mode 100644 acceptance/bundle/telemetry/deploy-select/test.toml diff --git a/acceptance/bundle/select/basic/output.txt b/acceptance/bundle/select/basic/output.txt index cd1ebeeac9e..56c20a404d4 100644 --- a/acceptance/bundle/select/basic/output.txt +++ b/acceptance/bundle/select/basic/output.txt @@ -28,6 +28,9 @@ Deploying resources... Updating deployment state... Deployment complete! +=== Telemetry: +select_used true + >>> print_requests.py --sort //jobs { "method": "POST", @@ -72,6 +75,24 @@ Deployment complete! } } +>>> [CLI] bundle summary +Name: select-[UNIQUE_NAME] +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/select-[UNIQUE_NAME]/default +Resources: + Jobs: + bar: + Name: bar-[UNIQUE_NAME] + URL: [DATABRICKS_URL]/jobs/[NUMID]?w=[NUMID] + baz: + Name: baz-[UNIQUE_NAME] + URL: (not deployed) + foo: + Name: foo-[UNIQUE_NAME] + URL: [DATABRICKS_URL]/jobs/[NUMID]?w=[NUMID] + === Full plan after partial deploy >>> [CLI] bundle plan create jobs.baz diff --git a/acceptance/bundle/select/basic/script b/acceptance/bundle/select/basic/script index 26a70d00357..88c61a537b7 100644 --- a/acceptance/bundle/select/basic/script +++ b/acceptance/bundle/select/basic/script @@ -25,8 +25,13 @@ trace $CLI bundle plan --select jobs.foo $CLI bundle plan --select jobs.foo -o json > plan.json title "bundle deploy --select jobs.foo\n" $CLI bundle deploy --select jobs.foo $(readplanarg plan.json) +# The deploy reports that --select was used via telemetry. +title "Telemetry:\n" +print_telemetry_bool_values | grep '^select_used ' # Only bar and foo were created, never baz. trace print_requests.py --sort //jobs +# Summary after the partial deploy: foo and bar are deployed, baz is not. +trace $CLI bundle summary # --- full plan/deploy after the partial deploy --- # foo and bar are already deployed, so only baz remains to create. diff --git a/acceptance/bundle/telemetry/deploy-select/databricks.yml b/acceptance/bundle/telemetry/deploy-select/databricks.yml deleted file mode 100644 index 2437ace1bde..00000000000 --- a/acceptance/bundle/telemetry/deploy-select/databricks.yml +++ /dev/null @@ -1,7 +0,0 @@ -bundle: - name: telemetry-select - -resources: - jobs: - my_job: - name: my-job diff --git a/acceptance/bundle/telemetry/deploy-select/out.test.toml b/acceptance/bundle/telemetry/deploy-select/out.test.toml deleted file mode 100644 index e90b6d5d1ba..00000000000 --- a/acceptance/bundle/telemetry/deploy-select/out.test.toml +++ /dev/null @@ -1,3 +0,0 @@ -Local = true -Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/telemetry/deploy-select/output.txt b/acceptance/bundle/telemetry/deploy-select/output.txt deleted file mode 100644 index 13d91fa10d4..00000000000 --- a/acceptance/bundle/telemetry/deploy-select/output.txt +++ /dev/null @@ -1,12 +0,0 @@ - ->>> [CLI] bundle deploy --select my_job -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/telemetry-select/default/files... -Deploying resources... -Updating deployment state... -Deployment complete! - ->>> cat out.requests.txt -{ - "key": "select_used", - "value": true -} diff --git a/acceptance/bundle/telemetry/deploy-select/script b/acceptance/bundle/telemetry/deploy-select/script deleted file mode 100644 index c2ea2c767a7..00000000000 --- a/acceptance/bundle/telemetry/deploy-select/script +++ /dev/null @@ -1,5 +0,0 @@ -trace $CLI bundle deploy --select my_job - -trace cat out.requests.txt | jq 'select(has("path") and .path == "/telemetry-ext") | .body.protoLogs[] | fromjson | .entry.databricks_cli_log.bundle_deploy_event.experimental.bool_values[] | select(.key == "select_used")' - -rm out.requests.txt diff --git a/acceptance/bundle/telemetry/deploy-select/test.toml b/acceptance/bundle/telemetry/deploy-select/test.toml deleted file mode 100644 index 9707d39a124..00000000000 --- a/acceptance/bundle/telemetry/deploy-select/test.toml +++ /dev/null @@ -1,2 +0,0 @@ -# --select is only supported by the direct engine. -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]