Skip to content

Commit 4bdab3c

Browse files
authored
internal: Parse permissions & grants out of terraform state (#3963)
## Changes - Update ExportedResourcesMap to be a flat map resourceKey -> resourceEntry - Extract permissions and grants from terraform state. ## Why Need to extract the state to migrate it in #3847
1 parent 5181680 commit 4bdab3c

File tree

9 files changed

+169
-221
lines changed

9 files changed

+169
-221
lines changed

bundle/deploy/terraform/check_dashboards_modified_remotely.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package terraform
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
"github.com/databricks/cli/bundle"
89
"github.com/databricks/cli/bundle/config/engine"
@@ -30,7 +31,18 @@ func collectDashboardsFromState(ctx context.Context, b *bundle.Bundle, directDep
3031
}
3132

3233
var dashboards []dashboardState
33-
for resourceName, instance := range state["dashboards"] {
34+
for resourceKey, instance := range state {
35+
// Check if this is a dashboard resource key
36+
if !strings.HasPrefix(resourceKey, "resources.dashboards.") {
37+
continue
38+
}
39+
// Extract dashboard name from "resources.dashboards.name"
40+
parts := strings.Split(resourceKey, ".")
41+
if len(parts) != 3 {
42+
continue
43+
}
44+
resourceName := parts[2]
45+
3446
dashboards = append(dashboards, dashboardState{
3547
Name: resourceName,
3648
ID: instance.ID,

bundle/deploy/terraform/check_running_resources.go

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strconv"
77

88
"github.com/databricks/cli/bundle"
9+
"github.com/databricks/cli/bundle/config"
910
"github.com/databricks/cli/libs/diag"
1011
"github.com/databricks/databricks-sdk-go"
1112
"github.com/databricks/databricks-sdk-go/service/jobs"
@@ -53,42 +54,41 @@ func CheckRunningResource() *checkRunningResources {
5354
func checkAnyResourceRunning(ctx context.Context, w *databricks.WorkspaceClient, state ExportedResourcesMap) error {
5455
errs, errCtx := errgroup.WithContext(ctx)
5556

56-
for _, jobAttrs := range state["jobs"] {
57-
id := jobAttrs.ID
57+
for resourceKey, attrs := range state {
58+
id := attrs.ID
5859
if id == "" {
5960
continue
6061
}
6162

62-
errs.Go(func() error {
63-
isRunning, err := IsJobRunning(errCtx, w, id)
64-
// If there's an error retrieving the job, we assume it's not running
65-
if err != nil {
66-
return err
67-
}
68-
if isRunning {
69-
return &ErrResourceIsRunning{resourceType: "job", resourceId: id}
70-
}
71-
return nil
72-
})
73-
}
74-
75-
for _, pipelineAttrs := range state["pipelines"] {
76-
id := pipelineAttrs.ID
77-
if id == "" {
78-
continue
63+
resourceType := config.GetResourceTypeFromKey(resourceKey)
64+
65+
if resourceType == "jobs" {
66+
errs.Go(func() error {
67+
isRunning, err := IsJobRunning(errCtx, w, id)
68+
// If there's an error retrieving the job, we assume it's not running
69+
if err != nil {
70+
return err
71+
}
72+
if isRunning {
73+
return &ErrResourceIsRunning{resourceType: "job", resourceId: id}
74+
}
75+
return nil
76+
})
7977
}
8078

81-
errs.Go(func() error {
82-
isRunning, err := IsPipelineRunning(errCtx, w, id)
83-
// If there's an error retrieving the pipeline, we assume it's not running
84-
if err != nil {
79+
if resourceType == "pipelines" {
80+
errs.Go(func() error {
81+
isRunning, err := IsPipelineRunning(errCtx, w, id)
82+
// If there's an error retrieving the pipeline, we assume it's not running
83+
if err != nil {
84+
return nil
85+
}
86+
if isRunning {
87+
return &ErrResourceIsRunning{resourceType: "pipeline", resourceId: id}
88+
}
8589
return nil
86-
}
87-
if isRunning {
88-
return &ErrResourceIsRunning{resourceType: "pipeline", resourceId: id}
89-
}
90-
return nil
91-
})
90+
})
91+
}
9292
}
9393

9494
return errs.Wait()

bundle/deploy/terraform/check_running_resources_test.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ func TestIsAnyResourceRunningWithEmptyState(t *testing.T) {
2121
func TestIsAnyResourceRunningWithJob(t *testing.T) {
2222
m := mocks.NewMockWorkspaceClient(t)
2323
resources := ExportedResourcesMap{
24-
"jobs": map[string]ResourceState{
25-
"job1": {ID: "123"},
26-
},
24+
"resources.jobs.job1": {ID: "123"},
2725
}
2826

2927
jobsApi := m.GetMockJobsAPI()
@@ -49,9 +47,7 @@ func TestIsAnyResourceRunningWithJob(t *testing.T) {
4947
func TestIsAnyResourceRunningWithPipeline(t *testing.T) {
5048
m := mocks.NewMockWorkspaceClient(t)
5149
resources := ExportedResourcesMap{
52-
"pipelines": map[string]ResourceState{
53-
"pipeline1": {ID: "123"},
54-
},
50+
"resources.pipelines.pipeline1": {ID: "123"},
5551
}
5652

5753
pipelineApi := m.GetMockPipelinesAPI()
@@ -79,9 +75,7 @@ func TestIsAnyResourceRunningWithAPIFailure(t *testing.T) {
7975
m := mocks.NewMockWorkspaceClient(t)
8076

8177
resources := ExportedResourcesMap{
82-
"pipelines": map[string]ResourceState{
83-
"pipeline1": {ID: "123"},
84-
},
78+
"resources.pipelines.pipeline1": {ID: "123"},
8579
}
8680

8781
pipelineApi := m.GetMockPipelinesAPI()

bundle/deploy/terraform/util.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ type stateInstanceAttributes struct {
4949
ETag string `json:"etag,omitempty"`
5050
}
5151

52-
// Returns a mapping group -> name -> stateInstanceAttributes
52+
// Returns a mapping resourceKey -> stateInstanceAttributes
5353
func parseResourcesState(ctx context.Context, path string) (ExportedResourcesMap, error) {
5454
rawState, err := os.ReadFile(path)
5555
if err != nil {
@@ -76,34 +76,48 @@ func parseResourcesState(ctx context.Context, path string) (ExportedResourcesMap
7676
}
7777
for _, instance := range resource.Instances {
7878
groupName, ok := TerraformToGroupName[resource.Type]
79+
7980
if !ok {
80-
// permissions, grants, secret_acls
81+
// secret_acls
8182
continue
8283
}
8384

84-
group, present := result[groupName]
85-
if !present {
86-
group = make(map[string]ResourceState)
87-
result[groupName] = group
88-
}
85+
var resourceKey string
86+
var resourceState ResourceState
8987

9088
switch groupName {
9189
case "apps":
92-
group[resource.Name] = ResourceState{ID: instance.Attributes.Name}
90+
resourceKey = "resources." + groupName + "." + resource.Name
91+
resourceState = ResourceState{ID: instance.Attributes.Name}
9392
case "secret_scopes":
94-
group[resource.Name] = ResourceState{ID: instance.Attributes.Name}
93+
resourceKey = "resources." + groupName + "." + resource.Name
94+
resourceState = ResourceState{ID: instance.Attributes.Name}
9595
case "dashboards":
96-
group[resource.Name] = ResourceState{ID: instance.Attributes.ID, ETag: instance.Attributes.ETag}
96+
resourceKey = "resources." + groupName + "." + resource.Name
97+
resourceState = ResourceState{ID: instance.Attributes.ID, ETag: instance.Attributes.ETag}
98+
case "permissions":
99+
resourceKey = convertPermissionsResourceNameToKey(resource.Name)
100+
resourceState = ResourceState{ID: instance.Attributes.ID}
101+
case "grants":
102+
resourceKey = convertGrantsResourceNameToKey(resource.Name)
103+
resourceState = ResourceState{ID: instance.Attributes.ID}
97104
default:
98-
group[resource.Name] = ResourceState{ID: instance.Attributes.ID}
105+
resourceKey = "resources." + groupName + "." + resource.Name
106+
resourceState = ResourceState{ID: instance.Attributes.ID}
99107
}
108+
109+
if resourceKey == "" {
110+
return nil, fmt.Errorf("cannot calculate resource key for type=%q name=%q id=%q", resource.Type, resource.Name, instance.Attributes.ID)
111+
}
112+
113+
result[resourceKey] = resourceState
100114
}
101115
}
102116

103117
return result, nil
104118
}
105119

106-
// Returns a mapping group -> name -> stateInstanceAttributes
120+
// Returns a mapping resourceKey -> stateInstanceAttributes
107121
func ParseResourcesState(ctx context.Context, b *bundle.Bundle) (ExportedResourcesMap, error) {
108122
cacheDir, err := Dir(ctx, b)
109123
if err != nil {

bundle/deploy/terraform/util_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,7 @@ func TestParseResourcesStateWithExistingStateFile(t *testing.T) {
9191
state, err := parseResourcesState(ctx, localPath)
9292
assert.NoError(t, err)
9393
expected := ExportedResourcesMap{
94-
"pipelines": map[string]ResourceState{
95-
"test_pipeline": {ID: "123"},
96-
},
94+
"resources.pipelines.test_pipeline": {ID: "123"},
9795
}
9896
assert.Equal(t, expected, state)
9997
}

bundle/direct/dstate/state.go

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"encoding/json"
66
"fmt"
77
"os"
8-
"strings"
98
"sync"
109

1110
"github.com/databricks/cli/bundle/config/resources"
@@ -30,22 +29,6 @@ type ResourceEntry struct {
3029
State any `json:"state"`
3130
}
3231

33-
// splitKey extracts group and name from the key: 'resources.jobs.foo' -> 'jobs', 'foo', true
34-
// For sub-resources like permissions it returns "", "", false
35-
// Note we don't use group/name anywhere in bundle/direct, this is only for ExportState
36-
// which makes ID available to other parts of DABs
37-
func splitKey(key string) (group, name string, ok bool) {
38-
items := strings.Split(key, ".")
39-
if len(items) != 3 {
40-
// e.g. resources.jobs.foo.permissions
41-
return "", "", false
42-
}
43-
if items[0] != "resources" {
44-
return "", "", false
45-
}
46-
return items[1], items[2], true
47-
}
48-
4932
func (db *DeploymentState) SaveState(key, newID string, state any) error {
5033
db.AssertOpened()
5134
db.mu.Lock()
@@ -140,15 +123,6 @@ func (db *DeploymentState) AssertOpened() {
140123
func (db *DeploymentState) ExportState(ctx context.Context) resourcestate.ExportedResourcesMap {
141124
result := make(resourcestate.ExportedResourcesMap)
142125
for key, entry := range db.Data.State {
143-
groupName, resourceName, ok := splitKey(key)
144-
if !ok {
145-
continue
146-
}
147-
resultGroup, ok := result[groupName]
148-
if !ok {
149-
resultGroup = make(map[string]resourcestate.ResourceState)
150-
result[groupName] = resultGroup
151-
}
152126
// Extract etag for dashboards.
153127
var etag string
154128
switch dashboard := entry.State.(type) {
@@ -166,7 +140,7 @@ func (db *DeploymentState) ExportState(ctx context.Context) resourcestate.Export
166140
etag = dashboard.Etag
167141
}
168142

169-
resultGroup[resourceName] = resourcestate.ResourceState{
143+
result[key] = resourcestate.ResourceState{
170144
ID: entry.ID,
171145
ETag: etag,
172146
}

bundle/statemgmt/resourcestate/resourcestate.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ type ResourceState struct {
1010
}
1111

1212
// ExportedResourcesMap stores relevant attributes from terraform/direct state for all resources
13-
// Maps group (e.g. "jobs") -> resource name -> ResourceState
14-
type ExportedResourcesMap map[string]map[string]ResourceState
13+
// Maps resource key (e.g. "resources.jobs.foo", "resources.jobs.foo.permissions") -> ResourceState
14+
type ExportedResourcesMap map[string]ResourceState

bundle/statemgmt/state_load.go

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"slices"
8+
"strings"
89

910
"github.com/databricks/cli/bundle"
1011
"github.com/databricks/cli/bundle/config"
@@ -63,8 +64,19 @@ func (l *load) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
6364
}
6465

6566
// Merge dashboard etags into configuration.
66-
for k, dstate := range state["dashboards"] {
67-
dconfig, ok := b.Config.Resources.Dashboards[k]
67+
for resourceKey, dstate := range state {
68+
// Check if this is a dashboard resource key
69+
if !strings.HasPrefix(resourceKey, "resources.dashboards.") {
70+
continue
71+
}
72+
// Extract dashboard name from "resources.dashboards.name"
73+
parts := strings.Split(resourceKey, ".")
74+
if len(parts) != 3 {
75+
continue
76+
}
77+
dashboardName := parts[2]
78+
79+
dconfig, ok := b.Config.Resources.Dashboards[dashboardName]
6880

6981
// Case: A dashboard is defined in state but not in configuration.
7082
// In this case the dashboard has been deleted and we do not need to load the etag.
@@ -98,31 +110,43 @@ func StateToBundle(ctx context.Context, state ExportedResourcesMap, config *conf
98110
return v, err
99111
}
100112

101-
for groupName, group := range state {
113+
for resourceKey, attrs := range state {
114+
// Parse resource key like "resources.jobs.foo" or "resources.jobs.foo.permissions"
115+
parts := strings.Split(resourceKey, ".")
116+
if len(parts) < 3 || parts[0] != "resources" {
117+
continue // Skip invalid resource keys
118+
}
119+
120+
groupName := parts[1]
121+
resourceName := parts[2]
122+
123+
// Skip permissions for now as they are sub-resources
124+
if len(parts) > 3 {
125+
continue
126+
}
127+
102128
var err error
103129
v, err = ensureMap(v, dyn.Path{dyn.Key("resources"), dyn.Key(groupName)})
104130
if err != nil {
105131
return v, err
106132
}
107133

108-
for resourceName, attrs := range group {
109-
path := dyn.Path{dyn.Key("resources"), dyn.Key(groupName), dyn.Key(resourceName)}
110-
resource, err := dyn.GetByPath(v, path)
111-
if !resource.IsValid() {
112-
m := dyn.NewMapping()
113-
m.SetLoc("id", nil, dyn.V(attrs.ID))
114-
m.SetLoc("modified_status", nil, dyn.V(resources.ModifiedStatusDeleted))
115-
v, err = dyn.SetByPath(v, path, dyn.V(m))
116-
if err != nil {
117-
return dyn.InvalidValue, err
118-
}
119-
} else if err != nil {
134+
path := dyn.Path{dyn.Key("resources"), dyn.Key(groupName), dyn.Key(resourceName)}
135+
resource, err := dyn.GetByPath(v, path)
136+
if !resource.IsValid() {
137+
m := dyn.NewMapping()
138+
m.SetLoc("id", nil, dyn.V(attrs.ID))
139+
m.SetLoc("modified_status", nil, dyn.V(resources.ModifiedStatusDeleted))
140+
v, err = dyn.SetByPath(v, path, dyn.V(m))
141+
if err != nil {
142+
return dyn.InvalidValue, err
143+
}
144+
} else if err != nil {
145+
return dyn.InvalidValue, err
146+
} else {
147+
v, err = dyn.SetByPath(v, dyn.Path{dyn.Key("resources"), dyn.Key(groupName), dyn.Key(resourceName), dyn.Key("id")}, dyn.V(attrs.ID))
148+
if err != nil {
120149
return dyn.InvalidValue, err
121-
} else {
122-
v, err = dyn.SetByPath(v, dyn.Path{dyn.Key("resources"), dyn.Key(groupName), dyn.Key(resourceName), dyn.Key("id")}, dyn.V(attrs.ID))
123-
if err != nil {
124-
return dyn.InvalidValue, err
125-
}
126150
}
127151
}
128152
}

0 commit comments

Comments
 (0)