@@ -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 .
3838type 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.
8790func (* 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
107120func (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+
239286func (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