diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index a1cc837626c..0fd8c1fb76f 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -5,6 +5,7 @@ ### Notable Changes ### CLI +* `experimental open` now opens every DABs resource type that has a workspace URL, picking up `catalogs`, `schemas`, `volumes`, `database_instances`, `database_catalogs`, `synced_database_tables`, `postgres_catalogs`, `postgres_synced_tables`, `quality_monitors`, `vector_search_endpoints`, and `vector_search_indexes` ([#5346](https://github.com/databricks/cli/pull/5346)). ### Bundles * The error reported when a direct-only resource (catalogs, external locations, vector search endpoints) is used with the terraform engine now also suggests setting `bundle.engine: direct` in `databricks.yml`, in addition to the `DATABRICKS_BUNDLE_ENGINE` environment variable ([#5295](https://github.com/databricks/cli/pull/5295)). diff --git a/acceptance/bundle/resources/postgres_synced_tables/basic/output.txt b/acceptance/bundle/resources/postgres_synced_tables/basic/output.txt index d355b3d15f4..87a649d9a8f 100644 --- a/acceptance/bundle/resources/postgres_synced_tables/basic/output.txt +++ b/acceptance/bundle/resources/postgres_synced_tables/basic/output.txt @@ -30,7 +30,7 @@ Resources: Postgres synced tables: my_table: Name: ${resources.postgres_catalogs.my_catalog.catalog_id}.public.trips_synced - URL: [DATABRICKS_URL]/explore/data/$%7Bresources/postgres_catalogs/my_catalog.catalog_id%7D.public.trips_synced + URL: (not deployed) Schemas: pipeline_storage: Name: pipeline_storage_[UNIQUE_NAME] diff --git a/acceptance/experimental/open/output.txt b/acceptance/experimental/open/output.txt index a83e0676fe8..1072a4ff231 100644 --- a/acceptance/experimental/open/output.txt +++ b/acceptance/experimental/open/output.txt @@ -9,7 +9,7 @@ === unknown resource type >>> [CLI] experimental open --url unknown 123 -Error: unknown resource type "unknown", must be one of: alerts, apps, clusters, dashboards, experiments, jobs, model_serving_endpoints, models, notebooks, pipelines, queries, registered_models, warehouses +Error: unknown resource type "unknown", must be one of: alerts, apps, catalogs, clusters, dashboards, database_catalogs, database_instances, experiments, jobs, model_serving_endpoints, models, notebooks, pipelines, postgres_catalogs, postgres_synced_tables, quality_monitors, queries, registered_models, schemas, synced_database_tables, vector_search_endpoints, vector_search_indexes, volumes, warehouses Exit code: 1 @@ -17,16 +17,27 @@ Exit code: 1 >>> [CLI] __complete experimental open , alerts apps +catalogs clusters dashboards +database_catalogs +database_instances experiments jobs model_serving_endpoints models notebooks pipelines +postgres_catalogs +postgres_synced_tables +quality_monitors queries registered_models +schemas +synced_database_tables +vector_search_endpoints +vector_search_indexes +volumes warehouses :4 Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/bundle/config/resources/catalog.go b/bundle/config/resources/catalog.go index d558f127f5c..0bc28f1d99f 100644 --- a/bundle/config/resources/catalog.go +++ b/bundle/config/resources/catalog.go @@ -3,7 +3,6 @@ package resources import ( "context" "net/url" - "strings" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/apierr" @@ -11,6 +10,7 @@ import ( "github.com/databricks/databricks-sdk-go/service/catalog" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/workspaceurls" ) type Catalog struct { @@ -48,8 +48,7 @@ func (c *Catalog) InitializeURL(baseURL url.URL) { if c.ID == "" { return } - baseURL.Path = "explore/data/" + strings.ReplaceAll(c.ID, ".", "/") - c.URL = baseURL.String() + c.URL = workspaceurls.ResourceURL(baseURL, "catalogs", c.ID) } func (c *Catalog) GetURL() string { diff --git a/bundle/config/resources/database_catalog.go b/bundle/config/resources/database_catalog.go index 0de0fa45715..faf9a0247a8 100644 --- a/bundle/config/resources/database_catalog.go +++ b/bundle/config/resources/database_catalog.go @@ -5,6 +5,7 @@ import ( "net/url" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/workspaceurls" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/service/database" @@ -45,6 +46,5 @@ func (d *DatabaseCatalog) InitializeURL(baseURL url.URL) { if d.Name == "" { return } - baseURL.Path = "explore/data/" + d.Name - d.URL = baseURL.String() + d.URL = workspaceurls.ResourceURL(baseURL, "database_catalogs", d.Name) } diff --git a/bundle/config/resources/database_instance.go b/bundle/config/resources/database_instance.go index 2ba19b84f41..41dfc049919 100644 --- a/bundle/config/resources/database_instance.go +++ b/bundle/config/resources/database_instance.go @@ -5,6 +5,7 @@ import ( "net/url" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/workspaceurls" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/service/database" @@ -50,6 +51,5 @@ func (d *DatabaseInstance) InitializeURL(baseURL url.URL) { if d.Name == "" { return } - baseURL.Path = "compute/database-instances/" + d.Name - d.URL = baseURL.String() + d.URL = workspaceurls.ResourceURL(baseURL, "database_instances", d.Name) } diff --git a/bundle/config/resources/postgres_catalog.go b/bundle/config/resources/postgres_catalog.go index c17ffaa1682..63b5c26d246 100644 --- a/bundle/config/resources/postgres_catalog.go +++ b/bundle/config/resources/postgres_catalog.go @@ -5,6 +5,7 @@ import ( "net/url" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/workspaceurls" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/marshal" "github.com/databricks/databricks-sdk-go/service/postgres" @@ -61,6 +62,5 @@ func (c *PostgresCatalog) InitializeURL(baseURL url.URL) { if c.CatalogId == "" { return } - baseURL.Path = "explore/data/" + c.CatalogId - c.URL = baseURL.String() + c.URL = workspaceurls.ResourceURL(baseURL, "postgres_catalogs", c.CatalogId) } diff --git a/bundle/config/resources/postgres_synced_table.go b/bundle/config/resources/postgres_synced_table.go index d4c665f6e3c..be2d533416c 100644 --- a/bundle/config/resources/postgres_synced_table.go +++ b/bundle/config/resources/postgres_synced_table.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/workspaceurls" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/marshal" "github.com/databricks/databricks-sdk-go/service/postgres" @@ -68,15 +69,12 @@ func (s *PostgresSyncedTable) GetURL() string { } func (s *PostgresSyncedTable) InitializeURL(baseURL url.URL) { - // UC explore expects /{catalog}/{schema}/{table}, not a single dotted segment. - catalog, rest, ok := strings.Cut(s.GetName(), ".") - if !ok { + // UC explore expects /{catalog}/{schema}/{table}, so bail if the name isn't + // a fully resolved three-part identifier; an unresolved ${...} reference + // would otherwise produce a misleading URL. + name := s.GetName() + if strings.Count(name, ".") != 2 { return } - schema, table, ok := strings.Cut(rest, ".") - if !ok { - return - } - baseURL.Path = "explore/data/" + catalog + "/" + schema + "/" + table - s.URL = baseURL.String() + s.URL = workspaceurls.ResourceURL(baseURL, "postgres_synced_tables", name) } diff --git a/bundle/config/resources/quality_monitor.go b/bundle/config/resources/quality_monitor.go index f937a7d6167..e3f39c7c69c 100644 --- a/bundle/config/resources/quality_monitor.go +++ b/bundle/config/resources/quality_monitor.go @@ -3,9 +3,9 @@ package resources import ( "context" "net/url" - "strings" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/workspaceurls" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/marshal" "github.com/databricks/databricks-sdk-go/service/catalog" @@ -54,8 +54,7 @@ func (s *QualityMonitor) InitializeURL(baseURL url.URL) { if s.TableName == "" { return } - baseURL.Path = "explore/data/" + strings.ReplaceAll(s.TableName, ".", "/") - s.URL = baseURL.String() + s.URL = workspaceurls.ResourceURL(baseURL, "quality_monitors", s.TableName) } func (s *QualityMonitor) GetName() string { diff --git a/bundle/config/resources/schema.go b/bundle/config/resources/schema.go index 6cf47272796..f40a94c3529 100644 --- a/bundle/config/resources/schema.go +++ b/bundle/config/resources/schema.go @@ -3,11 +3,11 @@ package resources import ( "context" "net/url" - "strings" "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/workspaceurls" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/marshal" @@ -50,8 +50,7 @@ func (s *Schema) InitializeURL(baseURL url.URL) { if s.ID == "" { return } - baseURL.Path = "explore/data/" + strings.ReplaceAll(s.ID, ".", "/") - s.URL = baseURL.String() + s.URL = workspaceurls.ResourceURL(baseURL, "schemas", s.ID) } func (s *Schema) GetURL() string { diff --git a/bundle/config/resources/synced_database_table.go b/bundle/config/resources/synced_database_table.go index 5577fe47b08..76ddf3ac95d 100644 --- a/bundle/config/resources/synced_database_table.go +++ b/bundle/config/resources/synced_database_table.go @@ -5,6 +5,7 @@ import ( "net/url" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/workspaceurls" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/service/database" @@ -45,6 +46,5 @@ func (s *SyncedDatabaseTable) InitializeURL(baseURL url.URL) { if s.Name == "" { return } - baseURL.Path = "explore/data/" + s.Name - s.URL = baseURL.String() + s.URL = workspaceurls.ResourceURL(baseURL, "synced_database_tables", s.Name) } diff --git a/bundle/config/resources/vector_search_endpoint.go b/bundle/config/resources/vector_search_endpoint.go index 900e91ccc57..5d7ef03ca2d 100644 --- a/bundle/config/resources/vector_search_endpoint.go +++ b/bundle/config/resources/vector_search_endpoint.go @@ -5,6 +5,7 @@ import ( "net/url" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/workspaceurls" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/marshal" @@ -51,8 +52,7 @@ func (e *VectorSearchEndpoint) InitializeURL(baseURL url.URL) { if e.Name == "" { return } - baseURL.Path = "compute/vector-search/" + e.Name - e.URL = baseURL.String() + e.URL = workspaceurls.ResourceURL(baseURL, "vector_search_endpoints", e.Name) } func (e *VectorSearchEndpoint) GetName() string { diff --git a/bundle/config/resources/vector_search_index.go b/bundle/config/resources/vector_search_index.go index 0cddada900b..c2990efac8d 100644 --- a/bundle/config/resources/vector_search_index.go +++ b/bundle/config/resources/vector_search_index.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/workspaceurls" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/marshal" @@ -51,19 +52,13 @@ func (e *VectorSearchIndex) ResourceDescription() ResourceDescription { } func (e *VectorSearchIndex) InitializeURL(baseURL url.URL) { - if e.Name == "" { + // UC explore expects /{catalog}/{schema}/{name}, so bail if the name isn't + // a fully resolved three-part identifier; an unresolved ${...} reference + // would otherwise produce a misleading URL. + if strings.Count(e.Name, ".") != 2 { return } - catalog, rest, ok := strings.Cut(e.Name, ".") - if !ok { - return - } - schema, name, ok := strings.Cut(rest, ".") - if !ok { - return - } - baseURL.Path = "explore/data/" + catalog + "/" + schema + "/" + name - e.URL = baseURL.String() + e.URL = workspaceurls.ResourceURL(baseURL, "vector_search_indexes", e.Name) } func (e *VectorSearchIndex) GetName() string { diff --git a/bundle/config/resources/volume.go b/bundle/config/resources/volume.go index 112f5020665..4b1faecc9c0 100644 --- a/bundle/config/resources/volume.go +++ b/bundle/config/resources/volume.go @@ -3,11 +3,11 @@ package resources import ( "context" "net/url" - "strings" "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/workspaceurls" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/marshal" "github.com/databricks/databricks-sdk-go/service/catalog" @@ -59,8 +59,7 @@ func (v *Volume) InitializeURL(baseURL url.URL) { if v.ID == "" { return } - baseURL.Path = "explore/data/volumes/" + strings.ReplaceAll(v.ID, ".", "/") - v.URL = baseURL.String() + v.URL = workspaceurls.ResourceURL(baseURL, "volumes", v.ID) } func (v *Volume) GetURL() string { diff --git a/bundle/config/resources_test.go b/bundle/config/resources_test.go index eb64f5c8474..4f51476536f 100644 --- a/bundle/config/resources_test.go +++ b/bundle/config/resources_test.go @@ -123,15 +123,26 @@ func TestBundleResourcePluralNamesResolveInWorkspaceURLs(t *testing.T) { withURLs := []string{ "alerts", "apps", + "catalogs", "clusters", "dashboards", + "database_catalogs", + "database_instances", "experiments", "jobs", "models", "model_serving_endpoints", "pipelines", + "postgres_catalogs", + "postgres_synced_tables", + "quality_monitors", "registered_models", + "schemas", "sql_warehouses", + "synced_database_tables", + "vector_search_endpoints", + "vector_search_indexes", + "volumes", } supported := SupportedResources() diff --git a/cmd/experimental/workspace_open_test.go b/cmd/experimental/workspace_open_test.go index 3550b9c67da..9f170a601aa 100644 --- a/cmd/experimental/workspace_open_test.go +++ b/cmd/experimental/workspace_open_test.go @@ -67,7 +67,7 @@ func TestBuildWorkspaceURLFragmentBasedResources(t *testing.T) { func TestBuildWorkspaceURLUnknownResourceType(t *testing.T) { _, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com", "unknown", "123", 0) assert.ErrorContains(t, err, "unknown resource type \"unknown\"") - assert.ErrorContains(t, err, "alerts, apps, clusters, dashboards, experiments, jobs, model_serving_endpoints, models, notebooks, pipelines, queries, registered_models, warehouses") + assert.ErrorContains(t, err, "alerts, apps, catalogs, clusters, dashboards, database_catalogs, database_instances, experiments, jobs, model_serving_endpoints, models, notebooks, pipelines, postgres_catalogs, postgres_synced_tables, quality_monitors, queries, registered_models, schemas, synced_database_tables, vector_search_endpoints, vector_search_indexes, volumes, warehouses") } func TestBuildWorkspaceURLHostWithTrailingSlash(t *testing.T) { @@ -106,20 +106,32 @@ func TestWorkspaceOpenCommandCompletion(t *testing.T) { completions, directive := cmd.ValidArgsFunction(cmd, []string{}, "") assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) - assert.Contains(t, completions, "alerts") - assert.Contains(t, completions, "apps") - assert.Contains(t, completions, "clusters") - assert.Contains(t, completions, "dashboards") - assert.Contains(t, completions, "experiments") - assert.Contains(t, completions, "jobs") - assert.Contains(t, completions, "models") - assert.Contains(t, completions, "model_serving_endpoints") - assert.Contains(t, completions, "notebooks") - assert.Contains(t, completions, "pipelines") - assert.Contains(t, completions, "queries") - assert.Contains(t, completions, "registered_models") - assert.Contains(t, completions, "warehouses") - assert.Len(t, completions, 13) + assert.Equal(t, []string{ + "alerts", + "apps", + "catalogs", + "clusters", + "dashboards", + "database_catalogs", + "database_instances", + "experiments", + "jobs", + "model_serving_endpoints", + "models", + "notebooks", + "pipelines", + "postgres_catalogs", + "postgres_synced_tables", + "quality_monitors", + "queries", + "registered_models", + "schemas", + "synced_database_tables", + "vector_search_endpoints", + "vector_search_indexes", + "volumes", + "warehouses", + }, completions) } func TestWorkspaceOpenCommandCompletionSecondArg(t *testing.T) { @@ -133,7 +145,7 @@ func TestWorkspaceOpenCommandCompletionSecondArg(t *testing.T) { func TestWorkspaceOpenCommandHelpText(t *testing.T) { cmd := newWorkspaceOpenCommand() - assert.Contains(t, cmd.Long, "Supported resource types: alerts, apps, clusters, dashboards, experiments, jobs, model_serving_endpoints, models, notebooks, pipelines, queries, registered_models, warehouses.") + assert.Contains(t, cmd.Long, "Supported resource types: alerts, apps, catalogs, clusters, dashboards, database_catalogs, database_instances, experiments, jobs, model_serving_endpoints, models, notebooks, pipelines, postgres_catalogs, postgres_synced_tables, quality_monitors, queries, registered_models, schemas, synced_database_tables, vector_search_endpoints, vector_search_indexes, volumes, warehouses.") assert.Contains(t, cmd.Long, "databricks experimental open jobs 123456789") assert.Contains(t, cmd.Long, "databricks experimental open notebooks /Users/user@example.com/my-notebook") assert.Contains(t, cmd.Long, "databricks experimental open registered_models catalog.schema.my_model") diff --git a/libs/workspaceurls/urls.go b/libs/workspaceurls/urls.go index c6ac8fdd7cd..1a8984d2990 100644 --- a/libs/workspaceurls/urls.go +++ b/libs/workspaceurls/urls.go @@ -11,16 +11,27 @@ import ( var resourceURLPatterns = map[string]string{ "alerts": "sql/alerts-v2/%s", "apps": "apps/%s", + "catalogs": "explore/data/%s", "clusters": "compute/clusters/%s", "dashboards": "dashboardsv3/%s/published", + "database_catalogs": "explore/data/%s", + "database_instances": "compute/database-instances/%s", "experiments": "ml/experiments/%s", "jobs": "jobs/%s", "models": "ml/models/%s", "model_serving_endpoints": "ml/endpoints/%s", "notebooks": "#notebook/%s", "pipelines": "pipelines/%s", + "postgres_catalogs": "explore/data/%s", + "postgres_synced_tables": "explore/data/%s", + "quality_monitors": "explore/data/%s", "queries": "sql/editor/%s", "registered_models": "explore/data/models/%s", + "schemas": "explore/data/%s", + "synced_database_tables": "explore/data/%s", + "vector_search_endpoints": "compute/vector-search/%s", + "vector_search_indexes": "explore/data/%s", + "volumes": "explore/data/volumes/%s", "warehouses": "sql/warehouses/%s", } @@ -37,7 +48,13 @@ var resourceAliases = map[string]string{ // provided as a dot-separated name (e.g. "catalog.schema.model") but the URL // requires slash-separated segments. var dotSeparatedResources = map[string]bool{ - "registered_models": true, + "catalogs": true, + "postgres_synced_tables": true, + "quality_monitors": true, + "registered_models": true, + "schemas": true, + "vector_search_indexes": true, + "volumes": true, } // ResourceTypes returns a sorted list of all supported resource type names.