Skip to content

Commit 0437317

Browse files
authored
direct: refactor deploy/plan interaction (#3350)
## Changes "bundle deploy" no longer calculates and classifies the diff, it relies on action types calculated during "bundle plan". Note, this will need to be further developed once we handle ${resources} references - we might want to recalculate the diff in some cases. We no longer have UpdateUpdatesID setting on the resource. Instead, resources can implement optional method DoUpdateWithID that can return an updated id as part of the update. Currently, only volumes uses that. The original DoUpdate method no longer allows to return ID. By convention, if possible, it also checks that ID is not changed by the backend. There is new ActionType: "update_with_id". This is what triggers DoUpdateWithID() call. However, when stringified, this is presented as "update" so that it's compatible with terraform output in "bundle plan". ## Why The main motivation is to have information at plan-time on whether we're going to update ID or not. If not, we can resolve ${resources.*.*.id} right away and have more parallelism at deploy time. Additionally, this simplifies usual case DoUpdate() that resources need to implement. ## Tests Existing tests. The new action type is tested by #3349
1 parent 1a7fe3f commit 0437317

10 files changed

Lines changed: 191 additions & 127 deletions

File tree

bundle/deployplan/plan.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,34 @@ func (a Action) IsInplaceSupported() bool {
3636
return false
3737
}
3838

39-
// These enum values correspond to action types defined in the tfjson library.
39+
// These enum values are superset to action types defined in the tfjson library.
4040
// "recreate" maps to the tfjson.Actions.Replace() function.
41-
// "update" maps to tfjson.Actions.Update() and so on. source:
41+
// "update" and "update_with_id" maps to tfjson.Actions.Update() and so on. source:
4242
// https://github.com/hashicorp/terraform-json/blob/0104004301ca8e7046d089cdc2e2db2179d225be/action.go#L14
4343
type ActionType string
4444

4545
const (
46-
ActionTypeUnset ActionType = ""
47-
ActionTypeNoop ActionType = "noop"
48-
ActionTypeCreate ActionType = "create"
49-
ActionTypeDelete ActionType = "delete"
50-
ActionTypeUpdate ActionType = "update"
51-
ActionTypeRecreate ActionType = "recreate"
46+
ActionTypeUnset ActionType = ""
47+
ActionTypeNoop ActionType = "noop"
48+
ActionTypeCreate ActionType = "create"
49+
ActionTypeDelete ActionType = "delete"
50+
ActionTypeUpdate ActionType = "update"
51+
ActionTypeUpdateWithID ActionType = "update_with_id"
52+
ActionTypeRecreate ActionType = "recreate"
5253
)
5354

55+
var ShortName = map[ActionType]ActionType{
56+
ActionTypeUpdateWithID: ActionTypeUpdate,
57+
}
58+
59+
func (a ActionType) String() string {
60+
shortAction := ShortName[a]
61+
if shortAction != "" {
62+
return string(shortAction)
63+
}
64+
return string(a)
65+
}
66+
5467
// Filter returns actions that match the specified action type
5568
func Filter(changes []Action, actionType ActionType) []Action {
5669
var result []Action

bundle/terranova/apply.go

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"github.com/databricks/cli/libs/diag"
1616
"github.com/databricks/cli/libs/log"
1717
"github.com/databricks/cli/libs/logdiag"
18-
"github.com/databricks/cli/libs/structdiff"
1918
"github.com/databricks/databricks-sdk-go"
2019
)
2120

@@ -143,46 +142,41 @@ func (d *Deployer) destroy(ctx context.Context, inputConfig any) error {
143142
}
144143

145144
func (d *Deployer) deploy(ctx context.Context, inputConfig any, actionType deployplan.ActionType) error {
146-
entry, hasEntry := d.db.GetResourceEntry(d.group, d.resourceName)
147-
148-
resource, cfgType, err := New(d.client, d.group, d.resourceName, inputConfig)
145+
resource, _, err := New(d.client, d.group, d.resourceName, inputConfig)
149146
if err != nil {
150147
return err
151148
}
152149

153150
config := resource.Config()
154151

155-
if !hasEntry {
152+
if actionType == deployplan.ActionTypeCreate {
156153
return d.Create(ctx, resource, config)
157154
}
158155

156+
entry, hasEntry := d.db.GetResourceEntry(d.group, d.resourceName)
157+
if !hasEntry {
158+
return errors.New("state entry not found")
159+
}
160+
159161
oldID := entry.ID
160162
if oldID == "" {
161163
return errors.New("invalid state: empty id")
162164
}
163165

164-
savedState, err := typeConvert(cfgType, entry.State)
165-
if err != nil {
166-
return fmt.Errorf("interpreting state: %w", err)
167-
}
168-
169-
localDiffType, err := calcDiff(d.settings, resource, savedState, config)
170-
if err != nil {
171-
return fmt.Errorf("comparing state and config: %w", err)
172-
}
173-
174-
if localDiffType == deployplan.ActionTypeRecreate {
166+
switch actionType {
167+
case deployplan.ActionTypeRecreate:
175168
return d.Recreate(ctx, resource, oldID, config)
176-
}
177-
178-
if localDiffType == deployplan.ActionTypeUpdate {
169+
case deployplan.ActionTypeUpdate:
179170
return d.Update(ctx, resource, oldID, config)
171+
case deployplan.ActionTypeUpdateWithID:
172+
updater, hasUpdater := resource.(IResourceUpdatesID)
173+
if !hasUpdater {
174+
return errors.New("internal error: plan is update_with_id but resource does not implement UpdateWithID")
175+
}
176+
return d.UpdateWithID(ctx, resource, updater, oldID, config)
177+
default:
178+
return fmt.Errorf("internal error: unexpected plan: %#v", actionType)
180179
}
181-
182-
// localDiffType is either None or Partial: we should proceed to fetching remote state and calculate local+remote diff
183-
184-
log.Debugf(ctx, "Unchanged %s.%s id=%#v", d.group, d.resourceName, oldID)
185-
return nil
186180
}
187181

188182
func (d *Deployer) Create(ctx context.Context, resource IResource, config any) error {
@@ -238,8 +232,27 @@ func (d *Deployer) Recreate(ctx context.Context, resource IResource, oldID strin
238232
return nil
239233
}
240234

241-
func (d *Deployer) Update(ctx context.Context, resource IResource, oldID string, config any) error {
242-
newID, err := resource.DoUpdate(ctx, oldID)
235+
func (d *Deployer) Update(ctx context.Context, resource IResource, id string, config any) error {
236+
err := resource.DoUpdate(ctx, id)
237+
if err != nil {
238+
return fmt.Errorf("updating id=%s: %w", id, err)
239+
}
240+
241+
err = d.db.SaveState(d.group, d.resourceName, id, config)
242+
if err != nil {
243+
return fmt.Errorf("saving state id=%s: %w", id, err)
244+
}
245+
246+
err = resource.WaitAfterUpdate(ctx)
247+
if err != nil {
248+
return fmt.Errorf("waiting after updating id=%s: %w", id, err)
249+
}
250+
251+
return nil
252+
}
253+
254+
func (d *Deployer) UpdateWithID(ctx context.Context, resource IResource, updater IResourceUpdatesID, oldID string, config any) error {
255+
newID, err := updater.DoUpdateWithID(ctx, oldID)
243256
if err != nil {
244257
return fmt.Errorf("updating id=%s: %w", oldID, err)
245258
}
@@ -255,14 +268,11 @@ func (d *Deployer) Update(ctx context.Context, resource IResource, oldID string,
255268
return fmt.Errorf("saving state id=%s: %w", oldID, err)
256269
}
257270

258-
if oldID != newID && !d.settings.UpdateUpdatesID {
259-
return fmt.Errorf("internal error, unexpected change of ID from %#v to %#v", oldID, newID)
260-
}
261-
262271
err = resource.WaitAfterUpdate(ctx)
263272
if err != nil {
264273
return fmt.Errorf("waiting after updating id=%s: %w", newID, err)
265274
}
275+
266276
return nil
267277
}
268278

@@ -296,22 +306,3 @@ func typeConvert(destType reflect.Type, src any) (any, error) {
296306

297307
return reflect.ValueOf(destPtr).Elem().Interface(), nil
298308
}
299-
300-
func calcDiff(settings ResourceSettings, resource IResource, savedState, config any) (deployplan.ActionType, error) {
301-
localDiff, err := structdiff.GetStructDiff(savedState, config)
302-
if err != nil {
303-
return "", err
304-
}
305-
306-
if settings.MustRecreate(localDiff) {
307-
return deployplan.ActionTypeRecreate, nil
308-
} else if len(localDiff) > 0 {
309-
result := resource.ClassifyChanges(localDiff)
310-
if result == deployplan.ActionTypeRecreate && !settings.RecreateAllowed {
311-
return "", errors.New("internal error: ClassifyChanges returned 'recreate' unexpectedly")
312-
}
313-
return result, nil
314-
}
315-
316-
return deployplan.ActionTypeNoop, nil
317-
}

bundle/terranova/plan.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/databricks/cli/bundle/deployplan"
1010
"github.com/databricks/cli/bundle/terranova/tnstate"
1111
"github.com/databricks/cli/libs/dyn"
12+
"github.com/databricks/cli/libs/structdiff"
1213
"github.com/databricks/cli/libs/utils"
1314
"github.com/databricks/databricks-sdk-go"
1415
)
@@ -22,6 +23,7 @@ type Planner struct {
2223
}
2324

2425
func (d *Planner) Plan(ctx context.Context, inputConfig any) (deployplan.ActionType, error) {
26+
// TODO: wrap errors with prefix "planning <group>.<name>:"
2527
entry, hasEntry := d.db.GetResourceEntry(d.group, d.resourceName)
2628

2729
resource, cfgType, err := New(d.client, d.group, d.resourceName, inputConfig)
@@ -158,3 +160,37 @@ func CalculateDestroyActions(ctx context.Context, b *bundle.Bundle) ([]deploypla
158160

159161
return actions, nil
160162
}
163+
164+
func calcDiff(settings ResourceSettings, resource IResource, savedState, config any) (deployplan.ActionType, error) {
165+
localDiff, err := structdiff.GetStructDiff(savedState, config)
166+
if err != nil {
167+
return "", err
168+
}
169+
170+
if len(localDiff) == 0 {
171+
return deployplan.ActionTypeNoop, nil
172+
}
173+
174+
if settings.MustRecreate(localDiff) {
175+
return deployplan.ActionTypeRecreate, nil
176+
}
177+
178+
customClassify, hasCustomClassify := resource.(IResourceCustomClassify)
179+
180+
if hasCustomClassify {
181+
_, hasUpdateWithID := resource.(IResourceUpdatesID)
182+
183+
result := customClassify.ClassifyChanges(localDiff)
184+
if result == deployplan.ActionTypeRecreate && !settings.RecreateAllowed {
185+
return "", errors.New("internal error: unexpected plan='recreate'")
186+
}
187+
188+
if result == deployplan.ActionTypeUpdateWithID && !hasUpdateWithID {
189+
return "", errors.New("internal error: unexpected plan='update_with_id'")
190+
}
191+
192+
return result, nil
193+
}
194+
195+
return deployplan.ActionTypeUpdate, nil
196+
}

bundle/terranova/resource.go

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ type ResourceSettings struct {
2626
// Function to delete a resource of this type
2727
DeleteFN DeleteResourceFN
2828

29-
// true if Update() method can return a different ID than that was passed in
30-
// If ID changes during Update and UpdateUpdatesID is false, deployment of that resource will fail with internal error.
31-
// This allows to make assumptions about references stability (${resources.jobs.foo.id}) when we see that
32-
// operation is going to be "update" & ID is guarantee not to change.
33-
UpdateUpdatesID bool
34-
3529
// true if ClassifyChanges() method can return a different ActionTypeRecreate
3630
// If RecreateAllowed is false and RecreateFields is empty, the resource id is stable.
3731
RecreateAllowed bool
@@ -41,9 +35,6 @@ type ResourceSettings struct {
4135
// Fields are in structdiff.Change.String() format.
4236
// Limitation: patterns like hello.*.world and hello[*].world are not supported
4337
RecreateFields map[string]struct{}
44-
45-
// If resource does not set RecreateFields, RecreateAllowed, UpdateUpdatesID then
46-
// it's ${resources.<group>.<name>.id} will considered stable for the purposes of concurrent deployment.
4738
}
4839

4940
func (s *ResourceSettings) MustRecreate(changes []structdiff.Change) bool {
@@ -94,10 +85,9 @@ var SupportedResources = map[string]ResourceSettings{
9485
),
9586
},
9687
"volumes": {
97-
New: reflect.ValueOf(tnresources.NewResourceVolume),
98-
ConfigType: TypeOfConfig(&tnresources.ResourceVolume{}),
99-
DeleteFN: tnresources.DeleteVolume,
100-
UpdateUpdatesID: true,
88+
New: reflect.ValueOf(tnresources.NewResourceVolume),
89+
ConfigType: TypeOfConfig(&tnresources.ResourceVolume{}),
90+
DeleteFN: tnresources.DeleteVolume,
10191
// TF: https://github.com/databricks/terraform-provider-databricks/blob/f5fce0f/catalog/resource_volume.go#L19
10292
RecreateFields: mkMap(
10393
".catalog_name",
@@ -124,17 +114,26 @@ type IResource interface {
124114
// Create the resource. Returns id of the resource.
125115
DoCreate(ctx context.Context) (string, error)
126116

127-
// Update the resource. Returns id of the resource.
128-
// Usually returns the same id as oldId but can also return a different one (e.g. schemas and volumes when certain fields are changed)
129-
// Note, SupportedResources[group].UpdateUpdatesID must be true for this group if ID can be changed. Otherwise function must return the same ID.
130-
DoUpdate(ctx context.Context, oldID string) (string, error)
117+
// Update the resource. ID must not change.
118+
DoUpdate(ctx context.Context, id string) error
131119

132120
WaitAfterCreate(ctx context.Context) error
133121
WaitAfterUpdate(ctx context.Context) error
122+
}
134123

124+
// Optional method for non-default change classification.
125+
// Default is to consider any change "an update" (RecreateFields handled separately).
126+
type IResourceCustomClassify interface {
135127
ClassifyChanges(changes []structdiff.Change) deployplan.ActionType
136128
}
137129

130+
// Optional method for resources that may update ID as part of update operation.
131+
type IResourceUpdatesID interface {
132+
// Update the resource. Returns new id of the resource, which may be different from the old one.
133+
// This will only be called if actiontype is ActionTypeUpdateWithID, so ClassifyChanges must be implemented as well.
134+
DoUpdateWithID(ctx context.Context, oldID string) (string, error)
135+
}
136+
138137
// invokeConstructor converts cfg to the parameter type expected by ctor and
139138
// executes the call, returning the IResource instance or error.
140139
func invokeConstructor(ctor reflect.Value, client *databricks.WorkspaceClient, cfg any) (IResource, error) {

bundle/terranova/tnresources/app.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import (
44
"context"
55

66
"github.com/databricks/cli/bundle/config/resources"
7-
"github.com/databricks/cli/bundle/deployplan"
8-
"github.com/databricks/cli/libs/structdiff"
7+
"github.com/databricks/cli/libs/log"
98
"github.com/databricks/databricks-sdk-go"
109
"github.com/databricks/databricks-sdk-go/service/apps"
1110
)
@@ -41,17 +40,21 @@ func (r *ResourceApp) DoCreate(ctx context.Context) (string, error) {
4140
return waiter.Response.Name, nil
4241
}
4342

44-
func (r *ResourceApp) DoUpdate(ctx context.Context, id string) (string, error) {
43+
func (r *ResourceApp) DoUpdate(ctx context.Context, id string) error {
4544
request := apps.UpdateAppRequest{
4645
App: r.config,
4746
Name: id,
4847
}
4948
response, err := r.client.Apps.Update(ctx, request)
5049
if err != nil {
51-
return "", SDKError{Method: "Apps.Update", Err: err}
50+
return SDKError{Method: "Apps.Update", Err: err}
5251
}
5352

54-
return response.Name, nil
53+
if response.Name != id {
54+
log.Warnf(ctx, "apps: response contains unexpected name=%#v (expected %#v)", response.Name, id)
55+
}
56+
57+
return nil
5558
}
5659

5760
func DeleteApp(ctx context.Context, client *databricks.WorkspaceClient, id string) error {
@@ -68,8 +71,3 @@ func (r *ResourceApp) WaitAfterUpdate(ctx context.Context) error {
6871
// Intentional no-op
6972
return nil
7073
}
71-
72-
func (r *ResourceApp) ClassifyChanges(changes []structdiff.Change) deployplan.ActionType {
73-
// TODO: changing name is recreation
74-
return deployplan.ActionTypeUpdate
75-
}

bundle/terranova/tnresources/job.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import (
55
"strconv"
66

77
"github.com/databricks/cli/bundle/config/resources"
8-
"github.com/databricks/cli/bundle/deployplan"
9-
"github.com/databricks/cli/libs/structdiff"
108
"github.com/databricks/databricks-sdk-go"
119
"github.com/databricks/databricks-sdk-go/service/jobs"
1210
)
@@ -40,16 +38,16 @@ func (r *ResourceJob) DoCreate(ctx context.Context) (string, error) {
4038
return strconv.FormatInt(response.JobId, 10), nil
4139
}
4240

43-
func (r *ResourceJob) DoUpdate(ctx context.Context, id string) (string, error) {
41+
func (r *ResourceJob) DoUpdate(ctx context.Context, id string) error {
4442
request, err := makeResetJob(r.config, id)
4543
if err != nil {
46-
return "", err
44+
return err
4745
}
4846
err = r.client.Jobs.Reset(ctx, request)
4947
if err != nil {
50-
return "", SDKError{Method: "Jobs.Reset", Err: err}
48+
return SDKError{Method: "Jobs.Reset", Err: err}
5149
}
52-
return id, nil
50+
return nil
5351
}
5452

5553
func DeleteJob(ctx context.Context, client *databricks.WorkspaceClient, id string) error {
@@ -74,10 +72,6 @@ func (r *ResourceJob) WaitAfterUpdate(ctx context.Context) error {
7472
return nil
7573
}
7674

77-
func (r *ResourceJob) ClassifyChanges(changes []structdiff.Change) deployplan.ActionType {
78-
return deployplan.ActionTypeUpdate
79-
}
80-
8175
func makeCreateJob(config jobs.JobSettings) (jobs.CreateJob, error) {
8276
result := jobs.CreateJob{}
8377
// TODO: Validate copy - all fields must be initialized or explicitly allowed to be empty

0 commit comments

Comments
 (0)