Skip to content

Commit 8c21f2d

Browse files
committed
addressed feedback
1 parent 9bf4eae commit 8c21f2d

File tree

12 files changed

+363
-111
lines changed

12 files changed

+363
-111
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
bundle:
2+
name: test-bundle-$UNIQUE_NAME
3+
4+
resources:
5+
apps:
6+
foo:
7+
name: app-foo-$UNIQUE_NAME
8+
source_code_path: ./app
9+
lifecycle:
10+
started: true
11+
bar:
12+
name: app-bar-$UNIQUE_NAME
13+
source_code_path: ./app
14+
lifecycle:
15+
started: ${resources.apps.foo.lifecycle.started}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Hello world\!")
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
bundle:
2+
name: lifecycle-started-omitted-$UNIQUE_NAME
3+
4+
resources:
5+
apps:
6+
mykey:
7+
name: $UNIQUE_NAME
8+
description: my_app_description
9+
source_code_path: ./app

acceptance/bundle/resources/apps/lifecycle-started-omitted/out.test.toml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
2+
=== (started omitted) -> deploy: app created with no_compute=true, no start/stop requests
3+
>>> [CLI] bundle deploy
4+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/lifecycle-started-omitted-[UNIQUE_NAME]/default/files...
5+
Deploying resources...
6+
Updating deployment state...
7+
Deployment complete!
8+
9+
>>> print_app_requests
10+
{
11+
"body": {
12+
"description": "my_app_description",
13+
"name": "[UNIQUE_NAME]"
14+
},
15+
"method": "POST",
16+
"path": "/api/2.0/apps",
17+
"q": {
18+
"no_compute": "true"
19+
}
20+
}
21+
22+
=== (started omitted) -> started: false -> deploy: no start/stop requests
23+
>>> [CLI] bundle deploy
24+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/lifecycle-started-omitted-[UNIQUE_NAME]/default/files...
25+
Deploying resources...
26+
Updating deployment state...
27+
Deployment complete!
28+
29+
>>> print_app_requests
30+
31+
=== started: false -> (started omitted) -> deploy: no start/stop requests
32+
>>> [CLI] bundle deploy
33+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/lifecycle-started-omitted-[UNIQUE_NAME]/default/files...
34+
Deploying resources...
35+
Updating deployment state...
36+
Deployment complete!
37+
38+
>>> print_app_requests
39+
40+
=== (started omitted) -> started: true -> deploy: app started
41+
>>> [CLI] bundle deploy
42+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/lifecycle-started-omitted-[UNIQUE_NAME]/default/files...
43+
Deploying resources...
44+
✓ Deployment succeeded.
45+
Updating deployment state...
46+
Deployment complete!
47+
48+
>>> print_app_requests
49+
{
50+
"body": {},
51+
"method": "POST",
52+
"path": "/api/2.0/apps/[UNIQUE_NAME]/start"
53+
}
54+
{
55+
"body": {
56+
"mode": "SNAPSHOT",
57+
"source_code_path": "/Workspace/Users/[USERNAME]/.bundle/lifecycle-started-omitted-[UNIQUE_NAME]/default/files/app"
58+
},
59+
"method": "POST",
60+
"path": "/api/2.0/apps/[UNIQUE_NAME]/deployments"
61+
}
62+
63+
=== started: true -> (started omitted) -> deploy: no start/stop requests (compute stays as-is)
64+
>>> [CLI] bundle deploy
65+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/lifecycle-started-omitted-[UNIQUE_NAME]/default/files...
66+
Deploying resources...
67+
Updating deployment state...
68+
Deployment complete!
69+
70+
>>> print_app_requests
71+
72+
=== (started omitted, app running) -> bundle plan shows no driftPlan: no drift detected
73+
74+
>>> [CLI] bundle destroy --auto-approve
75+
The following resources will be deleted:
76+
delete resources.apps.mykey
77+
78+
All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/lifecycle-started-omitted-[UNIQUE_NAME]/default
79+
80+
Deleting files...
81+
Destroy complete!
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
envsubst < databricks.yml.tmpl > databricks.yml
2+
3+
cleanup() {
4+
trace $CLI bundle destroy --auto-approve
5+
rm -f out.requests.txt
6+
}
7+
trap cleanup EXIT
8+
9+
print_app_requests() {
10+
jq --sort-keys 'select(.method != "GET" and (.path | contains("/apps")))' < out.requests.txt
11+
rm out.requests.txt
12+
}
13+
14+
title "(started omitted) -> deploy: app created with no_compute=true, no start/stop requests"
15+
trace $CLI bundle deploy
16+
trace print_app_requests
17+
18+
title "(started omitted) -> started: false -> deploy: no start/stop requests"
19+
cat > databricks.yml <<DABSEOF
20+
bundle:
21+
name: lifecycle-started-omitted-$UNIQUE_NAME
22+
23+
resources:
24+
apps:
25+
mykey:
26+
name: $UNIQUE_NAME
27+
description: my_app_description
28+
source_code_path: ./app
29+
lifecycle:
30+
started: false
31+
DABSEOF
32+
trace $CLI bundle deploy
33+
trace print_app_requests
34+
35+
title "started: false -> (started omitted) -> deploy: no start/stop requests"
36+
cat > databricks.yml <<DABSEOF
37+
bundle:
38+
name: lifecycle-started-omitted-$UNIQUE_NAME
39+
40+
resources:
41+
apps:
42+
mykey:
43+
name: $UNIQUE_NAME
44+
description: my_app_description
45+
source_code_path: ./app
46+
DABSEOF
47+
trace $CLI bundle deploy
48+
trace print_app_requests
49+
50+
title "(started omitted) -> started: true -> deploy: app started"
51+
cat > databricks.yml <<DABSEOF
52+
bundle:
53+
name: lifecycle-started-omitted-$UNIQUE_NAME
54+
55+
resources:
56+
apps:
57+
mykey:
58+
name: $UNIQUE_NAME
59+
description: my_app_description
60+
source_code_path: ./app
61+
lifecycle:
62+
started: true
63+
DABSEOF
64+
trace $CLI bundle deploy
65+
trace print_app_requests
66+
67+
title "started: true -> (started omitted) -> deploy: no start/stop requests (compute stays as-is)"
68+
cat > databricks.yml <<DABSEOF
69+
bundle:
70+
name: lifecycle-started-omitted-$UNIQUE_NAME
71+
72+
resources:
73+
apps:
74+
mykey:
75+
name: $UNIQUE_NAME
76+
description: my_app_description
77+
source_code_path: ./app
78+
DABSEOF
79+
trace $CLI bundle deploy
80+
trace print_app_requests
81+
82+
title "(started omitted, app running) -> bundle plan shows no drift"
83+
$CLI bundle plan -o json > LOG.planjson
84+
verify_no_drift.py LOG.planjson
85+
echo "Plan: no drift detected"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Local = true
2+
Cloud = true
3+
RecordRequests = true
4+
5+
Ignore = [".databricks", "databricks.yml"]
6+
7+
[EnvMatrix]
8+
DATABRICKS_BUNDLE_ENGINE = ["direct"]

