Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* Add `ipykernel` to the `default` template to enable Databricks Connect notebooks in Cursor/VS Code ([#4164](https://github.com/databricks/cli/pull/4164))
* Add interactive SQL warehouse picker to `default-sql` and `dbt-sql` bundle templates ([#4170](https://github.com/databricks/cli/pull/4170))
* Add `name`, `target` and `mode` fields to the deployment metadata file ([#4180](https://github.com/databricks/cli/pull/4180))
* Add Genie Space resource support for direct deploy mode ([#4191](https://github.com/databricks/cli/pull/4191))

### Dependency updates

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#
# Acceptance test for deploying genie spaces
#
bundle:
name: deploy-genie-space-test-$UNIQUE_NAME

resources:
genie_spaces:
genie1:
title: $GENIE_TITLE
warehouse_id: $TEST_DEFAULT_WAREHOUSE_ID
parent_path: /Users/$CURRENT_USER_NAME
serialized_space: '{"version":1,"config":{"sample_questions":[{"id":"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4","question":["What is the total count?"]}]},"data_sources":{"tables":[]}}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"plan_version": 1,
"cli_version": "[DEV_VERSION]",
"lineage": "[UUID]",
"serial": 1,
"plan": {
"resources.genie_spaces.genie1": {
"action": "recreate",
"new_state": {
"value": {
"parent_path": "/Workspace/Users/[USERNAME]",
"serialized_space": "{\"version\":1,\"config\":{\"sample_questions\":[{\"id\":\"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4\",\"question\":[\"What is the total count?\"]}]},\"data_sources\":{\"tables\":[]}}",
"title": "test bundle-deploy-genie-space [UUID]",
"warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]"
}
},
"remote_state": {
"serialized_space": "{\"version\":1,\"config\":{\"sample_questions\":[{\"id\":\"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4\",\"question\":[\"What is the total count?\"]}]},\"data_sources\":{\"tables\":[]}}",
"space_id": "[GENIE_ID]",
"title": "test bundle-deploy-genie-space [UUID]",
"warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]"
},
"changes": {
"remote": {
"parent_path": {
"action": "recreate",
"old": "/Workspace/Users/[USERNAME]"
}
}
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions acceptance/bundle/resources/genie_spaces/simple/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-genie-space-test-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] genie get-space [GENIE_ID]
{
"title": "test bundle-deploy-genie-space [UUID]",
"parent_path": null
}

>>> [CLI] bundle plan -o json

>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete resources.genie_spaces.genie1

All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-genie-space-test-[UNIQUE_NAME]/default

Deleting files...
Destroy complete!
24 changes: 24 additions & 0 deletions acceptance/bundle/resources/genie_spaces/simple/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
GENIE_TITLE="test bundle-deploy-genie-space $(uuid)"
if [ -z "$CLOUD_ENV" ]; then
export TEST_DEFAULT_WAREHOUSE_ID="warehouse-1234"
echo "warehouse-1234:TEST_DEFAULT_WAREHOUSE_ID" >> ACC_REPLS
fi

export GENIE_TITLE
envsubst < databricks.yml.tmpl > databricks.yml

cleanup() {
trace $CLI bundle destroy --auto-approve
}
trap cleanup EXIT

trace $CLI bundle deploy
GENIE_ID=$($CLI bundle summary --output json | jq -r '.resources.genie_spaces.genie1.id')

# Capture the genie space ID as a replacement.
echo "$GENIE_ID:GENIE_ID" >> ACC_REPLS

trace $CLI genie get-space $GENIE_ID | jq '{title, parent_path}'

# Verify that there is no drift right after deploy.
trace $CLI bundle plan -o json > out.plan.direct.json
8 changes: 8 additions & 0 deletions acceptance/bundle/resources/genie_spaces/simple/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Local = true
Cloud = true
RequiresWarehouse = true
RecordRequests = false

Ignore = [
"databricks.yml",
]
12 changes: 12 additions & 0 deletions acceptance/bundle/resources/genie_spaces/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Local = true
Cloud = true
RequiresWarehouse = true

# Genie spaces are direct mode only
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]

[Env]
# MSYS2 automatically converts absolute paths like /Users/$username/$UNIQUE_NAME to
# C:/Program Files/Git/Users/$username/UNIQUE_NAME before passing it to the CLI
# Setting this environment variable prevents that conversion on windows.
MSYS_NO_PATHCONV = "1"
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ var (
permissions.CAN_MANAGE: "CAN_MANAGE",
permissions.CAN_VIEW: "CAN_READ",
},
"genie_spaces": {
// Genie spaces also support CAN_EDIT, but bundle-level permissions
// don't have an equivalent. Users can set CAN_EDIT directly on the resource.
permissions.CAN_MANAGE: "CAN_MANAGE",
permissions.CAN_VIEW: "CAN_VIEW",
permissions.CAN_RUN: "CAN_RUN",
},
"apps": {
permissions.CAN_MANAGE: "CAN_MANAGE",
permissions.CAN_VIEW: "CAN_USE",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import (
// This list exists to ensure that this mutator is updated when new resource is added.
// These resources are there because they use grants, not permissions:
var unsupportedResources = []string{
"volumes",
"schemas",
"database_catalogs",
"quality_monitors",
"registered_models",
"database_catalogs",
"schemas",
"synced_database_tables",
"volumes",
}

func TestApplyBundlePermissions(t *testing.T) {
Expand Down Expand Up @@ -71,6 +71,10 @@ func TestApplyBundlePermissions(t *testing.T) {
"dashboard_1": {},
"dashboard_2": {},
},
GenieSpaces: map[string]*resources.GenieSpace{
"geniespace_1": {},
"geniespace_2": {},
},
Apps: map[string]*resources.App{
"app_1": {},
"app_2": {},
Expand Down Expand Up @@ -132,6 +136,11 @@ func TestApplyBundlePermissions(t *testing.T) {
require.Contains(t, b.Config.Resources.Dashboards["dashboard_1"].Permissions, resources.DashboardPermission{Level: "CAN_MANAGE", UserName: "TestUser"})
require.Contains(t, b.Config.Resources.Dashboards["dashboard_1"].Permissions, resources.DashboardPermission{Level: "CAN_READ", GroupName: "TestGroup"})

require.Len(t, b.Config.Resources.GenieSpaces["geniespace_1"].Permissions, 3)
require.Contains(t, b.Config.Resources.GenieSpaces["geniespace_1"].Permissions, resources.GenieSpacePermission{Level: "CAN_MANAGE", UserName: "TestUser"})
require.Contains(t, b.Config.Resources.GenieSpaces["geniespace_1"].Permissions, resources.GenieSpacePermission{Level: "CAN_VIEW", GroupName: "TestGroup"})
require.Contains(t, b.Config.Resources.GenieSpaces["geniespace_1"].Permissions, resources.GenieSpacePermission{Level: "CAN_RUN", ServicePrincipalName: "TestServicePrincipal"})

require.Len(t, b.Config.Resources.Apps["app_1"].Permissions, 2)
require.Contains(t, b.Config.Resources.Apps["app_1"].Permissions, resources.AppPermission{Level: "CAN_MANAGE", UserName: "TestUser"})
require.Contains(t, b.Config.Resources.Apps["app_1"].Permissions, resources.AppPermission{Level: "CAN_USE", GroupName: "TestGroup"})
Expand Down
8 changes: 8 additions & 0 deletions bundle/config/mutator/resourcemutator/apply_presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,14 @@ func (m *applyPresets) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnos
dashboard.DisplayName = prefix + dashboard.DisplayName
}

// Genie Spaces: Prefix
for _, genieSpace := range r.GenieSpaces {
if genieSpace == nil {
continue
}
genieSpace.Title = prefix + genieSpace.Title
}

// Apps: No presets

// Alerts: Prefix
Expand Down
10 changes: 10 additions & 0 deletions bundle/config/mutator/resourcemutator/apply_target_mode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ func mockBundle(mode config.Mode) *bundle.Bundle {
},
},
},
GenieSpaces: map[string]*resources.GenieSpace{
"geniespace1": {
GenieSpaceConfig: resources.GenieSpaceConfig{
Title: "geniespace1",
},
},
},
Apps: map[string]*resources.App{
"app1": {
App: apps.App{
Expand Down Expand Up @@ -258,6 +265,9 @@ func TestProcessTargetModeDevelopment(t *testing.T) {

// Dashboards
assert.Equal(t, "[dev lennart] dashboard1", b.Config.Resources.Dashboards["dashboard1"].DisplayName)

// Genie Spaces
assert.Equal(t, "[dev lennart] geniespace1", b.Config.Resources.GenieSpaces["geniespace1"].Title)
}

func TestProcessTargetModeDevelopmentTagNormalizationForAws(t *testing.T) {
Expand Down
33 changes: 33 additions & 0 deletions bundle/config/mutator/resourcemutator/genie_space_fixups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package resourcemutator

import (
"context"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/diag"
)

type genieSpaceFixups struct{}

func GenieSpaceFixups() bundle.Mutator {
return &genieSpaceFixups{}
}

func (m *genieSpaceFixups) Name() string {
return "GenieSpaceFixups"
}

// Apply ensures the parent_path has the /Workspace prefix to match what the API returns.
// This prevents persistent recreates when comparing local config vs remote state.
func (m *genieSpaceFixups) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
for _, genieSpace := range b.Config.Resources.GenieSpaces {
if genieSpace == nil {
continue
}

// Reuse the ensureWorkspacePrefix function from dashboard_fixups.go
genieSpace.ParentPath = ensureWorkspacePrefix(genieSpace.ParentPath)
}

return nil
}
6 changes: 6 additions & 0 deletions bundle/config/mutator/resourcemutator/resource_mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func applyInitializeMutators(ctx context.Context, b *bundle.Bundle) {
}{
{"resources.dashboards.*.parent_path", b.Config.Workspace.ResourcePath},
{"resources.dashboards.*.embed_credentials", false},
{"resources.genie_spaces.*.parent_path", b.Config.Workspace.ResourcePath},
{"resources.volumes.*.volume_type", "MANAGED"},

{"resources.alerts.*.parent_path", b.Config.Workspace.ResourcePath},
Expand Down Expand Up @@ -114,6 +115,11 @@ func applyInitializeMutators(ctx context.Context, b *bundle.Bundle) {
// Ensures dashboard parent paths have the required /Workspace prefix
DashboardFixups(),

// Reads (typed): b.Config.Resources.GenieSpaces (checks genie space configurations)
// Updates (typed): b.Config.Resources.GenieSpaces[].ParentPath (ensures /Workspace prefix is present)
// Ensures genie space parent paths have the required /Workspace prefix
GenieSpaceFixups(),

// Reads (typed): b.Config.Permissions (validates permission levels)
// Reads (dynamic): resources.{jobs,pipelines,experiments,models,model_serving_endpoints,dashboards,apps}.*.permissions (reads existing permissions)
// Updates (dynamic): resources.{jobs,pipelines,experiments,models,model_serving_endpoints,dashboards,apps}.*.permissions (adds permissions from bundle-level configuration)
Expand Down
6 changes: 4 additions & 2 deletions bundle/config/mutator/resourcemutator/run_as_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func allResourceTypes(t *testing.T) []string {
"database_catalogs",
"database_instances",
"experiments",
"genie_spaces",
"jobs",
"model_serving_endpoints",
"models",
Expand Down Expand Up @@ -149,11 +150,12 @@ var allowList = []string{
"database_catalogs",
"database_instances",
"synced_database_tables",
"experiments",
"genie_spaces",
"jobs",
"pipelines",
"models",
"pipelines",
"registered_models",
"experiments",
"schemas",
"secret_scopes",
"sql_warehouses",
Expand Down
9 changes: 9 additions & 0 deletions bundle/config/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Resources struct {
Volumes map[string]*resources.Volume `json:"volumes,omitempty"`
Clusters map[string]*resources.Cluster `json:"clusters,omitempty"`
Dashboards map[string]*resources.Dashboard `json:"dashboards,omitempty"`
GenieSpaces map[string]*resources.GenieSpace `json:"genie_spaces,omitempty"`
Apps map[string]*resources.App `json:"apps,omitempty"`
SecretScopes map[string]*resources.SecretScope `json:"secret_scopes,omitempty"`
Alerts map[string]*resources.Alert `json:"alerts,omitempty"`
Expand Down Expand Up @@ -90,6 +91,7 @@ func (r *Resources) AllResources() []ResourceGroup {
collectResourceMap(descriptions["schemas"], r.Schemas),
collectResourceMap(descriptions["clusters"], r.Clusters),
collectResourceMap(descriptions["dashboards"], r.Dashboards),
collectResourceMap(descriptions["genie_spaces"], r.GenieSpaces),
collectResourceMap(descriptions["volumes"], r.Volumes),
collectResourceMap(descriptions["apps"], r.Apps),
collectResourceMap(descriptions["alerts"], r.Alerts),
Expand Down Expand Up @@ -151,6 +153,12 @@ func (r *Resources) FindResourceByConfigKey(key string) (ConfigResource, error)
}
}

for k := range r.GenieSpaces {
if k == key {
found = append(found, r.GenieSpaces[k])
}
}

for k := range r.RegisteredModels {
if k == key {
found = append(found, r.RegisteredModels[k])
Expand Down Expand Up @@ -233,6 +241,7 @@ func SupportedResources() map[string]resources.ResourceDescription {
"schemas": (&resources.Schema{}).ResourceDescription(),
"clusters": (&resources.Cluster{}).ResourceDescription(),
"dashboards": (&resources.Dashboard{}).ResourceDescription(),
"genie_spaces": (&resources.GenieSpace{}).ResourceDescription(),
"volumes": (&resources.Volume{}).ResourceDescription(),
"apps": (&resources.App{}).ResourceDescription(),
"secret_scopes": (&resources.SecretScope{}).ResourceDescription(),
Expand Down
Loading