diff --git a/.nextchanges/bundles/app-source-code-path.md b/.nextchanges/bundles/app-source-code-path.md new file mode 100644 index 00000000000..4872fc6c021 --- /dev/null +++ b/.nextchanges/bundles/app-source-code-path.md @@ -0,0 +1 @@ +For apps deployed with the direct engine, `source_code_path` and `git_source` are now also sent as part of the Create/Update App request (in addition to the app Deploy API), so they are recorded on the app and drift-detected. The Terraform engine is unchanged (its `databricks_app` resource cannot manage these input-only fields). diff --git a/acceptance/bundle/empty_string_dropped/out.requests.direct.json b/acceptance/bundle/empty_string_dropped/out.requests.direct.json index c9bba29b9fa..54d8e44d45a 100644 --- a/acceptance/bundle/empty_string_dropped/out.requests.direct.json +++ b/acceptance/bundle/empty_string_dropped/out.requests.direct.json @@ -6,7 +6,8 @@ }, "body": { "description": "", - "name": "test-app-direct" + "name": "test-app-direct", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/direct/files/app" } } { diff --git a/acceptance/bundle/resource_deps/remote_app_url/out.requests.direct.json b/acceptance/bundle/resource_deps/remote_app_url/out.requests.direct.json new file mode 100644 index 00000000000..92997feef74 --- /dev/null +++ b/acceptance/bundle/resource_deps/remote_app_url/out.requests.direct.json @@ -0,0 +1,54 @@ +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state" + } +} +{ + "method": "POST", + "path": "/api/2.0/workspace/delete", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal", + "recursive": true + } +} +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal" + } +} +{ + "method": "POST", + "path": "/api/2.0/apps", + "q": { + "no_compute": "true" + }, + "body": { + "description": "best app", + "name": "myapp", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files/app" + } +} +{ + "method": "POST", + "path": "/api/2.0/pipelines", + "body": { + "channel": "CURRENT", + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" + }, + "edition": "ADVANCED", + "name": "app url is myapp-123.cloud.databricksapps.com" + } +} +{ + "method": "POST", + "path": "/api/2.0/workspace/delete", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/deploy.lock" + } +} diff --git a/acceptance/bundle/resource_deps/remote_app_url/out.requests.terraform.json b/acceptance/bundle/resource_deps/remote_app_url/out.requests.terraform.json new file mode 100644 index 00000000000..4e6eb966fe5 --- /dev/null +++ b/acceptance/bundle/resource_deps/remote_app_url/out.requests.terraform.json @@ -0,0 +1,53 @@ +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state" + } +} +{ + "method": "POST", + "path": "/api/2.0/workspace/delete", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal", + "recursive": true + } +} +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal" + } +} +{ + "method": "POST", + "path": "/api/2.0/apps", + "q": { + "no_compute": "true" + }, + "body": { + "description": "best app", + "name": "myapp" + } +} +{ + "method": "POST", + "path": "/api/2.0/pipelines", + "body": { + "channel": "CURRENT", + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" + }, + "edition": "ADVANCED", + "name": "app url is myapp-123.cloud.databricksapps.com" + } +} +{ + "method": "POST", + "path": "/api/2.0/workspace/delete", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/deploy.lock" + } +} diff --git a/acceptance/bundle/resource_deps/remote_app_url/output.txt b/acceptance/bundle/resource_deps/remote_app_url/output.txt index 9301329af12..13907cd4a82 100644 --- a/acceptance/bundle/resource_deps/remote_app_url/output.txt +++ b/acceptance/bundle/resource_deps/remote_app_url/output.txt @@ -31,59 +31,6 @@ Files: 1 uploaded, 0 deleted Resources: 2 created, 0 changed, 0 deleted, 0 unchanged >>> print_requests.py ^//import-file/ -{ - "method": "POST", - "path": "/api/2.0/workspace/mkdirs", - "body": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state" - } -} -{ - "method": "POST", - "path": "/api/2.0/workspace/delete", - "body": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal", - "recursive": true - } -} -{ - "method": "POST", - "path": "/api/2.0/workspace/mkdirs", - "body": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal" - } -} -{ - "method": "POST", - "path": "/api/2.0/apps", - "q": { - "no_compute": "true" - }, - "body": { - "description": "best app", - "name": "myapp" - } -} -{ - "method": "POST", - "path": "/api/2.0/pipelines", - "body": { - "channel": "CURRENT", - "deployment": { - "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" - }, - "edition": "ADVANCED", - "name": "app url is myapp-123.cloud.databricksapps.com" - } -} -{ - "method": "POST", - "path": "/api/2.0/workspace/delete", - "body": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/deploy.lock" - } -} >>> [CLI] bundle destroy --auto-approve The following resources will be deleted: diff --git a/acceptance/bundle/resource_deps/remote_app_url/script b/acceptance/bundle/resource_deps/remote_app_url/script index d38692366b3..6a4e2a0e316 100644 --- a/acceptance/bundle/resource_deps/remote_app_url/script +++ b/acceptance/bundle/resource_deps/remote_app_url/script @@ -3,7 +3,9 @@ trace $CLI bundle plan trace print_requests.py '^//import-file/' trace $CLI bundle deploy -trace print_requests.py '^//import-file/' +# The app create body diverges by engine (the direct engine sends source_code_path, +# the Terraform provider cannot manage it), so record these requests per engine. +trace print_requests.py '^//import-file/' > out.requests.$DATABRICKS_BUNDLE_ENGINE.json trace $CLI bundle destroy --auto-approve trace print_requests.py --sort '^//import-file/' diff --git a/acceptance/bundle/resources/apps/config-no-deployment/output.txt b/acceptance/bundle/resources/apps/config-no-deployment/output.txt index 81a1f6c8677..78f855877ca 100644 --- a/acceptance/bundle/resources/apps/config-no-deployment/output.txt +++ b/acceptance/bundle/resources/apps/config-no-deployment/output.txt @@ -6,11 +6,11 @@ Created apps.myapp Files: 0 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged -=== Plan is a no-op: config/source_code_path drift is skipped while the app has no active deployment +=== Plan is a no-op: config drift is skipped (no active deployment); source_code_path is input-only >>> [CLI] bundle plan Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged -=== Both deploy-only fields are skipped with reason "no active deployment" +=== config skipped with "no active deployment"; source_code_path skipped as input_only >>> [CLI] bundle plan -o json { "config": { @@ -43,8 +43,9 @@ Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged }, "source_code_path": { "action": "skip", - "reason": "no active deployment", + "reason": "spec:input_only", "old": "/Workspace/Users/[USERNAME]/.bundle/app-config-no-deployment/default/files/app", - "new": "/Workspace/Users/[USERNAME]/.bundle/app-config-no-deployment/default/files/app" + "new": "/Workspace/Users/[USERNAME]/.bundle/app-config-no-deployment/default/files/app", + "remote": "" } } diff --git a/acceptance/bundle/resources/apps/config-no-deployment/script b/acceptance/bundle/resources/apps/config-no-deployment/script index f1970999645..7c44942408b 100644 --- a/acceptance/bundle/resources/apps/config-no-deployment/script +++ b/acceptance/bundle/resources/apps/config-no-deployment/script @@ -3,8 +3,8 @@ echo "*" > .gitignore title "Deploy: app created with no_compute; config is not deployed until the app starts" trace $CLI bundle deploy -title "Plan is a no-op: config/source_code_path drift is skipped while the app has no active deployment" +title "Plan is a no-op: config drift is skipped (no active deployment); source_code_path is input-only" trace $CLI bundle plan | contains.py "Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged" -title "Both deploy-only fields are skipped with reason \"no active deployment\"" +title "config skipped with \"no active deployment\"; source_code_path skipped as input_only" trace $CLI bundle plan -o json | jq '.plan[].changes | {config, source_code_path}' diff --git a/acceptance/bundle/resources/apps/default_description/out.requests.direct.json b/acceptance/bundle/resources/apps/default_description/out.requests.direct.json new file mode 100644 index 00000000000..cc9779b7d46 --- /dev/null +++ b/acceptance/bundle/resources/apps/default_description/out.requests.direct.json @@ -0,0 +1,12 @@ +{ + "method": "POST", + "path": "/api/2.0/apps", + "q": { + "no_compute": "true" + }, + "body": { + "description": "", + "name": "myappname", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" + } +} diff --git a/acceptance/bundle/resources/apps/default_description/out.requests.terraform.json b/acceptance/bundle/resources/apps/default_description/out.requests.terraform.json new file mode 100644 index 00000000000..37ce34a674e --- /dev/null +++ b/acceptance/bundle/resources/apps/default_description/out.requests.terraform.json @@ -0,0 +1,11 @@ +{ + "method": "POST", + "path": "/api/2.0/apps", + "q": { + "no_compute": "true" + }, + "body": { + "description": "", + "name": "myappname" + } +} diff --git a/acceptance/bundle/resources/apps/default_description/output.txt b/acceptance/bundle/resources/apps/default_description/output.txt index 1f78fb9801d..66120057068 100644 --- a/acceptance/bundle/resources/apps/default_description/output.txt +++ b/acceptance/bundle/resources/apps/default_description/output.txt @@ -6,14 +6,3 @@ Files: 5 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged >>> print_requests.py //apps -{ - "method": "POST", - "path": "/api/2.0/apps", - "q": { - "no_compute": "true" - }, - "body": { - "description": "", - "name": "myappname" - } -} diff --git a/acceptance/bundle/resources/apps/default_description/script b/acceptance/bundle/resources/apps/default_description/script index b9d834794d3..a1e67ada078 100644 --- a/acceptance/bundle/resources/apps/default_description/script +++ b/acceptance/bundle/resources/apps/default_description/script @@ -1,2 +1,4 @@ trace $CLI bundle deploy -trace print_requests.py //apps +# The create body diverges by engine: the direct engine sends source_code_path, the +# Terraform provider cannot manage it (input_only), so record requests per engine. +trace print_requests.py //apps > out.requests.$DATABRICKS_BUNDLE_ENGINE.json diff --git a/acceptance/bundle/resources/apps/git-source-no-deployment/output.txt b/acceptance/bundle/resources/apps/git-source-no-deployment/output.txt index 7b6337e33dd..07a1cd92b86 100644 --- a/acceptance/bundle/resources/apps/git-source-no-deployment/output.txt +++ b/acceptance/bundle/resources/apps/git-source-no-deployment/output.txt @@ -1,21 +1,21 @@ -=== Deploy: app created with no_compute; git_source is not deployed until the app starts +=== Deploy: git_source is sent on Create; the app has no_compute so nothing is deployed yet >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/app-git-source-no-deployment/default/files... Created apps.myapp Files: 0 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged -=== Plan is a no-op: git_source drift is skipped while the app has no active deployment +=== Plan is a no-op: git_source is input-only, so its drift is suppressed >>> [CLI] bundle plan Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged -=== git_source is skipped with reason "no active deployment" +=== git_source is skipped as input_only >>> [CLI] bundle plan -o json { "git_source": { "action": "skip", - "reason": "no active deployment", + "reason": "spec:input_only", "old": { "branch": "main" }, diff --git a/acceptance/bundle/resources/apps/git-source-no-deployment/script b/acceptance/bundle/resources/apps/git-source-no-deployment/script index 63cce6312c3..ce89140d5a1 100644 --- a/acceptance/bundle/resources/apps/git-source-no-deployment/script +++ b/acceptance/bundle/resources/apps/git-source-no-deployment/script @@ -1,10 +1,10 @@ echo "*" > .gitignore -title "Deploy: app created with no_compute; git_source is not deployed until the app starts" +title "Deploy: git_source is sent on Create; the app has no_compute so nothing is deployed yet" trace $CLI bundle deploy -title "Plan is a no-op: git_source drift is skipped while the app has no active deployment" +title "Plan is a no-op: git_source is input-only, so its drift is suppressed" trace $CLI bundle plan | contains.py "Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged" -title "git_source is skipped with reason \"no active deployment\"" +title "git_source is skipped as input_only" trace $CLI bundle plan -o json | jq '.plan[].changes | {git_source}' diff --git a/acceptance/bundle/resources/apps/immutable/output.txt b/acceptance/bundle/resources/apps/immutable/output.txt index 4b25ae82b8b..f88893b676f 100644 --- a/acceptance/bundle/resources/apps/immutable/output.txt +++ b/acceptance/bundle/resources/apps/immutable/output.txt @@ -24,7 +24,8 @@ You can access the app at my-immutable-app-123.cloud.databricksapps.com }, "body": { "description": "", - "name": "my-immutable-app" + "name": "my-immutable-app", + "source_code_path": "/Workspace/Users/[UUID]/.snapshots/[UUID]/[SNAPSHOT_HASH]/files/app" } } { diff --git a/acceptance/bundle/resources/apps/lifecycle-started-omitted/output.txt b/acceptance/bundle/resources/apps/lifecycle-started-omitted/output.txt index abe37f235f8..874abae1905 100644 --- a/acceptance/bundle/resources/apps/lifecycle-started-omitted/output.txt +++ b/acceptance/bundle/resources/apps/lifecycle-started-omitted/output.txt @@ -15,7 +15,8 @@ Resources: 1 created, 0 changed, 0 deleted, 0 unchanged }, "body": { "description": "my_app_description", - "name": "[UNIQUE_NAME]" + "name": "[UNIQUE_NAME]", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/lifecycle-started-omitted-[UNIQUE_NAME]/default/files/app" } } diff --git a/acceptance/bundle/resources/apps/lifecycle-started-toggle/output.txt b/acceptance/bundle/resources/apps/lifecycle-started-toggle/output.txt index ef03a4541d9..d8822539be4 100644 --- a/acceptance/bundle/resources/apps/lifecycle-started-toggle/output.txt +++ b/acceptance/bundle/resources/apps/lifecycle-started-toggle/output.txt @@ -10,7 +10,8 @@ Resources: 1 created, 0 changed, 0 deleted, 0 unchanged { "body": { "description": "my_app_description", - "name": "[UNIQUE_NAME]" + "name": "[UNIQUE_NAME]", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/lifecycle-started-toggle-[UNIQUE_NAME]/default/files/app" }, "method": "POST", "path": "/api/2.0/apps", diff --git a/acceptance/bundle/resources/apps/lifecycle-started/output.txt b/acceptance/bundle/resources/apps/lifecycle-started/output.txt index 6e0d5e5d7f5..56466936d2a 100644 --- a/acceptance/bundle/resources/apps/lifecycle-started/output.txt +++ b/acceptance/bundle/resources/apps/lifecycle-started/output.txt @@ -12,7 +12,8 @@ Resources: 1 created, 0 changed, 0 deleted, 0 unchanged "path": "/api/2.0/apps", "body": { "description": "my_app_description", - "name": "[UNIQUE_NAME]" + "name": "[UNIQUE_NAME]", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/lifecycle-started-[UNIQUE_NAME]/default/files/app" } } { @@ -88,9 +89,10 @@ Resources: 0 created, 1 changed, 0 deleted, 0 unchanged "body": { "app": { "description": "MY_APP_DESCRIPTION_2", - "name": "[UNIQUE_NAME]" + "name": "[UNIQUE_NAME]", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/lifecycle-started-[UNIQUE_NAME]/default/files/app" }, - "update_mask": "description,budget_policy_id,usage_policy_id,resources,user_api_scopes,forward_user_access_token,compute_size,compute_min_instances,compute_max_instances,git_repository,telemetry_export_destinations" + "update_mask": "description,budget_policy_id,usage_policy_id,resources,user_api_scopes,forward_user_access_token,compute_size,compute_min_instances,compute_max_instances,git_repository,git_source,source_code_path,telemetry_export_destinations" } } @@ -115,9 +117,10 @@ Resources: 0 created, 1 changed, 0 deleted, 0 unchanged "body": { "app": { "description": "MY_APP_DESCRIPTION_3", - "name": "[UNIQUE_NAME]" + "name": "[UNIQUE_NAME]", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/lifecycle-started-[UNIQUE_NAME]/default/files/app" }, - "update_mask": "description,budget_policy_id,usage_policy_id,resources,user_api_scopes,forward_user_access_token,compute_size,compute_min_instances,compute_max_instances,git_repository,telemetry_export_destinations" + "update_mask": "description,budget_policy_id,usage_policy_id,resources,user_api_scopes,forward_user_access_token,compute_size,compute_min_instances,compute_max_instances,git_repository,git_source,source_code_path,telemetry_export_destinations" } } { diff --git a/acceptance/bundle/resources/apps/resource-refs/out.requests.direct.json b/acceptance/bundle/resources/apps/resource-refs/out.requests.direct.json new file mode 100644 index 00000000000..194f2e69940 --- /dev/null +++ b/acceptance/bundle/resources/apps/resource-refs/out.requests.direct.json @@ -0,0 +1,48 @@ +{ + "method": "POST", + "path": "/api/2.0/apps", + "q": { + "no_compute": "true" + }, + "body": { + "description": "A Streamlit app that uses a SQL warehouse", + "name": "data-app", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/resource-refs/default/files/app" + } +} +{ + "method": "POST", + "path": "/api/2.0/apps/data-app/start", + "body": {} +} +{ + "method": "POST", + "path": "/api/2.0/apps/data-app/deployments", + "body": { + "command": [ + "streamlit", + "run", + "app.py" + ], + "env_vars": [ + { + "name": "MY_EXAMPLE_SCHEMA", + "value": "main" + }, + { + "name": "MY_EXAMPLE_JOB", + "value": "example_job" + }, + { + "name": "MY_EXAMPLE_JOB_ID", + "value": "[NUMID]" + }, + { + "name": "MY_EXAMPLE_VAR", + "value": "example_value" + } + ], + "mode": "SNAPSHOT", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/resource-refs/default/files/app" + } +} diff --git a/acceptance/bundle/resources/apps/resource-refs/out.requests.terraform.json b/acceptance/bundle/resources/apps/resource-refs/out.requests.terraform.json new file mode 100644 index 00000000000..e6292cf773f --- /dev/null +++ b/acceptance/bundle/resources/apps/resource-refs/out.requests.terraform.json @@ -0,0 +1,47 @@ +{ + "method": "POST", + "path": "/api/2.0/apps", + "q": { + "no_compute": "true" + }, + "body": { + "description": "A Streamlit app that uses a SQL warehouse", + "name": "data-app" + } +} +{ + "method": "POST", + "path": "/api/2.0/apps/data-app/start", + "body": {} +} +{ + "method": "POST", + "path": "/api/2.0/apps/data-app/deployments", + "body": { + "command": [ + "streamlit", + "run", + "app.py" + ], + "env_vars": [ + { + "name": "MY_EXAMPLE_SCHEMA", + "value": "main" + }, + { + "name": "MY_EXAMPLE_JOB", + "value": "example_job" + }, + { + "name": "MY_EXAMPLE_JOB_ID", + "value": "[NUMID]" + }, + { + "name": "MY_EXAMPLE_VAR", + "value": "example_value" + } + ], + "mode": "SNAPSHOT", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/resource-refs/default/files/app" + } +} diff --git a/acceptance/bundle/resources/apps/resource-refs/output.txt b/acceptance/bundle/resources/apps/resource-refs/output.txt index 14fcdcb0972..6eb5eb0ca96 100644 --- a/acceptance/bundle/resources/apps/resource-refs/output.txt +++ b/acceptance/bundle/resources/apps/resource-refs/output.txt @@ -18,50 +18,3 @@ Resources: 3 created, 0 changed, 0 deleted, 0 unchanged You can access the app at data-app-123.cloud.databricksapps.com >>> print_requests.py //apps -{ - "method": "POST", - "path": "/api/2.0/apps", - "q": { - "no_compute": "true" - }, - "body": { - "description": "A Streamlit app that uses a SQL warehouse", - "name": "data-app" - } -} -{ - "method": "POST", - "path": "/api/2.0/apps/data-app/start", - "body": {} -} -{ - "method": "POST", - "path": "/api/2.0/apps/data-app/deployments", - "body": { - "command": [ - "streamlit", - "run", - "app.py" - ], - "env_vars": [ - { - "name": "MY_EXAMPLE_SCHEMA", - "value": "main" - }, - { - "name": "MY_EXAMPLE_JOB", - "value": "example_job" - }, - { - "name": "MY_EXAMPLE_JOB_ID", - "value": "[NUMID]" - }, - { - "name": "MY_EXAMPLE_VAR", - "value": "example_value" - } - ], - "mode": "SNAPSHOT", - "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/resource-refs/default/files/app" - } -} diff --git a/acceptance/bundle/resources/apps/resource-refs/script b/acceptance/bundle/resources/apps/resource-refs/script index ace337470da..1d023779d65 100644 --- a/acceptance/bundle/resources/apps/resource-refs/script +++ b/acceptance/bundle/resources/apps/resource-refs/script @@ -1,3 +1,5 @@ trace $CLI bundle deploy trace $CLI bundle run data_app -trace print_requests.py //apps +# The create body diverges by engine: the direct engine sends source_code_path, the +# Terraform provider cannot manage it (input_only), so record requests per engine. +trace print_requests.py //apps > out.requests.$DATABRICKS_BUNDLE_ENGINE.json diff --git a/acceptance/bundle/resources/apps/source-code-path-update/app/app.py b/acceptance/bundle/resources/apps/source-code-path-update/app/app.py new file mode 100644 index 00000000000..c471b2f5ecf --- /dev/null +++ b/acceptance/bundle/resources/apps/source-code-path-update/app/app.py @@ -0,0 +1 @@ +print("v1") diff --git a/acceptance/bundle/resources/apps/source-code-path-update/app2/app.py b/acceptance/bundle/resources/apps/source-code-path-update/app2/app.py new file mode 100644 index 00000000000..3d6edeb28df --- /dev/null +++ b/acceptance/bundle/resources/apps/source-code-path-update/app2/app.py @@ -0,0 +1 @@ +print("v2") diff --git a/acceptance/bundle/resources/apps/source-code-path-update/databricks.yml b/acceptance/bundle/resources/apps/source-code-path-update/databricks.yml new file mode 100644 index 00000000000..7c707baa0a4 --- /dev/null +++ b/acceptance/bundle/resources/apps/source-code-path-update/databricks.yml @@ -0,0 +1,9 @@ +bundle: + name: app-source-code-path-update + +resources: + apps: + myapp: + name: test-app-source-code-path-update + description: my_app_description + source_code_path: ./app diff --git a/acceptance/bundle/resources/apps/source-code-path-update/out.test.toml b/acceptance/bundle/resources/apps/source-code-path-update/out.test.toml new file mode 100644 index 00000000000..0938e678987 --- /dev/null +++ b/acceptance/bundle/resources/apps/source-code-path-update/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/apps/source-code-path-update/output.txt b/acceptance/bundle/resources/apps/source-code-path-update/output.txt new file mode 100644 index 00000000000..4377365da60 --- /dev/null +++ b/acceptance/bundle/resources/apps/source-code-path-update/output.txt @@ -0,0 +1,50 @@ + +=== Deploy: source_code_path is sent on Create +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/app-source-code-path-update/default/files... +Created apps.myapp +Files: 7 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +>>> print_requests.py //apps +{ + "method": "POST", + "path": "/api/2.0/apps", + "q": { + "no_compute": "true" + }, + "body": { + "description": "my_app_description", + "name": "test-app-source-code-path-update", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/app-source-code-path-update/default/files/app" + } +} + +=== Change source_code_path: plan detects an update +>>> update_file.py databricks.yml source_code_path: ./app source_code_path: ./app2 + +>>> [CLI] bundle plan +update apps.myapp + +Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged + +=== Redeploy: App Update sends the new source_code_path in the update mask +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/app-source-code-path-update/default/files... +Updated apps.myapp +Files: 3 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged + +>>> print_requests.py //apps +{ + "method": "POST", + "path": "/api/2.0/apps/test-app-source-code-path-update/update", + "body": { + "app": { + "description": "my_app_description", + "name": "test-app-source-code-path-update", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/app-source-code-path-update/default/files/app2" + }, + "update_mask": "description,budget_policy_id,usage_policy_id,resources,user_api_scopes,forward_user_access_token,compute_size,compute_min_instances,compute_max_instances,git_repository,git_source,source_code_path,telemetry_export_destinations" + } +} diff --git a/acceptance/bundle/resources/apps/source-code-path-update/script b/acceptance/bundle/resources/apps/source-code-path-update/script new file mode 100644 index 00000000000..9766c0933b9 --- /dev/null +++ b/acceptance/bundle/resources/apps/source-code-path-update/script @@ -0,0 +1,11 @@ +title "Deploy: source_code_path is sent on Create" +trace $CLI bundle deploy +trace print_requests.py //apps + +title "Change source_code_path: plan detects an update" +trace update_file.py databricks.yml "source_code_path: ./app" "source_code_path: ./app2" +trace $CLI bundle plan + +title "Redeploy: App Update sends the new source_code_path in the update mask" +trace $CLI bundle deploy +trace print_requests.py //apps diff --git a/acceptance/bundle/resources/apps/source-code-path-update/test.toml b/acceptance/bundle/resources/apps/source-code-path-update/test.toml new file mode 100644 index 00000000000..bebd818be7b --- /dev/null +++ b/acceptance/bundle/resources/apps/source-code-path-update/test.toml @@ -0,0 +1,7 @@ +Cloud = false + +Ignore = [".databricks"] + +# source_code_path is managed via Create/Update App, which only the direct engine +# supports (the Terraform databricks_app resource cannot manage this input_only field). +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/apps/update/out.requests.direct.json b/acceptance/bundle/resources/apps/update/out.requests.direct.json index faf9c2bbca4..8ab3c9fb7df 100644 --- a/acceptance/bundle/resources/apps/update/out.requests.direct.json +++ b/acceptance/bundle/resources/apps/update/out.requests.direct.json @@ -6,7 +6,8 @@ }, "body": { "description": "my_app_description", - "name": "myappname" + "name": "myappname", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files/app" } } { @@ -15,9 +16,10 @@ "body": { "app": { "description": "MY_APP_DESCRIPTION", - "name": "myappname" + "name": "myappname", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files/app" }, - "update_mask": "description,budget_policy_id,usage_policy_id,resources,user_api_scopes,forward_user_access_token,compute_size,compute_min_instances,compute_max_instances,git_repository,telemetry_export_destinations" + "update_mask": "description,budget_policy_id,usage_policy_id,resources,user_api_scopes,forward_user_access_token,compute_size,compute_min_instances,compute_max_instances,git_repository,git_source,source_code_path,telemetry_export_destinations" } } { @@ -32,7 +34,8 @@ }, "body": { "description": "MY_APP_DESCRIPTION", - "name": "mynewappname" + "name": "mynewappname", + "source_code_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files/app" } } { diff --git a/bundle/config/resources/apps.go b/bundle/config/resources/apps.go index a26670c4897..20442755980 100644 --- a/bundle/config/resources/apps.go +++ b/bundle/config/resources/apps.go @@ -41,10 +41,12 @@ type App struct { // Lifecycle shadows BaseResource.Lifecycle to add support for lifecycle.started. Lifecycle *LifecycleWithStarted `json:"lifecycle,omitempty"` - // SourceCodePath and GitSource come from the embedded apps.App. DABs treats them as - // deploy-only: source_code_path points at app source on local disk and is translated to - // its workspace path, and both are passed to the Deploy API rather than sent as resource - // attributes. Declaring them here too would duplicate their json tags and break diffing. + // SourceCodePath and GitSource come from the embedded apps.App and are sent as part of + // the Create/Update App request so they are recorded on the app. source_code_path points + // at app source on local disk and is translated to its workspace path. The API accepts + // both on write but does not echo them on read (input_only), so drift is suppressed via + // ignore_remote_changes. They are also passed to the Deploy API, which requires a + // deployment source. Declaring them here too would duplicate their json tags and break diffing. // Config represents inline app.yaml configuration for the app. // When specified, this configuration is written to an app.yaml file in the source code path during deployment. diff --git a/bundle/deploy/terraform/tfdyn/convert_app.go b/bundle/deploy/terraform/tfdyn/convert_app.go index edc6073a89d..ed9d02a1bce 100644 --- a/bundle/deploy/terraform/tfdyn/convert_app.go +++ b/bundle/deploy/terraform/tfdyn/convert_app.go @@ -27,10 +27,11 @@ func convertAppResource(ctx context.Context, vin dyn.Value) (dyn.Value, error) { log.Debugf(ctx, "app normalization diagnostic: %s", diag.Summary) } - // SDK v0.175 added source_code_path, git_source, and default_git_source to - // apps.App. These are deploy-only fields DABs handles via source upload (see - // bundle/direct/dresources/app.go), not databricks_app resource attributes, so - // keep them out of the Terraform state. + // SDK v0.175 added source_code_path, git_source, and default_git_source to apps.App. + // The databricks_app resource cannot manage source_code_path/git_source: the API accepts + // them on write but does not echo them on read (input_only), so the provider fails with + // "inconsistent result after apply" (the applied value reads back empty). DABs deploys + // them via the Deploy API instead (bundle run), so keep all three out of the TF state. vout, err := dyn.DropKeys(vout, []string{"source_code_path", "git_source", "default_git_source"}) if err != nil { return vout, err diff --git a/bundle/direct/dresources/app.go b/bundle/direct/dresources/app.go index c7108f87997..d9fd0a5f381 100644 --- a/bundle/direct/dresources/app.go +++ b/bundle/direct/dresources/app.go @@ -97,31 +97,21 @@ func (r *ResourceApp) DoRead(ctx context.Context, id string) (*AppRemote, error) Lifecycle: &StateLifecycle{Started: &started}, } if app.ActiveDeployment != nil { - // The source code path in active deployment is snapshotted version of the source code path in the app. - // We need to use the default source code path to get the correct source code path for drift detection. - remote.SourceCodePath = app.DefaultSourceCodePath - remote.GitSource = app.ActiveDeployment.GitSource + // config (command/env) is not part of apps.App, so it is read back from the + // active deployment for drift detection. source_code_path and git_source are + // sent on Create/Update and classified input_only, so their remote value is + // ignored (see resources.generated.yml) and does not need populating here. remote.Config = deploymentToAppConfig(app.ActiveDeployment) } return remote, nil } -// appRequestBody returns config.App with the deploy-only fields cleared. source_code_path -// and git_source became part of apps.App in SDK v0.175, but DABs applies them through the -// Deploy API (see manageLifecycle), so they must not ride along in create/update bodies. -func appRequestBody(config *AppState) apps.App { - app := config.App - app.SourceCodePath = "" - app.GitSource = nil - return app -} - func (r *ResourceApp) DoCreate(ctx context.Context, config *AppState) (string, *AppRemote, error) { // Start app compute only when lifecycle.started=true is explicit. // For nil (omitted) or false, use no_compute=true (do not start compute). noCompute := config.Lifecycle == nil || config.Lifecycle.Started == nil || !*config.Lifecycle.Started request := apps.CreateAppRequest{ - App: appRequestBody(config), + App: config.App, NoCompute: noCompute, ForceSendFields: nil, } @@ -168,16 +158,18 @@ var UpdateMaskFields = []string{ "compute_min_instances", "compute_max_instances", "git_repository", + "git_source", + "source_code_path", "telemetry_export_destinations", } var updateMask = strings.Join(UpdateMaskFields, ",") func (r *ResourceApp) DoUpdate(ctx context.Context, id string, config *AppState, entry *PlanEntry) (*AppRemote, error) { - // Deploy-only fields (source_code_path, config, git_source, lifecycle) are excluded - // from the request body; see appRequestBody. + // source_code_path and git_source are sent as part of the App body (in the update + // mask). Only config (command/env) is deploy-only and applied via manageLifecycle. if hasAppChanges(entry) { - app := appRequestBody(config) + app := config.App request := apps.AsyncUpdateAppRequest{ App: &app, AppName: id, @@ -222,6 +214,9 @@ func (r *ResourceApp) manageLifecycle(ctx context.Context, id string, config *Ap return err } } + // source_code_path and git_source are also sent on Create/Update (so they are + // recorded on the app and drift-detected), but the Deploy API requires a + // deployment source, so the deployment carries them too. deployment := appdeploy.BuildDeployment(config.SourceCodePath, config.Config, config.GitSource) if err := appdeploy.Deploy(ctx, r.client, id, deployment); err != nil { return err @@ -243,19 +238,19 @@ func (r *ResourceApp) manageLifecycle(ctx context.Context, id string, config *Ap } // hasAppChanges reports whether the plan entry contains any Update changes -// to fields that belong to the App Update API (i.e., not deploy-only fields). +// to fields that belong to the App Update API. config (command/env) is deploy-only +// and applied via manageLifecycle; lifecycle drives compute start/stop. func hasAppChanges(entry *PlanEntry) bool { - return entry.Changes.HasChangeExcept("source_code_path", "config", "git_source", "lifecycle", "lifecycle.started") + return entry.Changes.HasChangeExcept("config", "lifecycle", "lifecycle.started") } -// OverrideChangeDesc skips drift on the deploy-only fields (source_code_path, config, -// git_source) while the app has no active deployment. DoRead reads them only from the -// active deployment, so before the first deploy (or once a stop clears it) the remote -// side is empty and the diff is spurious; it applies on the next start (manageLifecycle). +// OverrideChangeDesc skips drift on config (command/env) while the app has no active +// deployment. config is read only from the active deployment (DoRead), so before the +// first deploy (or once a stop clears it) the remote side is empty and the diff is +// spurious; it applies on the next start (manageLifecycle). func (*ResourceApp) OverrideChangeDesc(_ context.Context, path *structpath.PathNode, change *ChangeDesc, remote *AppRemote) error { // Prefix(1) so a nested diff (e.g. config.command) matches its top-level field. - switch path.Prefix(1).String() { - case "source_code_path", "config", "git_source": + if path.Prefix(1).String() == "config" { if remote.ActiveDeployment == nil { change.Action = deployplan.Skip change.Reason = "no active deployment" @@ -372,8 +367,6 @@ func (r *ResourceApp) waitForApp(ctx context.Context, w *databricks.WorkspaceCli Lifecycle: &StateLifecycle{Started: &started}, } if app.ActiveDeployment != nil { - remote.SourceCodePath = app.DefaultSourceCodePath - remote.GitSource = app.ActiveDeployment.GitSource remote.Config = deploymentToAppConfig(app.ActiveDeployment) } return remote, nil diff --git a/libs/testserver/apps.go b/libs/testserver/apps.go index 7ef32ae1920..7d6b104b54d 100644 --- a/libs/testserver/apps.go +++ b/libs/testserver/apps.go @@ -28,6 +28,16 @@ func setUcSecurableKinds(app *apps.App) { } } +// maskInputOnly clears the fields the Apps API accepts on write but never echoes +// on read (source_code_path, git_source are input_only per the API spec; the read +// side exposes default_source_code_path / default_git_source instead). The stored +// app keeps the real values so deployments can resolve the configured source. +func maskInputOnly(app apps.App) apps.App { + app.SourceCodePath = "" + app.GitSource = nil + return app +} + func (s *FakeWorkspace) AppsCreateUpdate(req Request, name string) Response { var updateReq apps.AsyncUpdateAppRequest if err := json.Unmarshal(req.Body, &updateReq); err != nil { @@ -128,6 +138,7 @@ func (s *FakeWorkspace) AppsCreateDeployment(req Request, name string) Response app.ActiveDeployment = &deployment app.DefaultSourceCodePath = deployment.SourceCodePath + app.DefaultGitSource = deployment.GitSource s.Apps[name] = app return Response{Body: deployment} @@ -179,7 +190,7 @@ func (s *FakeWorkspace) AppsStart(_ Request, name string) Response { } s.Apps[name] = app - return Response{Body: app} + return Response{Body: maskInputOnly(app)} } func (s *FakeWorkspace) AppsStop(_ Request, name string) Response { @@ -205,7 +216,7 @@ func (s *FakeWorkspace) AppsStop(_ Request, name string) Response { app.PendingDeployment = nil s.Apps[name] = app - return Response{Body: app} + return Response{Body: maskInputOnly(app)} } // AppsGet returns the app, keeping DELETING resources visible so callers can @@ -221,7 +232,7 @@ func (s *FakeWorkspace) AppsGet(name string) Response { } } - return Response{Body: app} + return Response{Body: maskInputOnly(app)} } // AppsDelete simulates the real Apps DELETE lifecycle: the first DELETE flips @@ -320,9 +331,17 @@ func (s *FakeWorkspace) AppsUpsert(req Request, name string) Response { 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. + // Simulate the apps platform side effect: when an app with running compute + // is created, it is deployed from its configured source. source_code_path and + // git_source now arrive on the Create request; fall back to the default path + // for apps that configure neither. + sourceCodePath := app.SourceCodePath + if sourceCodePath == "" && app.GitSource == nil { + sourceCodePath = "/Workspace/Users/tester@databricks.com/" + name + } deployment := apps.AppDeployment{ - SourceCodePath: "/Workspace/Users/tester@databricks.com/" + name, + SourceCodePath: sourceCodePath, + GitSource: app.GitSource, } deployment.DeploymentId = fmt.Sprintf("deploy-%d", nextID()) @@ -333,6 +352,7 @@ func (s *FakeWorkspace) AppsUpsert(req Request, name string) Response { app.ActiveDeployment = &deployment app.DefaultSourceCodePath = deployment.SourceCodePath + app.DefaultGitSource = app.GitSource } app.Url = name + "-123.cloud.databricksapps.com" @@ -373,6 +393,6 @@ func (s *FakeWorkspace) AppsUpsert(req Request, name string) Response { s.Apps[name] = app return Response{ - Body: app, + Body: maskInputOnly(app), } }