bundle/direct/dresources/app.go

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ type AppState struct {
3333
Lifecycle *AppStateLifecycle `json:"lifecycle,omitempty"`
3434
}
3535

36-
// AppRemote extends apps.App with lifecycle.started so that it appears in
37-
// RemoteType and can be used for $resource resolution.
36+
// AppRemote extends apps.App with lifecycle.started and deployment fields so
37+
// that they appear in RemoteType and can be used for $resource resolution and drift detection.
3838
type AppRemote struct {
3939
apps.App
40-
Lifecycle *AppStateLifecycle `json:"lifecycle,omitempty"`
40+
Config *resources.AppConfig `json:"config,omitempty"`
41+
GitSource *apps.GitSource `json:"git_source,omitempty"`
42+
Lifecycle *AppStateLifecycle `json:"lifecycle,omitempty"`
4143
}
4244

4345
// Custom marshalers needed because embedded apps.App has its own MarshalJSON
@@ -81,16 +83,17 @@ func (*ResourceApp) PrepareState(input *resources.App) *AppState {
8183
}
8284

8385
// RemapState maps the remote AppRemote to AppState for diff comparison.
84-
// Deploy-only fields (SourceCodePath, Config, GitSource) are not in remote state,
85-
// so they default to zero values, which prevents false drift detection.
86+
// Config and GitSource are populated from the active deployment when one exists,
87+
// enabling drift detection for out-of-band redeploys.
88+
// SourceCodePath is not tracked for drift (it's a local-only deployment path).
8689
// Started is derived from compute status so the planner can detect start/stop changes.
8790
func (*ResourceApp) RemapState(remote *AppRemote) *AppState {
8891
started := !isComputeStopped(&remote.App)
8992
return &AppState{
9093
App: remote.App,
9194
SourceCodePath: "",
92-
Config: nil,
93-
GitSource: nil,
95+
Config: remote.Config,
96+
GitSource: remote.GitSource,
9497
Lifecycle: &AppStateLifecycle{Started: &started},
9598
}
9699
}
@@ -101,7 +104,17 @@ func (r *ResourceApp) DoRead(ctx context.Context, id string) (*AppRemote, error)
101104
return nil, err
102105
}
103106
started := !isComputeStopped(app)
104-
return &AppRemote{App: *app, Lifecycle: &AppStateLifecycle{Started: &started}}, nil
107+
remote := &AppRemote{
108+
App: *app,
109+
Config: nil,
110+
GitSource: nil,
111+
Lifecycle: &AppStateLifecycle{Started: &started},
112+
}
113+
if app.ActiveDeployment != nil {
114+
remote.GitSource = app.ActiveDeployment.GitSource
115+
remote.Config = deploymentToAppConfig(app.ActiveDeployment)
116+
}
117+
return remote, nil
105118
}
106119

