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/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/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.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/cmd/bundle/debug/list_targets.go b/cmd/bundle/debug/list_targets.go index ebd038da11d..5f8e8e36975 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"` @@ -34,27 +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 { - info.Host = t.Workspace.Host - } + info.Default = t.Default + info.Mode = t.Mode targetEngine := rootEngine if t.Bundle != nil && t.Bundle.Engine != engine.EngineNotSet { targetEngine = t.Bundle.Engine @@ -62,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 { @@ -73,7 +84,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", @@ -97,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: @@ -112,6 +126,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)) }