Skip to content

Commit 1c7832c

Browse files
committed
bundle/direct: drop MigrateMode, consolidate state-building in bundle/migrate
MigrateMode was a bool parameter on Apply that forked between "save state without deploying" and "actually deploy". The two MigrateMode(true) callers (bundle deployment migrate and upload_state_for_yaml_sync) now use migrate.BuildStateFromTF directly, reading from the local TF state file without any API calls. Changes: - New bundle/migrate/build_state.go: public BuildStateFromTF extracted from cmd/bundle/migrate.go, taking *config.Root so callers can pass an un-interpolated config (needed by upload_state_for_yaml_sync). - bundle/direct/bundle_apply.go: drop MigrateMode type and parameter; Apply now only handles real deployments. - bundle/phases/{deploy,destroy}.go: drop MigrateMode(false) argument. - upload_state_for_yaml_sync.go: replace CalculatePlan+Apply with BuildStateFromTF; keep reverseInterpolate since config is TF-interpolated. - cmd/bundle/deployment/migrate.go: replace CalculatePlan+Apply with BuildStateFromTF; drop exported RunPlanCheck/GetCommonArgs wrappers. - cmd/bundle/migrate.go: delegate to migrate.BuildStateFromTF. Co-authored-by: Isaac
1 parent bedd5c1 commit 1c7832c

7 files changed

Lines changed: 216 additions & 274 deletions

File tree

bundle/direct/bundle_apply.go

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ import (
1515
"github.com/databricks/databricks-sdk-go"
1616
)
1717