107120
func (r *ResourceApp) DoCreate(ctx context.Context, config *AppState) (string, *AppRemote, error) {
@@ -173,28 +186,27 @@ func (r *ResourceApp) DoUpdate(ctx context.Context, id string, config *AppState,
173186
}
174187

175188
if response.Status.State != apps.AppUpdateUpdateStatusUpdateStateSucceeded {
176-
return nil, fmt.Errorf("failed to update app %s: %s", id, response.Status.Message)
189+
return nil, fmt.Errorf("update status: %s: %s", response.Status.State, response.Status.Message)
177190
}
178191
}
179192

180193
if config.Lifecycle == nil || config.Lifecycle.Started == nil {
181194
return nil, nil
182195
}
183196

184-
// The planner computes the remote started value in RemapState based on compute status,
185-
// so changes["lifecycle.started"].Action == Update means the compute state differs from the desired state.
186-
startedChange := entry.Changes["lifecycle.started"]
197+
desiredStarted := *config.Lifecycle.Started
198+
remoteStarted := remoteIsStarted(entry)
187199

188-
if *config.Lifecycle.Started {
200+
if desiredStarted {
189201
// lifecycle.started=true: ensure the app compute is running and deploy the latest code.
190-
if startedChange != nil && startedChange.Action == deployplan.Update {
202+
if !remoteStarted {
191203
startWaiter, err := r.client.Apps.Start(ctx, apps.StartAppRequest{Name: id})
192204
if err != nil {
193-
return nil, fmt.Errorf("failed to start app %s: %w", id, err)
205+
return nil, err
194206
}
195207
startedApp, err := startWaiter.Get()
196208
if err != nil {
197-
return nil, fmt.Errorf("failed to wait for app %s to start: %w", id, err)
209+
return nil, err
198210
}
199211
if err := appdeploy.WaitForDeploymentToComplete(ctx, r.client, startedApp); err != nil {
200212
return nil, err
@@ -206,13 +218,13 @@ func (r *ResourceApp) DoUpdate(ctx context.Context, id string, config *AppState,
206218
}
207219
} else {
208220
// lifecycle.started=false: ensure the app compute is stopped.
209-
if startedChange != nil && startedChange.Action == deployplan.Update {
221+
if remoteStarted {
210222
stopWaiter, err := r.client.Apps.Stop(ctx, apps.StopAppRequest{Name: id})
211223
if err != nil {
212-
return nil, fmt.Errorf("failed to stop app %s: %w", id, err)
224+
return nil, err
213225
}
214226
if _, err = stopWaiter.Get(); err != nil {
215-
return nil, fmt.Errorf("failed to wait for app %s to stop: %w", id, err)
227+
return nil, err
216228
}
217229
}
218230
}
@@ -236,6 +248,41 @@ func isComputeStopped(app *apps.App) bool {
236248
app.ComputeStatus.State == apps.ComputeStateError
237249
}
238250

251+
// remoteIsStarted reads the compute started state from the plan entry's remote state.
252+
func remoteIsStarted(entry *PlanEntry) bool {
253+
if entry.RemoteState == nil {
254+
return false
255+
}
256+
remote, ok := entry.RemoteState.(*AppRemote)
257+
if !ok || remote.Lifecycle == nil || remote.Lifecycle.Started == nil {
258+
return false
259+
}
260+
return *remote.Lifecycle.Started
261+
}
262+
263+
// deploymentToAppConfig extracts an AppConfig from an active deployment.
264+
// Returns nil if the deployment has no command or env vars.
265+
func deploymentToAppConfig(d *apps.AppDeployment) *resources.AppConfig {
266+
if len(d.Command) == 0 && len(d.EnvVars) == 0 {
267+
return nil
268+
}
269+
config := &resources.AppConfig{}
270+
if len(d.Command) > 0 {
271+
config.Command = d.Command
272+
}
273+
if len(d.EnvVars) > 0 {
274+
config.Env = make([]resources.AppEnvVar, len(d.EnvVars))
275+
for i, ev := range d.EnvVars {
276+
config.Env[i] = resources.AppEnvVar{
277+
Name: ev.Name,
278+
Value: ev.Value,
279+
ValueFrom: ev.ValueFrom,
280+
}
281+
}
282+
}
283+
return config
284+
}
285+
239286
func (r *ResourceApp) DoDelete(ctx context.Context, id string) error {
240287
_, err := r.client.Apps.DeleteByName(ctx, id)
241288
return err
@@ -274,5 +321,10 @@ func (r *ResourceApp) waitForApp(ctx context.Context, w *databricks.WorkspaceCli
274321
return nil, err
275322
}
276323
started := !isComputeStopped(app)
277-
return &AppRemote{App: *app, Lifecycle: &AppStateLifecycle{Started: &started}}, nil
324+
remote := &AppRemote{App: *app, Lifecycle: &AppStateLifecycle{Started: &started}}
325+
if app.ActiveDeployment != nil {
326+
remote.GitSource = app.ActiveDeployment.GitSource
327+
remote.Config = deploymentToAppConfig(app.ActiveDeployment)
328+
}
329+
return remote, nil
278330
}

0 commit comments

Comments
 (0)