From cb6d9140ab32daaeea200526386e612ce5a1e482 Mon Sep 17 00:00:00 2001 From: "pavlo.kozlov" Date: Mon, 26 Oct 2026 12:16:54 +0000 Subject: [PATCH 1/3] Report workspace_id in bundle debug list-targets `bundle debug list-targets` now reports each target's workspace ID next to its host. Both are normalized the same way authentication normalizes them: a ?o=, ?w= or ?workspace_id= query parameter in the host becomes the workspace ID, an explicit workspace.workspace_id takes precedence, and the query is removed from the host. Co-authored-by: Isaac --- .../bundle/debug/list-targets/databricks.yml | 11 ++++++++++ .../bundle/debug/list-targets/output.txt | 18 ++++++++++++++++ cmd/bundle/debug/list_targets.go | 21 +++++++++++++------ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/acceptance/bundle/debug/list-targets/databricks.yml b/acceptance/bundle/debug/list-targets/databricks.yml index 438256ff729..8194820880c 100644 --- a/acceptance/bundle/debug/list-targets/databricks.yml +++ b/acceptance/bundle/debug/list-targets/databricks.yml @@ -20,6 +20,17 @@ targets: mode: production workspace: host: https://prod.example.com + custom_host_query_id: + workspace: + host: https://custom.test/?o=1234 + custom_host_explicit_id: + workspace: + host: https://custom.test + workspace_id: 5678 + custom_host_explicit_id_over_query: + workspace: + host: https://custom.test/?o=1234 + workspace_id: "5678" uses_terraform: bundle: engine: terraform diff --git a/acceptance/bundle/debug/list-targets/output.txt b/acceptance/bundle/debug/list-targets/output.txt index abf957e8729..a3bf1d27634 100644 --- a/acceptance/bundle/debug/list-targets/output.txt +++ b/acceptance/bundle/debug/list-targets/output.txt @@ -1,5 +1,8 @@ >>> [CLI] bundle debug list-targets +custom_host_explicit_id https://custom.test workspace_id:5678 +custom_host_explicit_id_over_query https://custom.test workspace_id:5678 +custom_host_query_id https://custom.test workspace_id:1234 dev (default) development https://dev.example.com prod production https://prod.example.com staging @@ -10,6 +13,21 @@ with_vars ${var.target_mode} ${var.target_host} >>> [CLI] bundle debug list-targets --output json { "targets": [ + { + "name": "custom_host_explicit_id", + "host": "https://custom.test", + "workspace_id": "5678" + }, + { + "name": "custom_host_explicit_id_over_query", + "host": "https://custom.test", + "workspace_id": "5678" + }, + { + "name": "custom_host_query_id", + "host": "https://custom.test", + "workspace_id": "1234" + }, { "name": "dev", "default": true, diff --git a/cmd/bundle/debug/list_targets.go b/cmd/bundle/debug/list_targets.go index ebd038da11d..5cb9fe3d2f0 100644 --- a/cmd/bundle/debug/list_targets.go +++ b/cmd/bundle/debug/list_targets.go @@ -20,10 +20,11 @@ import ( ) type targetInfo struct { - Name string `json:"name"` - Default bool `json:"default,omitempty"` - Mode config.Mode `json:"mode,omitempty"` - Host string `json:"host,omitempty"` + Name string `json:"name"` + Default bool `json:"default,omitempty"` + Mode config.Mode `json:"mode,omitempty"` + Host string `json:"host,omitempty"` + WorkspaceID string `json:"workspace_id,omitempty"` // Engine is only reported for targets that use the removed Terraform engine. Engine engine.EngineType `json:"engine,omitempty"` @@ -53,7 +54,12 @@ func collectTargets(targets map[string]*config.Target, rootEngine engine.EngineT Mode: t.Mode, } if t.Workspace != nil { - info.Host = t.Workspace.Host + // Normalize a copy so the host and workspace ID match what + // authentication uses, e.g. ?o= in a custom host becomes the ID. + w := *t.Workspace + w.NormalizeHostURL() + info.Host = w.Host + info.WorkspaceID = w.WorkspaceID } targetEngine := rootEngine if t.Bundle != nil && t.Bundle.Engine != engine.EngineNotSet { @@ -73,7 +79,7 @@ func terraformOnly(e engine.EngineType) engine.EngineType { } // NewListTargetsCommand returns a command that lists all bundle targets -// with their name, default, mode, workspace host, and (if terraform) engine fields. +// with their name, default, mode, workspace host and ID, and (if terraform) engine fields. func NewListTargetsCommand() *cobra.Command { cmd := &cobra.Command{ Use: "list-targets", @@ -112,6 +118,9 @@ func NewListTargetsCommand() *cobra.Command { if t.Host != "" { parts = append(parts, t.Host) } + if t.WorkspaceID != "" { + parts = append(parts, "workspace_id:"+t.WorkspaceID) + } if t.Engine != "" { parts = append(parts, "engine:"+string(t.Engine)) } From 479aff050de5b74d9962fbb1bb91ad024dca306b Mon Sep 17 00:00:00 2001 From: "pavlo.kozlov" Date: Mon, 26 Oct 2026 12:41:33 +0000 Subject: [PATCH 2/3] Merge top-level workspace settings in bundle debug list-targets list-targets normalized only the target's own workspace section, so a target inherited neither the top-level host nor the top-level workspace_id. With a top-level workspace_id of 5678 and a target host of https://custom.test/?o=1234, it reported 1234 while authentication uses 5678. Merge the target's workspace over the top-level one, as MergeTargetOverrides does, before normalizing. Co-authored-by: Isaac --- .../databricks.yml | 17 +++++ .../list-targets-root-workspace/out.test.toml | 2 + .../list-targets-root-workspace/output.txt | 33 ++++++++++ .../debug/list-targets-root-workspace/script | 2 + bundle/config/root.go | 22 +++++++ bundle/config/root_test.go | 63 +++++++++++++++++++ cmd/bundle/debug/list_targets.go | 44 +++++++------ 7 files changed, 165 insertions(+), 18 deletions(-) create mode 100644 acceptance/bundle/debug/list-targets-root-workspace/databricks.yml create mode 100644 acceptance/bundle/debug/list-targets-root-workspace/out.test.toml create mode 100644 acceptance/bundle/debug/list-targets-root-workspace/output.txt create mode 100644 acceptance/bundle/debug/list-targets-root-workspace/script diff --git a/acceptance/bundle/debug/list-targets-root-workspace/databricks.yml b/acceptance/bundle/debug/list-targets-root-workspace/databricks.yml new file mode 100644 index 00000000000..8732d647fbd --- /dev/null +++ b/acceptance/bundle/debug/list-targets-root-workspace/databricks.yml @@ -0,0 +1,17 @@ +bundle: + name: test-bundle + +workspace: + host: https://custom.test + workspace_id: "5678" + +targets: + inherited: + default: true + target_query_id: + workspace: + host: https://custom.test/?o=1234 + target_workspace_id: + workspace: + workspace_id: "9012" + null_target: diff --git a/acceptance/bundle/debug/list-targets-root-workspace/out.test.toml b/acceptance/bundle/debug/list-targets-root-workspace/out.test.toml new file mode 100644 index 00000000000..a927a5fbc06 --- /dev/null +++ b/acceptance/bundle/debug/list-targets-root-workspace/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/debug/list-targets-root-workspace/output.txt b/acceptance/bundle/debug/list-targets-root-workspace/output.txt new file mode 100644 index 00000000000..176eb4511b4 --- /dev/null +++ b/acceptance/bundle/debug/list-targets-root-workspace/output.txt @@ -0,0 +1,33 @@ + +>>> [CLI] bundle debug list-targets +inherited (default) https://custom.test workspace_id:5678 +null_target https://custom.test workspace_id:5678 +target_query_id https://custom.test workspace_id:5678 +target_workspace_id https://custom.test workspace_id:9012 + +>>> [CLI] bundle debug list-targets --output json +{ + "targets": [ + { + "name": "inherited", + "default": true, + "host": "https://custom.test", + "workspace_id": "5678" + }, + { + "name": "null_target", + "host": "https://custom.test", + "workspace_id": "5678" + }, + { + "name": "target_query_id", + "host": "https://custom.test", + "workspace_id": "5678" + }, + { + "name": "target_workspace_id", + "host": "https://custom.test", + "workspace_id": "9012" + } + ] +} \ No newline at end of file diff --git a/acceptance/bundle/debug/list-targets-root-workspace/script b/acceptance/bundle/debug/list-targets-root-workspace/script new file mode 100644 index 00000000000..fb699dee0cc --- /dev/null +++ b/acceptance/bundle/debug/list-targets-root-workspace/script @@ -0,0 +1,2 @@ +trace $CLI bundle debug list-targets +trace $CLI bundle debug list-targets --output json diff --git a/bundle/config/root.go b/bundle/config/root.go index a64b742630e..bbff6a7f04e 100644 --- a/bundle/config/root.go +++ b/bundle/config/root.go @@ -326,6 +326,28 @@ func mergeField(rv, ov dyn.Value, name string) (dyn.Value, error) { return dyn.SetByPath(rv, path, out) } +// TargetWorkspace returns the workspace settings of the named target as +// [Root.MergeTargetOverrides] merges them, without modifying the root. +func (r *Root) TargetWorkspace(name string) (Workspace, error) { + target, err := dyn.GetByPath(r.value, dyn.NewPath(dyn.Key("targets"), dyn.Key(name))) + if err != nil { + return Workspace{}, err + } + merged, err := mergeField(r.value, target, "workspace") + if err != nil { + return Workspace{}, fmt.Errorf("failed to merge target=%s field=workspace: %w", name, err) + } + + var w Workspace + // Neither the root nor the target sets a workspace section. + v := merged.Get("workspace") + if !v.IsValid() { + return w, nil + } + err = convert.ToTyped(&w, v) + return w, err +} + func (r *Root) MergeTargetOverrides(name string) error { root := r.value target, err := dyn.GetByPath(root, dyn.NewPath(dyn.Key("targets"), dyn.Key(name))) diff --git a/bundle/config/root_test.go b/bundle/config/root_test.go index f735c44405e..d654af12add 100644 --- a/bundle/config/root_test.go +++ b/bundle/config/root_test.go @@ -252,3 +252,66 @@ func TestIsFullVariableOverrideDef(t *testing.T) { assert.Equal(t, tc.expected, isFullVariableOverrideDef(tc.value), "test case %d", i) } } + +const targetWorkspaceConfig = ` +bundle: + name: test +workspace: + host: https://custom.test + workspace_id: "5678" +targets: + inherited: + host_override: + workspace: + host: https://custom.test/?o=1234 + id_override: + workspace: + workspace_id: "9012" +` + +func TestRootTargetWorkspace(t *testing.T) { + root, diags := LoadFromBytes("databricks.yml", []byte(targetWorkspaceConfig)) + require.Empty(t, diags) + + for _, tc := range []struct { + target string + host string + workspaceID string + }{ + {"inherited", "https://custom.test", "5678"}, + {"host_override", "https://custom.test/?o=1234", "5678"}, + {"id_override", "https://custom.test", "9012"}, + } { + t.Run(tc.target, func(t *testing.T) { + w, err := root.TargetWorkspace(tc.target) + require.NoError(t, err) + assert.Equal(t, tc.host, w.Host) + assert.Equal(t, tc.workspaceID, w.WorkspaceID) + + merged, diags := LoadFromBytes("databricks.yml", []byte(targetWorkspaceConfig)) + require.Empty(t, diags) + require.NoError(t, merged.MergeTargetOverrides(tc.target)) + assert.Equal(t, merged.Workspace, w) + }) + } + + assert.Equal(t, "5678", root.Workspace.WorkspaceID) + assert.Len(t, root.Targets, 3) +} + +func TestRootTargetWorkspaceWithoutWorkspace(t *testing.T) { + root, diags := LoadFromBytes("databricks.yml", []byte("targets:\n dev:\n")) + require.Empty(t, diags) + + w, err := root.TargetWorkspace("dev") + require.NoError(t, err) + assert.Equal(t, Workspace{}, w) +} + +func TestRootTargetWorkspaceUnknownTarget(t *testing.T) { + root, diags := LoadFromBytes("databricks.yml", []byte("targets:\n dev:\n")) + require.Empty(t, diags) + + _, err := root.TargetWorkspace("prod") + assert.Error(t, err) +} diff --git a/cmd/bundle/debug/list_targets.go b/cmd/bundle/debug/list_targets.go index 5cb9fe3d2f0..5f8e8e36975 100644 --- a/cmd/bundle/debug/list_targets.go +++ b/cmd/bundle/debug/list_targets.go @@ -35,32 +35,37 @@ type listTargetsOutput struct { } // collectTargets returns the targets sorted by name. The engine of a target is -// its own bundle.engine if set, otherwise the top-level bundle.engine. -func collectTargets(targets map[string]*config.Target, rootEngine engine.EngineType) []targetInfo { +// its own bundle.engine if set, otherwise the top-level bundle.engine. The host +// and workspace ID come from the target's workspace merged over the top-level +// one, normalized the same way authentication normalizes them. +func collectTargets(cfg *config.Root) ([]targetInfo, error) { + targets := cfg.Targets + rootEngine := cfg.Bundle.Engine names := slices.Sorted(maps.Keys(targets)) result := make([]targetInfo, 0, len(names)) for _, name := range names { + w, err := cfg.TargetWorkspace(name) + if err != nil { + return nil, err + } + w.NormalizeHostURL() + info := targetInfo{ + Name: name, + Host: w.Host, + WorkspaceID: w.WorkspaceID, + } + t := targets[name] // YAML decoding can leave a nil entry in the map when a target is // declared with a null value. Skip rather than dereference and panic. if t == nil { - result = append(result, targetInfo{Name: name, Engine: terraformOnly(rootEngine)}) + info.Engine = terraformOnly(rootEngine) + result = append(result, info) continue } - info := targetInfo{ - Name: name, - Default: t.Default, - Mode: t.Mode, - } - if t.Workspace != nil { - // Normalize a copy so the host and workspace ID match what - // authentication uses, e.g. ?o= in a custom host becomes the ID. - w := *t.Workspace - w.NormalizeHostURL() - info.Host = w.Host - info.WorkspaceID = w.WorkspaceID - } + info.Default = t.Default + info.Mode = t.Mode targetEngine := rootEngine if t.Bundle != nil && t.Bundle.Engine != engine.EngineNotSet { targetEngine = t.Bundle.Engine @@ -68,7 +73,7 @@ func collectTargets(targets map[string]*config.Target, rootEngine engine.EngineT info.Engine = terraformOnly(targetEngine) result = append(result, info) } - return result + return result, nil } func terraformOnly(e engine.EngineType) engine.EngineType { @@ -103,7 +108,10 @@ func NewListTargetsCommand() *cobra.Command { return root.ErrAlreadyPrinted } - targets := collectTargets(b.Config.Targets, b.Config.Bundle.Engine) + targets, err := collectTargets(&b.Config) + if err != nil { + return err + } switch root.OutputType(cmd) { case flags.OutputText: From 145a7703c8844dc8d06d1ce1637481f924362ea6 Mon Sep 17 00:00:00 2001 From: "pavlo.kozlov" Date: Mon, 26 Oct 2026 13:03:47 +0000 Subject: [PATCH 3/3] Replace TargetWorkspace unit tests with an acceptance test The value cases were already covered by the list-targets acceptance tests. Replace the check against MergeTargetOverrides with an acceptance test that compares the workspace ID list-targets reports with the one authentication uses for the same target. Co-authored-by: Isaac --- .../list_targets/databricks.yml | 14 +++++ .../list_targets/out.test.toml | 2 + .../list_targets/output.txt | 16 +++++ .../strict_profile_match/list_targets/script | 7 +++ bundle/config/root_test.go | 63 ------------------- 5 files changed, 39 insertions(+), 63 deletions(-) create mode 100644 acceptance/bundle/strict_profile_match/list_targets/databricks.yml create mode 100644 acceptance/bundle/strict_profile_match/list_targets/out.test.toml create mode 100644 acceptance/bundle/strict_profile_match/list_targets/output.txt create mode 100644 acceptance/bundle/strict_profile_match/list_targets/script diff --git a/acceptance/bundle/strict_profile_match/list_targets/databricks.yml b/acceptance/bundle/strict_profile_match/list_targets/databricks.yml new file mode 100644 index 00000000000..2d395eca457 --- /dev/null +++ b/acceptance/bundle/strict_profile_match/list_targets/databricks.yml @@ -0,0 +1,14 @@ +bundle: + name: strict-profile-match + +include: + - scripts.yml + +workspace: + workspace_id: "$WORKSPACE_ID" + +targets: + inherited_workspace_id: + default: true + workspace: + host: $CUSTOM_HOST/?o=2222222222 diff --git a/acceptance/bundle/strict_profile_match/list_targets/out.test.toml b/acceptance/bundle/strict_profile_match/list_targets/out.test.toml new file mode 100644 index 00000000000..a927a5fbc06 --- /dev/null +++ b/acceptance/bundle/strict_profile_match/list_targets/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/strict_profile_match/list_targets/output.txt b/acceptance/bundle/strict_profile_match/list_targets/output.txt new file mode 100644 index 00000000000..03af2d917f0 --- /dev/null +++ b/acceptance/bundle/strict_profile_match/list_targets/output.txt @@ -0,0 +1,16 @@ + +=== list-targets reports the inherited workspace ID +>>> [CLI] bundle debug list-targets -o json +{ + "targets": [ + { + "name": "inherited_workspace_id", + "default": true, + "host": "[CUSTOM_HOST]", + "workspace_id": "[WORKSPACE_ID]" + } + ] +} +=== Authentication uses the same workspace ID +>>> errcode [CLI] bundle run -t inherited_workspace_id show_auth +profile=workspace_custom_host host=[CUSTOM_HOST] workspace_id=[WORKSPACE_ID] diff --git a/acceptance/bundle/strict_profile_match/list_targets/script b/acceptance/bundle/strict_profile_match/list_targets/script new file mode 100644 index 00000000000..0dc0164c6dd --- /dev/null +++ b/acceptance/bundle/strict_profile_match/list_targets/script @@ -0,0 +1,7 @@ +# The top-level workspace_id takes precedence over ?o= in the target's host, so +# list-targets must report the workspace ID that authentication uses. +title "list-targets reports the inherited workspace ID" +trace $CLI bundle debug list-targets -o json + +title "Authentication uses the same workspace ID" +trace errcode $CLI bundle run -t inherited_workspace_id show_auth diff --git a/bundle/config/root_test.go b/bundle/config/root_test.go index d654af12add..f735c44405e 100644 --- a/bundle/config/root_test.go +++ b/bundle/config/root_test.go @@ -252,66 +252,3 @@ func TestIsFullVariableOverrideDef(t *testing.T) { assert.Equal(t, tc.expected, isFullVariableOverrideDef(tc.value), "test case %d", i) } } - -const targetWorkspaceConfig = ` -bundle: - name: test -workspace: - host: https://custom.test - workspace_id: "5678" -targets: - inherited: - host_override: - workspace: - host: https://custom.test/?o=1234 - id_override: - workspace: - workspace_id: "9012" -` - -func TestRootTargetWorkspace(t *testing.T) { - root, diags := LoadFromBytes("databricks.yml", []byte(targetWorkspaceConfig)) - require.Empty(t, diags) - - for _, tc := range []struct { - target string - host string - workspaceID string - }{ - {"inherited", "https://custom.test", "5678"}, - {"host_override", "https://custom.test/?o=1234", "5678"}, - {"id_override", "https://custom.test", "9012"}, - } { - t.Run(tc.target, func(t *testing.T) { - w, err := root.TargetWorkspace(tc.target) - require.NoError(t, err) - assert.Equal(t, tc.host, w.Host) - assert.Equal(t, tc.workspaceID, w.WorkspaceID) - - merged, diags := LoadFromBytes("databricks.yml", []byte(targetWorkspaceConfig)) - require.Empty(t, diags) - require.NoError(t, merged.MergeTargetOverrides(tc.target)) - assert.Equal(t, merged.Workspace, w) - }) - } - - assert.Equal(t, "5678", root.Workspace.WorkspaceID) - assert.Len(t, root.Targets, 3) -} - -func TestRootTargetWorkspaceWithoutWorkspace(t *testing.T) { - root, diags := LoadFromBytes("databricks.yml", []byte("targets:\n dev:\n")) - require.Empty(t, diags) - - w, err := root.TargetWorkspace("dev") - require.NoError(t, err) - assert.Equal(t, Workspace{}, w) -} - -func TestRootTargetWorkspaceUnknownTarget(t *testing.T) { - root, diags := LoadFromBytes("databricks.yml", []byte("targets:\n dev:\n")) - require.Empty(t, diags) - - _, err := root.TargetWorkspace("prod") - assert.Error(t, err) -}