Skip to content

Commit a7196e3

Browse files
committed
split StructVar into StructVar + StructVarJSON
1 parent c92086b commit a7196e3

File tree

10 files changed

+209
-109
lines changed

10 files changed

+209
-109
lines changed

bundle/deployplan/plan.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ func LoadPlanFromFile(path string) (*Plan, error) {
6464
}
6565

6666
type PlanEntry struct {
67-
ID string `json:"id,omitempty"`
68-
DependsOn []DependsOnEntry `json:"depends_on,omitempty"`
69-
Action string `json:"action,omitempty"`
70-
NewState *structvar.StructVar `json:"new_state,omitempty"`
71-
RemoteState any `json:"remote_state,omitempty"`
72-
Changes *Changes `json:"changes,omitempty"`
67+
ID string `json:"id,omitempty"`
68+
DependsOn []DependsOnEntry `json:"depends_on,omitempty"`
69+
Action string `json:"action,omitempty"`
70+
NewState *structvar.StructVarJSON `json:"new_state,omitempty"`
71+
RemoteState any `json:"remote_state,omitempty"`
72+
Changes *Changes `json:"changes,omitempty"`
7373
}
7474

7575
type DependsOnEntry struct {

bundle/direct/bundle_apply.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,19 @@ func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.Workspa
9797
// We don't keep NewState around for 'skip' nodes
9898

9999
if at != deployplan.ActionTypeSkip {
100-
if !b.resolveReferences(ctx, entry, errorPrefix, false, adapter.StateType()) {
100+
if !b.resolveReferences(ctx, resourceKey, entry, errorPrefix, false) {
101101
return false
102102
}
103103

104-
if len(entry.NewState.Refs) > 0 {
105-
logdiag.LogError(ctx, fmt.Errorf("%s: unresolved references: %s", errorPrefix, jsonDump(entry.NewState.Refs)))
104+
// Get the cached StructVar to check for unresolved refs and get value
105+
sv, ok := b.StructVarCache.Load(resourceKey)
106+
if !ok {
107+
logdiag.LogError(ctx, fmt.Errorf("%s: internal error: missing cached StructVar", errorPrefix))
108+
return false
109+
}
110+
111+
if len(sv.Refs) > 0 {
112+
logdiag.LogError(ctx, fmt.Errorf("%s: unresolved references: %s", errorPrefix, jsonDump(sv.Refs)))
106113
return false
107114
}
108115

@@ -113,10 +120,10 @@ func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.Workspa
113120
logdiag.LogError(ctx, fmt.Errorf("state entry not found for %q", resourceKey))
114121
return false
115122
}
116-
err = b.StateDB.SaveState(resourceKey, dbentry.ID, entry.NewState.GetValue(), entry.DependsOn)
123+
err = b.StateDB.SaveState(resourceKey, dbentry.ID, sv.Value, entry.DependsOn)
117124
} else {
118125
// TODO: redo calcDiff to downgrade planned action if possible (?)
119-
err = d.Deploy(ctx, &b.StateDB, entry.NewState.GetValue(), at, entry.Changes)
126+
err = d.Deploy(ctx, &b.StateDB, sv.Value, at, entry.Changes)
120127
}
121128

122129
if err != nil {

bundle/direct/bundle_plan.go

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ func (b *DeploymentBundle) InitForApply(ctx context.Context, client *databricks.
5050
return err
5151
}
5252

53-
// Load NewState.Value from json.RawMessage into typed structs.
53+
// Eagerly parse all StructVarJSON entries to catch parsing errors early.
5454
// When the plan is read from JSON, Value contains raw JSON bytes.
55-
// We need to parse them into the correct types for each resource.
55+
// We parse them into typed structs and cache them for later use.
5656
for resourceKey, entry := range plan.Plan {
5757
if entry.NewState == nil || len(entry.NewState.Value) == 0 {
5858
continue
@@ -63,9 +63,12 @@ func (b *DeploymentBundle) InitForApply(ctx context.Context, client *databricks.
6363
return fmt.Errorf("cannot convert plan entry %s: %w", resourceKey, err)
6464
}
6565

66-
if err := entry.NewState.Load(adapter.StateType()); err != nil {
66+
sv, err := entry.NewState.ToStructVar(adapter.StateType())
67+
if err != nil {
6768
return fmt.Errorf("cannot load plan entry %s: %w", resourceKey, err)
6869
}
70+
71+
b.StructVarCache.Store(resourceKey, sv)
6972
}
7073

7174
b.Plan = plan
@@ -153,7 +156,7 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks
153156

154157
// Process all references in the resource using Refs map
155158
// Refs maps path inside resource to references e.g. "${resources.jobs.foo.id} ${resources.jobs.foo.name}"
156-
if !b.resolveReferences(ctx, entry, errorPrefix, true, adapter.StateType()) {
159+
if !b.resolveReferences(ctx, resourceKey, entry, errorPrefix, true) {
157160
return false
158161
}
159162

@@ -180,7 +183,8 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks
180183
// for integers: compare 0 with actual object ID. As long as real object IDs are never 0 we're good.
181184
// Once we add non-id fields or add per-field details to "bundle plan", we must read dynamic data and deal with references as first class citizen.
182185
// This means distinguishing between 0 that are actually object ids and 0 that are there because typed struct integer cannot contain ${...} string.
183-
localDiff, err := structdiff.GetStructDiff(savedState, entry.NewState.GetValue(), adapter.KeyedSlices())
186+
sv, _ := b.StructVarCache.Load(resourceKey)
187+
localDiff, err := structdiff.GetStructDiff(savedState, sv.Value, adapter.KeyedSlices())
184188
if err != nil {
185189
logdiag.LogError(ctx, fmt.Errorf("%s: diffing local state: %w", errorPrefix, err))
186190
return false
@@ -374,13 +378,19 @@ func (b *DeploymentBundle) LookupReferenceLocal(ctx context.Context, path *struc
374378
return nil, fmt.Errorf("internal error: %s: action is %q missing new_state", targetResourceKey, targetEntry.Action)
375379
}
376380

377-
_, isUnresolved := targetEntry.NewState.Refs[fieldPathS]
381+
// Get StructVar from cache
382+
sv, ok := b.StructVarCache.Load(targetResourceKey)
383+
if !ok {
384+
return nil, fmt.Errorf("internal error: %s: missing cached StructVar", targetResourceKey)
385+
}
386+
387+
_, isUnresolved := sv.Refs[fieldPathS]
378388
if isUnresolved {
379389
// The value that is requested is itself a reference; this means it will be resolved after apply
380390
return nil, errDelayed
381391
}
382392

383-
localConfig := targetEntry.NewState.GetValue()
393+
localConfig := sv.Value
384394

385395
targetGroup := config.GetResourceTypeFromKey(targetResourceKey)
386396
adapter := b.Adapters[targetGroup]
@@ -437,17 +447,28 @@ func (b *DeploymentBundle) LookupReferenceLocal(ctx context.Context, path *struc
437447
return nil, errDelayed
438448
}
439449

450+
// getStructVar returns the cached StructVar for the given resource key.
451+
// The StructVar must have been eagerly loaded during plan creation or InitForApply.
452+
func (b *DeploymentBundle) getStructVar(resourceKey string) (*structvar.StructVar, error) {
453+
sv, ok := b.StructVarCache.Load(resourceKey)
454+
if !ok {
455+
return nil, fmt.Errorf("internal error: StructVar not found in cache for %s", resourceKey)
456+
}
457+
return sv, nil
458+
}
459+
440460
// resolveReferences processes all references in entry.NewState.Refs.
441461
// If isLocal is true, uses LookupReferenceLocal (for planning phase).
442462
// If isLocal is false, uses LookupReferenceRemote (for apply phase).
443-
func (b *DeploymentBundle) resolveReferences(ctx context.Context, entry *deployplan.PlanEntry, errorPrefix string, isLocal bool, stateType reflect.Type) bool {
444-
if err := entry.NewState.Load(stateType); err != nil {
445-
logdiag.LogError(ctx, fmt.Errorf("%s: cannot load state: %w", errorPrefix, err))
463+
func (b *DeploymentBundle) resolveReferences(ctx context.Context, resourceKey string, entry *deployplan.PlanEntry, errorPrefix string, isLocal bool) bool {
464+
sv, err := b.getStructVar(resourceKey)
465+
if err != nil {
466+
logdiag.LogError(ctx, fmt.Errorf("%s: %w", errorPrefix, err))
446467
return false
447468
}
448469

449470
var resolved bool
450-
for fieldPathStr, refString := range entry.NewState.Refs {
471+
for fieldPathStr, refString := range sv.Refs {
451472
refs, ok := dynvar.NewRef(dyn.V(refString))
452473
if !ok {
453474
logdiag.LogError(ctx, fmt.Errorf("%s: cannot parse %q", errorPrefix, refString))
@@ -480,7 +501,7 @@ func (b *DeploymentBundle) resolveReferences(ctx context.Context, entry *deployp
480501
}
481502
}
482503

483-
err = entry.NewState.ResolveRef(ref, value)
504+
err = sv.ResolveRef(ref, value)
484505
if err != nil {
485506
logdiag.LogError(ctx, fmt.Errorf("%s: cannot update %s with value of %q: %w", errorPrefix, fieldPathStr, ref, err))
486507
return false
@@ -489,12 +510,14 @@ func (b *DeploymentBundle) resolveReferences(ctx context.Context, entry *deployp
489510
}
490511
}
491512

513+
// Sync resolved values back to StructVarJSON for serialization
492514
if resolved {
493-
if err := entry.NewState.Save(); err != nil {
515+
if err := sv.SyncToJSON(entry.NewState); err != nil {
494516
logdiag.LogError(ctx, fmt.Errorf("%s: cannot save state: %w", errorPrefix, err))
495517
return false
496518
}
497519
}
520+
498521
return true
499522
}
500523

@@ -574,14 +597,14 @@ func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root
574597
if err != nil {
575598
return nil, err
576599
}
577-
inputConfig = inputConfigStructVar.GetValue()
600+
inputConfig = inputConfigStructVar.Value
578601
baseRefs = inputConfigStructVar.Refs
579602
} else if strings.HasSuffix(node, ".grants") {
580603
inputConfigStructVar, err := dresources.PrepareGrantsInputConfig(inputConfig, node)
581604
if err != nil {
582605
return nil, err
583606
}
584-
inputConfig = inputConfigStructVar.GetValue()
607+
inputConfig = inputConfigStructVar.Value
585608
baseRefs = inputConfigStructVar.Refs
586609
}
587610

@@ -642,14 +665,23 @@ func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root
642665
return strings.Compare(a.Label, b.Label)
643666
})
644667

645-
newState, err := structvar.NewStructVar(newStateConfig, refs)
668+
newState := &structvar.StructVar{
669+
Value: newStateConfig,
670+
Refs: refs,
671+
}
672+
673+
// Store in cache for use during planning phase
674+
b.StructVarCache.Store(node, newState)
675+
676+
// Convert to JSON for serialization in plan
677+
newStateJSON, err := newState.ToJSON()
646678
if err != nil {
647-
return nil, fmt.Errorf("%s: cannot create state: %w", node, err)
679+
return nil, fmt.Errorf("%s: cannot serialize state: %w", node, err)
648680
}
649681

650682
e := deployplan.PlanEntry{
651683
DependsOn: dependsOn,
652-
NewState: newState,
684+
NewState: newStateJSON,
653685
}
654686

655687
p.Plan[node] = &e

bundle/direct/dresources/grants.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,16 @@ func PrepareGrantsInputConfig(inputConfig any, node string) (*structvar.StructVa
9494
})
9595
}
9696

97-
return structvar.NewStructVar(
98-
&GrantsState{
97+
return &structvar.StructVar{
98+
Value: &GrantsState{
9999
SecurableType: securableType,
100100
FullName: "",
101101
Grants: grants,
102102
},
103-
map[string]string{
103+
Refs: map[string]string{
104104
"full_name": "${" + baseNode + ".id}",
105105
},
106-
)
106+
}, nil
107107
}
108108

109109
type ResourceGrants struct {

bundle/direct/dresources/permissions.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,15 @@ func PreparePermissionsInputConfig(inputConfig any, node string) (*structvar.Str
6666
objectIdRef = prefix + "${" + baseNode + ".endpoint_id}"
6767
}
6868

69-
return structvar.NewStructVar(
70-
&PermissionsState{
69+
return &structvar.StructVar{
70+
Value: &PermissionsState{
7171
ObjectID: "", // Always a reference, defined in Refs below
7272
Permissions: permissions,
7373
},
74-
map[string]string{
74+
Refs: map[string]string{
7575
"object_id": objectIdRef,
7676
},
77-
)
77+
}, nil
7878
}
7979

8080
func (*ResourcePermissions) New(client *databricks.WorkspaceClient) *ResourcePermissions {

bundle/direct/dresources/secret_scope_acls.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,15 @@ func PrepareSecretScopeAclsInputConfig(inputConfig []resources.SecretScopePermis
4545
acls = append(acls, acl)
4646
}
4747

48-
return structvar.NewStructVar(
49-
&SecretScopeAclsState{
48+
return &structvar.StructVar{
49+
Value: &SecretScopeAclsState{
5050
ScopeName: "", // Always a reference, defined in Refs below
5151
Acls: acls,
5252
},
53-
map[string]string{
53+
Refs: map[string]string{
5454
"scope_name": "${" + baseNode + ".name}",
5555
},
56-
)
56+
}, nil
5757
}
5858

5959
func (*ResourceSecretScopeAcls) New(client *databricks.WorkspaceClient) *ResourceSecretScopeAcls {

bundle/direct/pkg.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/databricks/cli/bundle/direct/dresources"
1111
"github.com/databricks/cli/bundle/direct/dstate"
1212
"github.com/databricks/cli/bundle/statemgmt/resourcestate"
13+
"github.com/databricks/cli/libs/structs/structvar"
1314
)
1415

1516
// How many parallel operations (API calls) are allowed
@@ -42,6 +43,7 @@ type DeploymentBundle struct {
4243
Adapters map[string]*dresources.Adapter
4344
Plan *deployplan.Plan
4445
RemoteStateCache sync.Map
46+
StructVarCache structvar.Cache
4547
}
4648

4749
// SetRemoteState updates the remote state with type validation and marks as fresh.

cmd/bundle/deployment/migrate.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,18 +217,20 @@ To start using direct engine, deploy with DATABRICKS_BUNDLE_ENGINE=direct env va
217217
// comes from remote state. If we don't store "etag" in state, we won't detect remote drift, because
218218
// local=nil, remote="<some new etag>" which will be classified as "server_side_default".
219219

220-
for key, planEntry := range plan.Plan {
220+
for key := range plan.Plan {
221221
etag := etags[key]
222222
if etag == "" {
223223
continue
224224
}
225-
err := structaccess.Set(planEntry.NewState.GetValue(), structpath.NewStringKey(nil, "etag"), etag)
225+
// Get the cached StructVar created during planning
226+
sv, ok := deploymentBundle.StructVarCache.Load(key)
227+
if !ok {
228+
return fmt.Errorf("failed to get cached state for %s", key)
229+
}
230+
err := structaccess.Set(sv.Value, structpath.NewStringKey(nil, "etag"), etag)
226231
if err != nil {
227232
return fmt.Errorf("failed to set etag on %s: %w", key, err)
228233
}
229-
if err := planEntry.NewState.Save(); err != nil {
230-
return fmt.Errorf("failed to save state for %s: %w", key, err)
231-
}
232234
}
233235

234236
deploymentBundle.Apply(ctx, b.WorkspaceClient(), &b.Config, plan, direct.MigrateMode(true))

0 commit comments

Comments
 (0)