Skip to content

Commit dbb681a

Browse files
Add app_spaces as a DABs resource type (direct mode only)
Adds support for declaring Databricks App Spaces in bundle YAML via the `app_spaces` resource type. Spaces are containers for apps that provide shared resources, OAuth scopes, and a service principal. Implements direct mode CRUD with async operation waiting, a merge mutator for the space resources array, run_as validation, and test server handlers for the fake workspace. Co-authored-by: Isaac
1 parent 6f48f8b commit dbb681a

16 files changed

Lines changed: 344 additions & 1 deletion

bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
// This list exists to ensure that this mutator is updated when new resource is added.
1919
// These resources are there because they use grants, not permissions:
2020
var unsupportedResources = []string{
21+
"app_spaces",
2122
"catalogs",
2223
"external_locations",
2324
"volumes",

bundle/config/mutator/resourcemutator/apply_target_mode_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ func mockBundle(mode config.Mode) *bundle.Bundle {
161161
},
162162
},
163163
},
164+
AppSpaces: map[string]*resources.AppSpace{
165+
"app_space1": {
166+
Space: apps.Space{
167+
Name: "app-space-1",
168+
},
169+
},
170+
},
164171
SecretScopes: map[string]*resources.SecretScope{
165172
"secretScope1": {
166173
Name: "secretScope1",
@@ -442,7 +449,7 @@ func TestAllNonUcResourcesAreRenamed(t *testing.T) {
442449

443450
// Skip resources that are not renamed (either because they don't have a user-facing Name field,
444451
// or because their Name is server-generated rather than user-specified)
445-
if resourceType == "Apps" || resourceType == "SecretScopes" || resourceType == "DatabaseInstances" || resourceType == "DatabaseCatalogs" || resourceType == "SyncedDatabaseTables" || resourceType == "PostgresProjects" || resourceType == "PostgresBranches" || resourceType == "PostgresEndpoints" {
452+
if resourceType == "Apps" || resourceType == "AppSpaces" || resourceType == "SecretScopes" || resourceType == "DatabaseInstances" || resourceType == "DatabaseCatalogs" || resourceType == "SyncedDatabaseTables" || resourceType == "PostgresProjects" || resourceType == "PostgresBranches" || resourceType == "PostgresEndpoints" {
446453
continue
447454
}
448455

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package resourcemutator
2+
3+
import (
4+
"context"
5+
6+
"github.com/databricks/cli/bundle"
7+
"github.com/databricks/cli/libs/diag"
8+
"github.com/databricks/cli/libs/dyn"
9+
"github.com/databricks/cli/libs/dyn/merge"
10+
)
11+
12+
type mergeAppSpaces struct{}
13+
14+
func MergeAppSpaces() bundle.Mutator {
15+
return &mergeAppSpaces{}
16+
}
17+
18+
func (m *mergeAppSpaces) Name() string {
19+
return "MergeAppSpaces"
20+
}
21+
22+
func (m *mergeAppSpaces) resourceName(v dyn.Value) string {
23+
switch v.Kind() {
24+
case dyn.KindInvalid, dyn.KindNil:
25+
return ""
26+
case dyn.KindString:
27+
return v.MustString()
28+
default:
29+
panic("app space resource name must be a string")
30+
}
31+
}
32+
33+
func (m *mergeAppSpaces) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
34+
err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) {
35+
if v.Kind() == dyn.KindNil {
36+
return v, nil
37+
}
38+
39+
return dyn.Map(v, "resources.app_spaces", dyn.Foreach(func(_ dyn.Path, space dyn.Value) (dyn.Value, error) {
40+
return dyn.Map(space, "resources", merge.ElementsByKeyWithOverride("name", m.resourceName))
41+
}))
42+
})
43+
44+
return diag.FromErr(err)
45+
}

bundle/config/mutator/resourcemutator/resource_mutator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ func applyNormalizeMutators(ctx context.Context, b *bundle.Bundle) {
167167
// Updates (dynamic): resources.apps.*.resources (merges app resources with the same name)
168168
MergeApps(),
169169

170+
// Reads (dynamic): resources.app_spaces.*.resources (reads app space resources to merge)
171+
// Updates (dynamic): resources.app_spaces.*.resources (merges app space resources with the same name)
172+
MergeAppSpaces(),
173+
170174
// Reads (dynamic): resources.{catalogs,schemas,external_locations,volumes,registered_models}.*.grants
171175
// Updates (dynamic): same paths — merges grant entries by principal and deduplicates privileges
172176
MergeGrants(),

bundle/config/mutator/resourcemutator/run_as.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,16 @@ func validateRunAs(b *bundle.Bundle) diag.Diagnostics {
126126
))
127127
}
128128

129+
// App spaces do not support run_as in the API.
130+
if len(b.Config.Resources.AppSpaces) > 0 {
131+
diags = diags.Extend(reportRunAsNotSupported(
132+
"app_spaces",
133+
b.Config.GetLocation("resources.app_spaces"),
134+
b.Config.Workspace.CurrentUser.UserName,
135+
identity,
136+
))
137+
}
138+
129139
return diags
130140
}
131141

