diff --git a/acceptance/bundle/generate/app_not_yet_deployed/output.txt b/acceptance/bundle/generate/app_not_yet_deployed/output.txt index 2ebe86fd89..b6a68104e5 100644 --- a/acceptance/bundle/generate/app_not_yet_deployed/output.txt +++ b/acceptance/bundle/generate/app_not_yet_deployed/output.txt @@ -1,5 +1,5 @@ ->>> [CLI] apps create my-app +>>> [CLI] apps create my-app --no-compute --no-wait { "app_status": { "message":"Application is running.", @@ -7,8 +7,8 @@ }, "compute_size":"MEDIUM", "compute_status": { - "message":"App compute is active.", - "state":"ACTIVE" + "message":"App compute is stopped.", + "state":"STOPPED" }, "id":"1000", "name":"my-app", diff --git a/acceptance/bundle/generate/app_not_yet_deployed/script b/acceptance/bundle/generate/app_not_yet_deployed/script index f9521c5717..8883d05d15 100644 --- a/acceptance/bundle/generate/app_not_yet_deployed/script +++ b/acceptance/bundle/generate/app_not_yet_deployed/script @@ -1,2 +1,2 @@ -trace $CLI apps create my-app +trace $CLI apps create my-app --no-compute --no-wait trace $CLI bundle generate app --existing-app-name my-app --config-dir . --key out diff --git a/acceptance/bundle/resources/apps/config-drift/out.plan.direct.json b/acceptance/bundle/resources/apps/config-drift/out.plan.direct.json index fe52fca587..0fe76b5ecf 100644 --- a/acceptance/bundle/resources/apps/config-drift/out.plan.direct.json +++ b/acceptance/bundle/resources/apps/config-drift/out.plan.direct.json @@ -1,53 +1,5 @@ +{} { - "active_deployment": { - "action": "skip", - "reason": "spec:output_only", - "remote": { - "command": [ - "streamlit", - "run", - "dashboard.py" - ], - "deployment_id": "deploy-[NUMID]", - "env_vars": [ - { - "name": "MY_VAR", - "value": "changed_value" - }, - { - "name": "NEW_VAR", - "value": "new_value" - } - ], - "mode": "SNAPSHOT", - "source_code_path": "./app", - "status": { - "message": "Deployment succeeded", - "state": "SUCCEEDED" - } - } - }, - "app_status": { - "action": "skip", - "reason": "spec:output_only", - "remote": { - "message": "Application is running.", - "state": "RUNNING" - } - }, - "compute_size": { - "action": "skip", - "reason": "backend_default", - "remote": "MEDIUM" - }, - "compute_status": { - "action": "skip", - "reason": "spec:output_only", - "remote": { - "message": "App compute is active.", - "state": "ACTIVE" - } - }, "config.command": { "action": "update", "old": [ @@ -88,41 +40,6 @@ "value": "new_value" } ] - }, - "default_source_code_path": { - "action": "skip", - "reason": "spec:output_only", - "remote": "./app" - }, - "id": { - "action": "skip", - "reason": "spec:output_only", - "remote": "1000" - }, - "service_principal_client_id": { - "action": "skip", - "reason": "spec:output_only", - "remote": "[UUID]" - }, - "service_principal_id": { - "action": "skip", - "reason": "spec:output_only", - "remote": [NUMID] - }, - "service_principal_name": { - "action": "skip", - "reason": "spec:output_only", - "remote": "app-[UNIQUE_NAME]" - }, - "source_code_path": { - "action": "update", - "old": "/Workspace/Users/[USERNAME]/.bundle/config-drift-[UNIQUE_NAME]/default/files/app", - "new": "/Workspace/Users/[USERNAME]/.bundle/config-drift-[UNIQUE_NAME]/default/files/app", - "remote": "./app" - }, - "url": { - "action": "skip", - "reason": "spec:output_only", - "remote": "[UNIQUE_NAME]-123.cloud.databricksapps.com" } } +{} diff --git a/acceptance/bundle/resources/apps/config-drift/out.test.toml b/acceptance/bundle/resources/apps/config-drift/out.test.toml index 19b2c349a3..54146af564 100644 --- a/acceptance/bundle/resources/apps/config-drift/out.test.toml +++ b/acceptance/bundle/resources/apps/config-drift/out.test.toml @@ -1,5 +1,5 @@ Local = true -Cloud = true +Cloud = false [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/apps/config-drift/output.txt b/acceptance/bundle/resources/apps/config-drift/output.txt index 6a82393fa9..6ad4310b31 100644 --- a/acceptance/bundle/resources/apps/config-drift/output.txt +++ b/acceptance/bundle/resources/apps/config-drift/output.txt @@ -1,12 +1,4 @@ -=== First deploy: creates app ->>> [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/config-drift-[UNIQUE_NAME]/default/files... -Deploying resources... -Updating deployment state... -Deployment complete! - -=== Second deploy: pushes code with config >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/config-drift-[UNIQUE_NAME]/default/files... Deploying resources... @@ -14,8 +6,9 @@ Updating deployment state... Deployment complete! === Verify no drift after deploy ->>> [CLI] bundle plan -Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged +>>> [CLI] bundle plan -o json + +>>> [CLI] apps get [UNIQUE_NAME] --output json === Simulate out-of-band deployment with changed command and env === Plan should detect config drift @@ -32,15 +25,7 @@ Updating deployment state... Deployment complete! === Verify no drift after fix ->>> [CLI] bundle plan -Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged - -=== Simulate out-of-band deployment with git_source added -=== Plan should detect git_source drift ->>> [CLI] bundle plan -update apps.myapp - -Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged +>>> [CLI] bundle plan -o json >>> [CLI] bundle destroy --auto-approve The following resources will be deleted: diff --git a/acceptance/bundle/resources/apps/config-drift/script b/acceptance/bundle/resources/apps/config-drift/script index e37ac80fde..4728255dc8 100644 --- a/acceptance/bundle/resources/apps/config-drift/script +++ b/acceptance/bundle/resources/apps/config-drift/script @@ -6,18 +6,16 @@ cleanup() { } trap cleanup EXIT -title "First deploy: creates app" -trace $CLI bundle deploy - -title "Second deploy: pushes code with config" trace $CLI bundle deploy title "Verify no drift after deploy" -trace $CLI bundle plan +trace $CLI bundle plan -o json | jq '.plan."resources.apps.myapp".changes.config // .plan."resources.apps.myapp".changes' | jq 'del(.[] | select(.action == "skip"))' > out.plan.direct.json + +SOURCE_CODE_PATH=$(trace $CLI apps get $UNIQUE_NAME --output json | jq -r '.active_deployment.source_code_path') title "Simulate out-of-band deployment with changed command and env" $CLI apps deploy $UNIQUE_NAME --no-wait --json '{ - "source_code_path": "./app", + "source_code_path": "'$SOURCE_CODE_PATH'", "mode": "SNAPSHOT", "command": ["streamlit", "run", "dashboard.py"], "env_vars": [ @@ -28,22 +26,14 @@ $CLI apps deploy $UNIQUE_NAME --no-wait --json '{ title "Plan should detect config drift" trace $CLI bundle plan -$CLI bundle plan -o json | jq '.plan."resources.apps.myapp".changes.config // .plan."resources.apps.myapp".changes' > out.plan.direct.json +# Skip entries with action "skip" +$CLI bundle plan -o json | jq '.plan."resources.apps.myapp".changes.config // .plan."resources.apps.myapp".changes' | jq 'del(.[] | select(.action == "skip"))' >> out.plan.direct.json title "Redeploy to fix drift" trace $CLI bundle deploy title "Verify no drift after fix" -trace $CLI bundle plan +trace $CLI bundle plan -o json | jq '.plan."resources.apps.myapp".changes.config // .plan."resources.apps.myapp".changes' | jq 'del(.[] | select(.action == "skip"))' >> out.plan.direct.json -title "Simulate out-of-band deployment with git_source added" -$CLI apps deploy $UNIQUE_NAME --no-wait --json '{ - "source_code_path": "./app", - "mode": "SNAPSHOT", - "git_source": {"branch": "feature-branch"}, - "command": ["python", "app.py"], - "env_vars": [{"name": "MY_VAR", "value": "original_value"}] - }' > /dev/null - -title "Plan should detect git_source drift" -trace $CLI bundle plan +# TODO: add test for git_source drift when git_source is supported in the Deploy API +# Currently it fails with the error: Git source reference is required diff --git a/acceptance/bundle/resources/apps/config-drift/test.toml b/acceptance/bundle/resources/apps/config-drift/test.toml index b5c148642a..bf01b60b18 100644 --- a/acceptance/bundle/resources/apps/config-drift/test.toml +++ b/acceptance/bundle/resources/apps/config-drift/test.toml @@ -1,5 +1,5 @@ Local = true -Cloud = true +Cloud = false # This currently fails on Cloud due to incorrect API behaviour. RecordRequests = true Ignore = [".databricks", "databricks.yml"] diff --git a/acceptance/bundle/resources/apps/create_already_exists/output.txt b/acceptance/bundle/resources/apps/create_already_exists/output.txt index 63c0b4a245..bac47c04f9 100644 --- a/acceptance/bundle/resources/apps/create_already_exists/output.txt +++ b/acceptance/bundle/resources/apps/create_already_exists/output.txt @@ -1,6 +1,14 @@ >>> [CLI] apps create test-app-already-exists { + "active_deployment": { + "deployment_id":"deploy-[NUMID]", + "source_code_path":"/Workspace/Users/[USERNAME]/test-app-already-exists", + "status": { + "message":"Deployment succeeded", + "state":"SUCCEEDED" + } + }, "app_status": { "message":"Application is running.", "state":"RUNNING" @@ -10,6 +18,7 @@ "message":"App compute is active.", "state":"ACTIVE" }, + "default_source_code_path":"/Workspace/Users/[USERNAME]/test-app-already-exists", "id":"1000", "name":"test-app-already-exists", "service_principal_client_id":"[UUID]", diff --git a/acceptance/bundle/resources/apps/lifecycle-started/output.txt b/acceptance/bundle/resources/apps/lifecycle-started/output.txt index 68e56a6814..cfe10a2d65 100644 --- a/acceptance/bundle/resources/apps/lifecycle-started/output.txt +++ b/acceptance/bundle/resources/apps/lifecycle-started/output.txt @@ -15,6 +15,14 @@ Deployment complete! "name": "[UNIQUE_NAME]" } } +{ + "method": "POST", + "path": "/api/2.0/apps/[UNIQUE_NAME]/deployments", + "body": { + "mode": "SNAPSHOT", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/lifecycle-started-[UNIQUE_NAME]/default/files/app" + } +} >>> errcode [CLI] apps get [UNIQUE_NAME] "ACTIVE" diff --git a/acceptance/cmd/workspace/apps/output.txt b/acceptance/cmd/workspace/apps/output.txt index ada0e6407f..e722afbe86 100644 --- a/acceptance/cmd/workspace/apps/output.txt +++ b/acceptance/cmd/workspace/apps/output.txt @@ -2,6 +2,14 @@ === Apps create with correct input >>> [CLI] apps create --json @input.json { + "active_deployment": { + "deployment_id":"deploy-[NUMID]", + "source_code_path":"/Workspace/Users/[USERNAME]/test-name", + "status": { + "message":"Deployment succeeded", + "state":"SUCCEEDED" + } + }, "app_status": { "message":"Application is running.", "state":"RUNNING" @@ -11,6 +19,7 @@ "message":"App compute is active.", "state":"ACTIVE" }, + "default_source_code_path":"/Workspace/Users/[USERNAME]/test-name", "description":"My app description.", "id":"1000", "name":"test-name", @@ -34,6 +43,14 @@ === Apps update with correct input >>> [CLI] apps update test-name --json @input.json { + "active_deployment": { + "deployment_id":"deploy-[NUMID]", + "source_code_path":"/Workspace/Users/[USERNAME]/test-name", + "status": { + "message":"Deployment succeeded", + "state":"SUCCEEDED" + } + }, "app_status": { "message":"Application is running.", "state":"RUNNING" @@ -43,6 +60,7 @@ "message":"App compute is active.", "state":"ACTIVE" }, + "default_source_code_path":"/Workspace/Users/[USERNAME]/test-name", "description":"My app description.", "id":"1001", "name":"test-name", diff --git a/bundle/appdeploy/app.go b/bundle/appdeploy/app.go index 6bea74fac3..4f590be60a 100644 --- a/bundle/appdeploy/app.go +++ b/bundle/appdeploy/app.go @@ -20,6 +20,10 @@ func logProgress(ctx context.Context, msg string) { // BuildDeployment constructs an AppDeployment from the app's source code path, inline config and git source. func BuildDeployment(sourcePath string, config *resources.AppConfig, gitSource *sdkapps.GitSource) sdkapps.AppDeployment { + // GitRepository is not supported in the Deploy API, only as part of Create, so we need to remove it. + if gitSource != nil { + gitSource.GitRepository = nil + } deployment := sdkapps.AppDeployment{ Mode: sdkapps.AppDeploymentModeSnapshot, SourceCodePath: sourcePath, diff --git a/bundle/direct/dresources/app.go b/bundle/direct/dresources/app.go index 76a0881f9e..5f88217061 100644 --- a/bundle/direct/dresources/app.go +++ b/bundle/direct/dresources/app.go @@ -199,46 +199,48 @@ func (r *ResourceApp) DoUpdate(ctx context.Context, id string, config *AppState, } } + return nil, r.manageLifecycle(ctx, id, config, remoteIsStarted(entry)) +} + +func (r *ResourceApp) manageLifecycle(ctx context.Context, id string, config *AppState, alreadyStarted bool) error { if config.Lifecycle == nil || config.Lifecycle.Started == nil { - return nil, nil + return nil } desiredStarted := *config.Lifecycle.Started - remoteStarted := remoteIsStarted(entry) - if desiredStarted { // lifecycle.started=true: ensure the app compute is running and deploy the latest code. - if !remoteStarted { + if !alreadyStarted { startWaiter, err := r.client.Apps.Start(ctx, apps.StartAppRequest{Name: id}) if err != nil { - return nil, err + return err } startedApp, err := startWaiter.Get() if err != nil { - return nil, err + return err } if err := appdeploy.WaitForDeploymentToComplete(ctx, r.client, startedApp); err != nil { - return nil, err + return err } } deployment := appdeploy.BuildDeployment(config.SourceCodePath, config.Config, config.GitSource) if err := appdeploy.Deploy(ctx, r.client, id, deployment); err != nil { - return nil, err + return err } } else { // lifecycle.started=false: ensure the app compute is stopped. - if remoteStarted { + if alreadyStarted { stopWaiter, err := r.client.Apps.Stop(ctx, apps.StopAppRequest{Name: id}) if err != nil { - return nil, err + return err } if _, err = stopWaiter.Get(); err != nil { - return nil, err + return err } } } - return nil, nil + return nil } // deployOnlyFields are AppState fields managed via the Deploy API, not the App Update API. @@ -266,7 +268,7 @@ func hasAppChanges(entry *PlanEntry) bool { // OverrideChangeDesc skips source_code_path drift when the remote value is empty. // This happens when an app has no deployment yet (DefaultSourceCodePath is unset). func (*ResourceApp) OverrideChangeDesc(_ context.Context, path *structpath.PathNode, change *ChangeDesc, remote *AppRemote) error { - if path.String() == "source_code_path" && remote.SourceCodePath == "" { + if path.String() == "source_code_path" && (remote.SourceCodePath == "" || remote.SourceCodePath == "null") { change.Action = deployplan.Skip change.Reason = "no deployment" } @@ -320,7 +322,15 @@ func (r *ResourceApp) DoDelete(ctx context.Context, id string) error { } func (r *ResourceApp) WaitAfterCreate(ctx context.Context, config *AppState) (*AppRemote, error) { - return r.waitForApp(ctx, r.client, config.Name) + remote, err := r.waitForApp(ctx, r.client, config.Name) + if err != nil { + return nil, err + } + alreadyStarted := remote.Lifecycle != nil && remote.Lifecycle.Started != nil && *remote.Lifecycle.Started + if err := r.manageLifecycle(ctx, config.Name, config, alreadyStarted); err != nil { + return nil, err + } + return remote, nil } // waitForApp waits for the app to reach the target state. The target state is either ACTIVE or STOPPED. diff --git a/libs/testserver/apps.go b/libs/testserver/apps.go index e3726c650d..b35632e27a 100644 --- a/libs/testserver/apps.go +++ b/libs/testserver/apps.go @@ -221,6 +221,20 @@ func (s *FakeWorkspace) AppsUpsert(req Request, name string) Response { State: "ACTIVE", Message: "App compute is active.", } + + // Simulate the apps platform side effect: when an app is created, it is deployed with the default source code path. + deployment := apps.AppDeployment{ + SourceCodePath: "/Workspace/Users/tester@databricks.com/" + name, + } + + deployment.DeploymentId = fmt.Sprintf("deploy-%d", nextID()) + deployment.Status = &apps.AppDeploymentStatus{ + State: apps.AppDeploymentStateSucceeded, + Message: "Deployment succeeded", + } + + app.ActiveDeployment = &deployment + app.DefaultSourceCodePath = deployment.SourceCodePath } app.Url = name + "-123.cloud.databricksapps.com"