18-
type MigrateMode bool
19-
20-
func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.WorkspaceClient, plan *deployplan.Plan, migrateMode MigrateMode) {
18+
func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.WorkspaceClient, plan *deployplan.Plan) {
2119
if plan == nil {
2220
panic("Planning is not done")
2321
}
@@ -52,9 +50,6 @@ func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.Workspa
5250

5351
action := entry.Action
5452
errorPrefix := fmt.Sprintf("cannot %s %s", action, resourceKey)
55-
if migrateMode {
56-
errorPrefix = "cannot migrate " + resourceKey
57-
}
5853

5954
if action == deployplan.Undefined {
6055
logdiag.LogError(ctx, fmt.Errorf("cannot deploy %s: unknown action %q", resourceKey, action))
@@ -82,10 +77,6 @@ func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.Workspa
8277
}
8378

8479
if action == deployplan.Delete {
85-
if migrateMode {
86-
logdiag.LogError(ctx, fmt.Errorf("%s: Unexpected delete action during migration", errorPrefix))
87-
return false
88-
}
8980
err = d.Destroy(ctx, &b.StateDB)
9081
if err != nil {
9182
logdiag.LogError(ctx, fmt.Errorf("%s: %w", errorPrefix, err))
@@ -113,18 +104,8 @@ func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.Workspa
113104
return false
114105
}
115106

116-
if migrateMode {
117-
// In migration mode we're reading resources in DAG order so that we have fully resolved config snapshots stored
118-
id := b.StateDB.GetResourceID(resourceKey)
119-
if id == "" {
120-
logdiag.LogError(ctx, fmt.Errorf("state entry not found for %q", resourceKey))
121-
return false
122-
}
123-
err = b.StateDB.SaveState(resourceKey, id, sv.Value, entry.DependsOn)
124-
} else {
125-
// TODO: redo calcDiff to downgrade planned action if possible (?)
126-
err = d.Deploy(ctx, &b.StateDB, sv.Value, action, entry)
127-
}
107+
// TODO: redo calcDiff to downgrade planned action if possible (?)
108+
err = d.Deploy(ctx, &b.StateDB, sv.Value, action, entry)
128109

129110
if err != nil {
130111
logdiag.LogError(ctx, fmt.Errorf("%s: %w", errorPrefix, err))

bundle/migrate/build_state.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package migrate
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"maps"
7+
"strings"
8+
9+
"github.com/databricks/cli/bundle/config"
10+
"github.com/databricks/cli/bundle/config/resources"
11+
"github.com/databricks/cli/bundle/deploy/terraform"
12+
"github.com/databricks/cli/bundle/direct"
13+
"github.com/databricks/cli/bundle/direct/dresources"
14+
"github.com/databricks/cli/bundle/direct/dstate"
15+
"github.com/databricks/cli/libs/dyn"
16+
"github.com/databricks/cli/libs/log"
17+
"github.com/databricks/cli/libs/structs/structaccess"
18+
"github.com/databricks/cli/libs/structs/structpath"
19+
"github.com/databricks/cli/libs/structs/structvar"
20+
)
21+
22+
// BuildStateFromTF iterates over bundle resources, resolves cross-resource
23+
// references using TF state attributes, and writes each resource's state entry.
24+
// configRoot should be an un-interpolated config (with ${resources.*} references).
25+
func BuildStateFromTF(
26+
ctx context.Context,
27+
configRoot *config.Root,
28+
adapters map[string]*dresources.Adapter,
29+
stateDB *dstate.DeploymentState,
30+
tfAttrs TFStateAttrs,
31+
tfIDs terraform.ExportedResourcesMap,
32+
etags map[string]string,
33+
) error {
34+
// Collect all resource nodes (same patterns as makePlan).
35+
var nodes []string
36+
patterns := []dyn.Pattern{
37+
dyn.NewPattern(dyn.Key("resources"), dyn.AnyKey(), dyn.AnyKey()),
38+
dyn.NewPattern(dyn.Key("resources"), dyn.AnyKey(), dyn.AnyKey(), dyn.Key("permissions")),
39+
dyn.NewPattern(dyn.Key("resources"), dyn.AnyKey(), dyn.AnyKey(), dyn.Key("grants")),
40+
}
41+
for _, pat := range patterns {
42+
_, err := dyn.MapByPattern(
43+
configRoot.Value(),
44+
pat,
45+
func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
46+
nodes = append(nodes, p.String())
47+
return dyn.InvalidValue, nil
48+
},
49+
)
50+
if err != nil {
51+
return err
52+
}
53+
}
54+
55+
for _, node := range nodes {
56+
idEntry, ok := tfIDs[node]
57+
if !ok {
58+
// Resource is in config but not in TF state (new resource); skip.
59+
continue
60+
}
61+
62+
group := config.GetResourceTypeFromKey(node)
63+
if group == "" {
64+
return fmt.Errorf("cannot determine resource type for %q", node)
65+
}
66+
67+
adapter, ok := adapters[group]
68+
if !ok {
69+
log.Warnf(ctx, "unsupported resource type %q for %s, skipping", group, node)
70+
continue
71+
}
72+
73+
inputConfig, err := configRoot.GetResourceConfig(node)
74+
if err != nil {
75+
return fmt.Errorf("%s: getting config: %w", node, err)
76+
}
77+
78+
baseRefs := map[string]string{}
79+
80+
switch {
81+
case strings.HasSuffix(node, ".permissions"):
82+
var sv *structvar.StructVar
83+
if strings.HasPrefix(node, "resources.secret_scopes.") {
84+
typedConfig, ok := inputConfig.(*[]resources.SecretScopePermission)
85+
if !ok {
86+
return fmt.Errorf("%s: expected *[]resources.SecretScopePermission, got %T", node, inputConfig)
87+
}
88+
sv, err = dresources.PrepareSecretScopeAclsInputConfig(*typedConfig, node)
89+
if err != nil {
90+
return fmt.Errorf("%s: preparing secret scope ACLs config: %w", node, err)
91+
}
92+
} else {
93+
sv, err = dresources.PreparePermissionsInputConfig(inputConfig, node)
94+
if err != nil {
95+
return fmt.Errorf("%s: preparing permissions config: %w", node, err)
96+
}
97+
}
98+
inputConfig = sv.Value
99+
baseRefs = sv.Refs
100+
101+
case strings.HasSuffix(node, ".grants"):
102+
sv, err := dresources.PrepareGrantsInputConfig(inputConfig, node)
103+
if err != nil {
104+
return fmt.Errorf("%s: preparing grants config: %w", node, err)
105+
}
106+
inputConfig = sv.Value
107+
baseRefs = sv.Refs
108+
}
109+
110+
newStateValue, err := adapter.PrepareState(inputConfig)
111+
if err != nil {
112+
return fmt.Errorf("%s: PrepareState: %w", node, err)
113+
}
114+
115+
refs, err := direct.ExtractReferences(configRoot.Value(), node)
116+
if err != nil {
117+
return fmt.Errorf("%s: extracting references: %w", node, err)
118+
}
119+
maps.Copy(refs, baseRefs)
120+
121+
sv := structvar.NewStructVar(newStateValue, refs)
122+
123+
// Resolve each reference using TF state.
124+
// node format: "resources.<group>.<name>" or "resources.<group>.<name>.permissions"
125+
parts := strings.SplitN(node, ".", 4)
126+
var srcGroup, srcName string
127+
if len(parts) >= 3 {
128+
srcGroup = parts[1]
129+
srcName = parts[2]
130+
}
131+
132+
// Collect all field paths that need resolution (avoid modifying map during iteration).
133+
type refEntry struct {
134+
fieldPathStr string
135+
refTemplate string
136+
}
137+
var pendingRefs []refEntry
138+
for fieldPathStr, refTemplate := range sv.Refs {
139+
pendingRefs = append(pendingRefs, refEntry{fieldPathStr, refTemplate})
140+
}
141+
142+
for _, pending := range pendingRefs {
143+
fieldPath, err := structpath.ParsePath(pending.fieldPathStr)
144+
if err != nil {
145+
return fmt.Errorf("%s: parsing field path %q: %w", node, pending.fieldPathStr, err)
146+
}
147+
148+
// ResolveFieldRef returns the fully resolved value for this field,
149+
// using either Method A (TF state lookup) or Method B (template evaluation).
150+
value, err := ResolveFieldRef(ctx, tfAttrs, srcGroup, srcName, fieldPath, pending.refTemplate)
151+
if err != nil {
152+
return fmt.Errorf("%s: cannot resolve field %q (template %q): %w", node, pending.fieldPathStr, pending.refTemplate, err)
153+
}
154+
155+
// Set the resolved value directly and remove the ref entry.
156+
if err := structaccess.Set(sv.Value, fieldPath, value); err != nil {
157+
return fmt.Errorf("%s: cannot set resolved value for field %q: %w", node, pending.fieldPathStr, err)
158+
}
159+
delete(sv.Refs, pending.fieldPathStr)
160+
}
161+
162+
if len(sv.Refs) > 0 {
163+
return fmt.Errorf("%s: unresolved references: %v", node, sv.Refs)
164+
}
165+
166+
// Handle etag for dashboards.
167+
if etag := etags[node]; etag != "" {
168+
if err := structaccess.Set(sv.Value, structpath.NewStringKey(nil, "etag"), etag); err != nil {
169+
return fmt.Errorf("%s: cannot set etag: %w", node, err)
170+
}
171+
}
172+
173+
if err := stateDB.SaveState(node, idEntry.ID, sv.Value, nil); err != nil {
174+
return fmt.Errorf("%s: SaveState: %w", node, err)
175+
}
176+
}
177+
178+
return nil
179+
}

bundle/phases/deploy.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"github.com/databricks/cli/bundle/deploy/metadata"
1515
"github.com/databricks/cli/bundle/deploy/terraform"
1616
"github.com/databricks/cli/bundle/deployplan"
17-
"github.com/databricks/cli/bundle/direct"
1817
"github.com/databricks/cli/bundle/libraries"
1918
"github.com/databricks/cli/bundle/metrics"
2019
"github.com/databricks/cli/bundle/permissions"
@@ -81,7 +80,7 @@ func deployCore(ctx context.Context, b *bundle.Bundle, plan *deployplan.Plan, ta
8180
err error
8281
)
8382
if targetEngine.IsDirect() {
84-
b.DeploymentBundle.Apply(ctx, b.WorkspaceClient(ctx), plan, direct.MigrateMode(false))
83+
b.DeploymentBundle.Apply(ctx, b.WorkspaceClient(ctx), plan)
8584
state, err = b.DeploymentBundle.StateDB.Finalize(ctx)
8685
} else {
8786
bundle.ApplyContext(ctx, b, terraform.Apply())

bundle/phases/destroy.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"github.com/databricks/cli/bundle/deploy/lock"
1313
"github.com/databricks/cli/bundle/deploy/terraform"
1414
"github.com/databricks/cli/bundle/deployplan"
15-
"github.com/databricks/cli/bundle/direct"
1615
"github.com/databricks/cli/libs/cmdio"
1716
"github.com/databricks/cli/libs/diag"
1817
"github.com/databricks/cli/libs/log"
@@ -76,7 +75,7 @@ func approvalForDestroy(ctx context.Context, b *bundle.Bundle, plan *deployplan.
7675

7776
func destroyCore(ctx context.Context, b *bundle.Bundle, plan *deployplan.Plan, engine engine.EngineType) {
7877
if engine.IsDirect() {
79-
b.DeploymentBundle.Apply(ctx, b.WorkspaceClient(ctx), plan, direct.MigrateMode(false))
78+
b.DeploymentBundle.Apply(ctx, b.WorkspaceClient(ctx), plan)
8079
} else {
8180
// Core destructive mutators for destroy. These require informed user consent.
8281
bundle.ApplyContext(ctx, b, terraform.Apply())

bundle/statemgmt/upload_state_for_yaml_sync.go

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,16 @@ import (
1414
"github.com/databricks/cli/bundle/config/mutator/resourcemutator"
1515
"github.com/databricks/cli/bundle/deploy"
1616
"github.com/databricks/cli/bundle/deploy/terraform"
17-
"github.com/databricks/cli/bundle/deployplan"
18-
"github.com/databricks/cli/bundle/direct"
17+
"github.com/databricks/cli/bundle/direct/dresources"
1918
"github.com/databricks/cli/bundle/direct/dstate"
2019
"github.com/databricks/cli/bundle/env"
20+
"github.com/databricks/cli/bundle/migrate"
2121
"github.com/databricks/cli/libs/diag"
2222
"github.com/databricks/cli/libs/dyn"
2323
"github.com/databricks/cli/libs/dyn/dynvar"
2424
"github.com/databricks/cli/libs/filer"
2525
"github.com/databricks/cli/libs/log"
2626
"github.com/databricks/cli/libs/logdiag"
27-
"github.com/databricks/cli/libs/structs/structaccess"
28-
"github.com/databricks/cli/libs/structs/structpath"
2927
)
3028

3129
type uploadStateForYamlSync struct {
@@ -117,6 +115,11 @@ func (m *uploadStateForYamlSync) convertState(ctx context.Context, b *bundle.Bun
117115
return false, fmt.Errorf("failed to read terraform state: %w", err)
118116
}
119117

118+
tfAttrs, err := migrate.ParseTFStateAttrs(localTerraformPath)
119+
if err != nil {
120+
return false, fmt.Errorf("failed to read terraform state attributes: %w", err)
121+
}
122+
120123
state := make(map[string]dstate.ResourceEntry)
121124
etags := map[string]string{}
122125

@@ -141,8 +144,8 @@ func (m *uploadStateForYamlSync) convertState(ctx context.Context, b *bundle.Bun
141144
migratedDB := dstate.NewDatabase(tfState.Lineage, tfState.Serial+1)
142145
migratedDB.State = state
143146

144-
deploymentBundle := &direct.DeploymentBundle{}
145-
deploymentBundle.StateDB.OpenWithData(snapshotPath, migratedDB)
147+
var stateDB dstate.DeploymentState
148+
stateDB.OpenWithData(snapshotPath, migratedDB)
146149

147150
// Apply SecretScopeFixups so the config matches what the direct engine expects.
148151
// This adds MANAGE ACL for the current user to all secret scopes, ensuring
@@ -152,9 +155,9 @@ func (m *uploadStateForYamlSync) convertState(ctx context.Context, b *bundle.Bun
152155
return false, errors.New("failed to apply secret scope fixups")
153156
}
154157

155-
// Get the dynamic value from b.Config and reverse the interpolation.
156158
// b.Config has been modified by terraform.Interpolate which converts bundle-style
157159
// references (${resources.pipelines.x.id}) to terraform-style (${databricks_pipeline.x.id}).
160+
// BuildStateFromTF expects ${resources.*} references, so reverse the interpolation first.
158161
interpolatedRoot := b.Config.Value()
159162
uninterpolatedRoot, err := reverseInterpolate(interpolatedRoot)
160163
if err != nil {
@@ -169,36 +172,20 @@ func (m *uploadStateForYamlSync) convertState(ctx context.Context, b *bundle.Bun
169172
return false, fmt.Errorf("failed to create uninterpolated config: %w", err)
170173
}
171174

172-
plan, err := deploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &uninterpolatedConfig)
175+
adapters, err := dresources.InitAll(b.WorkspaceClient(ctx))
173176
if err != nil {
174177
return false, err
175178
}
176179

177-
for _, entry := range plan.Plan {
178-
entry.Action = deployplan.Update
179-
}
180-
181-
for key := range plan.Plan {
182-
etag := etags[key]
183-
if etag == "" {
184-
continue
185-
}
186-
sv, ok := deploymentBundle.StateCache.Load(key)
187-
if !ok {
188-
continue
189-
}
190-
err := structaccess.Set(sv.Value, structpath.NewStringKey(nil, "etag"), etag)
191-
if err != nil {
192-
log.Warnf(ctx, "Failed to set etag on %q: %v", key, err)
193-
}
180+
if err := stateDB.UpgradeToWrite(); err != nil {
181+
return false, fmt.Errorf("upgrading state for apply: %w", err)
194182
}
195183

196-
if err := deploymentBundle.StateDB.UpgradeToWrite(); err != nil {
197-
return false, fmt.Errorf("upgrading state for apply: %w", err)
184+
if err := migrate.BuildStateFromTF(ctx, &uninterpolatedConfig, adapters, &stateDB, tfAttrs, terraformResources, etags); err != nil {
185+
return false, err
198186
}
199187

200-
deploymentBundle.Apply(ctx, b.WorkspaceClient(ctx), plan, direct.MigrateMode(true))
201-
if _, err := deploymentBundle.StateDB.Finalize(ctx); err != nil {
188+
if _, err := stateDB.Finalize(ctx); err != nil {
202189
return false, err
203190
}
204191

0 commit comments

Comments
 (0)