bundle/config/mutator/resourcemutator/run_as_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func allResourceTypes(t *testing.T) []string {
3333
// also update this check when adding a new resource
3434
require.Equal(t, []string{
3535
"alerts",
36+
"app_spaces",
3637
"apps",
3738
"catalogs",
3839
"clusters",

bundle/config/resources.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Resources struct {
2626
Clusters map[string]*resources.Cluster `json:"clusters,omitempty"`
2727
Dashboards map[string]*resources.Dashboard `json:"dashboards,omitempty"`
2828
Apps map[string]*resources.App `json:"apps,omitempty"`
29+
AppSpaces map[string]*resources.AppSpace `json:"app_spaces,omitempty"`
2930
SecretScopes map[string]*resources.SecretScope `json:"secret_scopes,omitempty"`
3031
Alerts map[string]*resources.Alert `json:"alerts,omitempty"`
3132
SqlWarehouses map[string]*resources.SqlWarehouse `json:"sql_warehouses,omitempty"`
@@ -103,6 +104,7 @@ func (r *Resources) AllResources() []ResourceGroup {
103104
collectResourceMap(descriptions["dashboards"], r.Dashboards),
104105
collectResourceMap(descriptions["volumes"], r.Volumes),
105106
collectResourceMap(descriptions["apps"], r.Apps),
107+
collectResourceMap(descriptions["app_spaces"], r.AppSpaces),
106108
collectResourceMap(descriptions["alerts"], r.Alerts),
107109
collectResourceMap(descriptions["secret_scopes"], r.SecretScopes),
108110
collectResourceMap(descriptions["sql_warehouses"], r.SqlWarehouses),
@@ -158,6 +160,7 @@ func SupportedResources() map[string]resources.ResourceDescription {
158160
"dashboards": (&resources.Dashboard{}).ResourceDescription(),
159161
"volumes": (&resources.Volume{}).ResourceDescription(),
160162
"apps": (&resources.App{}).ResourceDescription(),
163+
"app_spaces": (&resources.AppSpace{}).ResourceDescription(),
161164
"secret_scopes": (&resources.SecretScope{}).ResourceDescription(),
162165
"alerts": (&resources.Alert{}).ResourceDescription(),
163166
"sql_warehouses": (&resources.SqlWarehouse{}).ResourceDescription(),
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package resources
2+
3+
import (
4+
"context"
5+
"net/url"
6+
7+
"github.com/databricks/cli/libs/log"
8+
"github.com/databricks/databricks-sdk-go"
9+
"github.com/databricks/databricks-sdk-go/marshal"
10+
"github.com/databricks/databricks-sdk-go/service/apps"
11+
)
12+
13+
type AppSpace struct {
14+
BaseResource
15+
apps.Space
16+
}
17+
18+
func (s *AppSpace) UnmarshalJSON(b []byte) error {
19+
return marshal.Unmarshal(b, s)
20+
}
21+
22+
func (s AppSpace) MarshalJSON() ([]byte, error) {
23+
return marshal.Marshal(s)
24+
}
25+
26+
func (s *AppSpace) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) (bool, error) {
27+
_, err := w.Apps.GetSpace(ctx, apps.GetSpaceRequest{Name: id})
28+
if err != nil {
29+
log.Debugf(ctx, "app space %s does not exist", id)
30+
return false, err
31+
}
32+
return true, nil
33+
}
34+
35+
func (*AppSpace) ResourceDescription() ResourceDescription {
36+
return ResourceDescription{
37+
SingularName: "app_space",
38+
PluralName: "app_spaces",
39+
SingularTitle: "App Space",
40+
PluralTitle: "App Spaces",
41+
}
42+
}
43+
44+
func (s *AppSpace) InitializeURL(baseURL url.URL) {
45+
if s.ModifiedStatus == "" || s.ModifiedStatus == ModifiedStatusCreated {
46+
return
47+
}
48+
baseURL.Path = "apps/spaces/" + s.GetName()
49+
s.URL = baseURL.String()
50+
}
51+
52+
func (s *AppSpace) GetName() string {
53+
if s.ID != "" {
54+
return s.ID
55+
}
56+
return s.Name
57+
}
58+
59+
func (s *AppSpace) GetURL() string {
60+
return s.URL
61+
}

bundle/config/resources_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ func TestResourcesBindSupport(t *testing.T) {
205205
App: apps.App{},
206206
},
207207
},
208+
AppSpaces: map[string]*resources.AppSpace{
209+
"my_app_space": {
210+
Space: apps.Space{},
211+
},
212+
},
208213
Alerts: map[string]*resources.Alert{
209214
"my_alert": {
210215
AlertV2: sql.AlertV2{},
@@ -300,6 +305,7 @@ func TestResourcesBindSupport(t *testing.T) {
300305
m.GetMockLakeviewAPI().EXPECT().Get(mock.Anything, mock.Anything).Return(nil, nil)
301306
m.GetMockVolumesAPI().EXPECT().Read(mock.Anything, mock.Anything).Return(nil, nil)
302307
m.GetMockAppsAPI().EXPECT().GetByName(mock.Anything, mock.Anything).Return(nil, nil)
308+
m.GetMockAppsAPI().EXPECT().GetSpace(mock.Anything, mock.Anything).Return(nil, nil)
303309
m.GetMockAlertsV2API().EXPECT().GetAlertById(mock.Anything, mock.Anything).Return(nil, nil)
304310
m.GetMockQualityMonitorsAPI().EXPECT().Get(mock.Anything, mock.Anything).Return(nil, nil)
305311
m.GetMockServingEndpointsAPI().EXPECT().Get(mock.Anything, mock.Anything).Return(nil, nil)

bundle/direct/dresources/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var SupportedResources = map[string]any{
1616
"volumes": (*ResourceVolume)(nil),
1717
"models": (*ResourceMlflowModel)(nil),
1818
"apps": (*ResourceApp)(nil),
19+
"app_spaces": (*ResourceAppSpace)(nil),
1920
"sql_warehouses": (*ResourceSqlWarehouse)(nil),
2021
"database_instances": (*ResourceDatabaseInstance)(nil),
2122
"database_catalogs": (*ResourceDatabaseCatalog)(nil),

0 commit comments

Comments
 (0)