From 9b87e881e6be053c78a3e2da91795520b795981d Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Tue, 3 Nov 2026 16:30:45 +0100 Subject: [PATCH 1/2] Fix app resoruce drift on config field --- .nextchanges/bundles/app-config-env-drift.md | 1 + .../apps/config-drift-stopped/output.txt | 3 +- .../apps/config-drift/out.plan.direct.json | 6 +- .../apps/config-env-no-drift/app/app.py | 5 ++ .../apps/config-env-no-drift/databricks.yml | 25 ++++++ .../apps/config-env-no-drift/out.test.toml | 2 + .../apps/config-env-no-drift/output.txt | 81 +++++++++++++++++++ .../resources/apps/config-env-no-drift/script | 23 ++++++ .../apps/config-env-no-drift/test.toml | 5 ++ bundle/direct/dresources/configs/apps.yml | 7 ++ libs/testserver/apps.go | 21 ++--- 11 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 .nextchanges/bundles/app-config-env-drift.md create mode 100644 acceptance/bundle/resources/apps/config-env-no-drift/app/app.py create mode 100644 acceptance/bundle/resources/apps/config-env-no-drift/databricks.yml create mode 100644 acceptance/bundle/resources/apps/config-env-no-drift/out.test.toml create mode 100644 acceptance/bundle/resources/apps/config-env-no-drift/output.txt create mode 100644 acceptance/bundle/resources/apps/config-env-no-drift/script create mode 100644 acceptance/bundle/resources/apps/config-env-no-drift/test.toml diff --git a/.nextchanges/bundles/app-config-env-drift.md b/.nextchanges/bundles/app-config-env-drift.md new file mode 100644 index 00000000000..14923c59ea0 --- /dev/null +++ b/.nextchanges/bundles/app-config-env-drift.md @@ -0,0 +1 @@ +* Fix `bundle plan` reporting an update to an app's `config.env` values on every run, which also redeployed the app on every `bundle deploy`. The Apps API returns environment variable names only, so `value` and `value_from` are no longer compared against the remote deployment ([#7301](https://github.com/databricks/cli/issues/7301)). diff --git a/acceptance/bundle/resources/apps/config-drift-stopped/output.txt b/acceptance/bundle/resources/apps/config-drift-stopped/output.txt index e04d8765834..50c91532377 100644 --- a/acceptance/bundle/resources/apps/config-drift-stopped/output.txt +++ b/acceptance/bundle/resources/apps/config-drift-stopped/output.txt @@ -15,8 +15,7 @@ Resources: 1 created, 0 changed, 0 deleted, 0 unchanged "config.env[0].value": { "action": "update", "old": "original_value", - "new": "changed_value", - "remote": "original_value" + "new": "changed_value" } } 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 0fe76b5ecf4..217f6fa7af6 100644 --- a/acceptance/bundle/resources/apps/config-drift/out.plan.direct.json +++ b/acceptance/bundle/resources/apps/config-drift/out.plan.direct.json @@ -32,12 +32,10 @@ ], "remote": [ { - "name": "MY_VAR", - "value": "changed_value" + "name": "MY_VAR" }, { - "name": "NEW_VAR", - "value": "new_value" + "name": "NEW_VAR" } ] } diff --git a/acceptance/bundle/resources/apps/config-env-no-drift/app/app.py b/acceptance/bundle/resources/apps/config-env-no-drift/app/app.py new file mode 100644 index 00000000000..ad1e64f9fb0 --- /dev/null +++ b/acceptance/bundle/resources/apps/config-env-no-drift/app/app.py @@ -0,0 +1,5 @@ +import http.server, os + +http.server.HTTPServer( + ("", int(os.environ["DATABRICKS_APP_PORT"])), http.server.SimpleHTTPRequestHandler +).serve_forever() diff --git a/acceptance/bundle/resources/apps/config-env-no-drift/databricks.yml b/acceptance/bundle/resources/apps/config-env-no-drift/databricks.yml new file mode 100644 index 00000000000..6f44bf1de2d --- /dev/null +++ b/acceptance/bundle/resources/apps/config-env-no-drift/databricks.yml @@ -0,0 +1,25 @@ +bundle: + name: apps-config-env-no-drift + +resources: + apps: + myapp: + name: myapp + source_code_path: ./app + resources: + - name: my-secret + secret: + scope: my-scope + key: my-key + permission: READ + config: + command: + - python + - app.py + env: + - name: GREETING + value: hello + - name: API_TOKEN + value_from: my-secret + lifecycle: + started: true diff --git a/acceptance/bundle/resources/apps/config-env-no-drift/out.test.toml b/acceptance/bundle/resources/apps/config-env-no-drift/out.test.toml new file mode 100644 index 00000000000..a927a5fbc06 --- /dev/null +++ b/acceptance/bundle/resources/apps/config-env-no-drift/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/resources/apps/config-env-no-drift/output.txt b/acceptance/bundle/resources/apps/config-env-no-drift/output.txt new file mode 100644 index 00000000000..82d16c28a37 --- /dev/null +++ b/acceptance/bundle/resources/apps/config-env-no-drift/output.txt @@ -0,0 +1,81 @@ + +=== Deploy a started app with config.env entries set via value and value_from +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/apps-config-env-no-drift/default/files... +✓ Deployment succeeded +Created apps.myapp +Files: 2 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +=== The Apps API returns deployment env vars with their name only +>>> [CLI] bundle plan -o json +[ + { + "name": "GREETING" + }, + { + "name": "API_TOKEN" + } +] + +=== Re-plan is clean: value and value_from are input-only and must not drift +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged + +>>> [CLI] bundle plan -o json +{ + "config.env[0].value": { + "action": "skip", + "reason": "input_only", + "old": "hello", + "new": "hello" + }, + "config.env[1].value_from": { + "action": "skip", + "reason": "input_only", + "old": "my-secret", + "new": "my-secret" + } +} + +=== A local change to an env value still plans an update +>>> update_file.py databricks.yml value: hello value: goodbye + +>>> [CLI] bundle plan +update apps.myapp + +Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan -o json +{ + "config.env[0].value": { + "action": "update", + "old": "hello", + "new": "goodbye" + }, + "config.env[1].value_from": { + "action": "skip", + "reason": "input_only", + "old": "my-secret", + "new": "my-secret" + } +} + +=== Deploy applies the change and the plan is clean again +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/apps-config-env-no-drift/default/files... +✓ Deployment succeeded +Updated apps.myapp +Files: 1 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged + +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.apps.myapp + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/apps-config-env-no-drift/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/resources/apps/config-env-no-drift/script b/acceptance/bundle/resources/apps/config-env-no-drift/script new file mode 100644 index 00000000000..ba73a0fb09a --- /dev/null +++ b/acceptance/bundle/resources/apps/config-env-no-drift/script @@ -0,0 +1,23 @@ +cleanup() { + trace $CLI bundle destroy --auto-approve +} +trap cleanup EXIT + +title "Deploy a started app with config.env entries set via value and value_from" +trace $CLI bundle deploy + +title "The Apps API returns deployment env vars with their name only" +trace $CLI bundle plan -o json | nostamp | jq '.plan."resources.apps.myapp".remote_state.config.env' + +title "Re-plan is clean: value and value_from are input-only and must not drift" +trace $CLI bundle plan | contains.py "Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged" +trace $CLI bundle plan -o json | nostamp | jq '.plan."resources.apps.myapp".changes // {} | with_entries(select(.key | startswith("config.env")))' + +title "A local change to an env value still plans an update" +trace update_file.py databricks.yml "value: hello" "value: goodbye" +trace $CLI bundle plan | contains.py "Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged" +trace $CLI bundle plan -o json | nostamp | jq '.plan."resources.apps.myapp".changes // {} | with_entries(select(.key | startswith("config.env")))' + +title "Deploy applies the change and the plan is clean again" +trace $CLI bundle deploy +trace $CLI bundle plan | contains.py "Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged" diff --git a/acceptance/bundle/resources/apps/config-env-no-drift/test.toml b/acceptance/bundle/resources/apps/config-env-no-drift/test.toml new file mode 100644 index 00000000000..398feb4ffbe --- /dev/null +++ b/acceptance/bundle/resources/apps/config-env-no-drift/test.toml @@ -0,0 +1,5 @@ +# The Apps API returns deployment env vars with their name only, so config.env value and +# value_from are classified input-only (ignore_remote_changes) and the fake server models +# that masking. A real workspace would need the referenced secret to exist, so this runs +# locally only. See https://github.com/databricks/cli/issues/7301 +RecordRequests = false diff --git a/bundle/direct/dresources/configs/apps.yml b/bundle/direct/dresources/configs/apps.yml index a6725316bec..808a552cd15 100644 --- a/bundle/direct/dresources/configs/apps.yml +++ b/bundle/direct/dresources/configs/apps.yml @@ -18,3 +18,10 @@ backend_defaults: ignore_remote_changes: - field: space # This field is not yet supported by Update APIs but exposed in the API spec. TODO: fix when update APIs supports it. reason: managed + # The Apps API returns deployment env vars with their name only: value and value_from are + # never read back, so the remote side is always empty and would otherwise plan an update + # on every run. See https://github.com/databricks/cli/issues/7301 + - field: config.env[*].value + reason: input_only + - field: config.env[*].value_from + reason: input_only diff --git a/libs/testserver/apps.go b/libs/testserver/apps.go index c70115d060f..29bc70adc2a 100644 --- a/libs/testserver/apps.go +++ b/libs/testserver/apps.go @@ -147,6 +147,13 @@ func (s *FakeWorkspace) AppsCreateDeployment(req Request, name string) Response Message: "Deployment succeeded", } + // The backend returns env vars with their name only: value and value_from are never + // read back, neither from get-deployment nor from the app's active_deployment. Mask + // them before storing so every read path agrees. See https://github.com/databricks/cli/issues/7301 + for i, ev := range deployment.EnvVars { + deployment.EnvVars[i] = apps.EnvVar{Name: ev.Name} + } + app.ActiveDeployment = &deployment app.DefaultSourceCodePath = deployment.SourceCodePath s.Apps[name] = app @@ -166,19 +173,7 @@ func (s *FakeWorkspace) AppsGetDeployment(_ Request, name, deploymentID string) return Response{StatusCode: 404} } - // Return a copy so the masking below does not mutate the stored deployment. - deployment := *app.ActiveDeployment - - // The real GET response strips env var values, returning only the name of - // each variable. Reproduce that masking so the local golden matches - // recorded cloud behavior. - maskedEnvVars := make([]apps.EnvVar, len(deployment.EnvVars)) - for i, ev := range deployment.EnvVars { - maskedEnvVars[i] = apps.EnvVar{Name: ev.Name} - } - deployment.EnvVars = maskedEnvVars - - return Response{Body: deployment} + return Response{Body: app.ActiveDeployment} } func (s *FakeWorkspace) AppsStart(_ Request, name string) Response { From 6292de68915a6fa5f22edcce99963466f14d9ca0 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Tue, 3 Nov 2026 18:12:52 +0100 Subject: [PATCH 2/2] fix changelog --- .nextchanges/bundles/app-config-env-drift.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nextchanges/bundles/app-config-env-drift.md b/.nextchanges/bundles/app-config-env-drift.md index 14923c59ea0..c96a769e5af 100644 --- a/.nextchanges/bundles/app-config-env-drift.md +++ b/.nextchanges/bundles/app-config-env-drift.md @@ -1 +1 @@ -* Fix `bundle plan` reporting an update to an app's `config.env` values on every run, which also redeployed the app on every `bundle deploy`. The Apps API returns environment variable names only, so `value` and `value_from` are no longer compared against the remote deployment ([#7301](https://github.com/databricks/cli/issues/7301)). +* Fix `bundle plan` reporting an update to an app's `config.env` values on every run, which also redeployed the app on every `bundle deploy`. The Apps API returns environment variable names only, so `value` and `value_from` are no longer compared against the remote deployment. ([#7326](https://github.com/databricks/cli/pull/7326))