-
Notifications
You must be signed in to change notification settings - Fork 234
direct: Fix RemoteAlreadySet skipping input-only fields #6112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4fe0fb7
aefa9a1
3a27b34
8abd019
3815ce5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| direct: A local change to an input-only field (one the API accepts on write but never returns on read, e.g. pipelines' `run_as` or external locations' `skip_validation`) is no longer silently skipped when the new value coincidentally matches the field's fabricated remote value. Previously such a change could hit the `remote_already_set` shortcut and be dropped from the plan. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| bundle: | ||
| name: test-bundle | ||
|
|
||
| resources: | ||
| pipelines: | ||
| my_pipeline: | ||
| name: "Test Pipeline run_as guard" | ||
| serverless: true | ||
| libraries: | ||
| - notebook: | ||
| path: /Users/{{workspace_user_name}}/test_notebook | ||
| run_as: | ||
| user_name: original@example.test |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| { | ||
| "plan_version": 2, | ||
| "cli_version": "[CLI_VERSION]", | ||
| "lineage": "[UUID]", | ||
| "serial": 1, | ||
| "plan": { | ||
| "resources.pipelines.my_pipeline": { | ||
| "action": "update", | ||
| "new_state": { | ||
| "value": { | ||
| "channel": "CURRENT", | ||
| "deployment": { | ||
| "kind": "BUNDLE", | ||
| "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" | ||
| }, | ||
| "edition": "ADVANCED", | ||
| "libraries": [ | ||
| { | ||
| "notebook": { | ||
| "path": "/Users/{{workspace_user_name}}/test_notebook" | ||
| } | ||
| } | ||
| ], | ||
| "name": "Test Pipeline run_as guard", | ||
| "run_as": { | ||
| "user_name": "changed@example.test" | ||
| }, | ||
| "serverless": true | ||
| } | ||
| }, | ||
| "remote_state": { | ||
| "channel": "CURRENT", | ||
| "creator_user_name": "[USERNAME]", | ||
| "deployment": { | ||
| "kind": "BUNDLE", | ||
| "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" | ||
| }, | ||
| "edition": "ADVANCED", | ||
| "effective_publishing_mode": "DEFAULT_PUBLISHING_MODE", | ||
| "id": "[MY_PIPELINE_ID]", | ||
| "last_modified": [UNIX_TIME_MILLIS], | ||
| "libraries": [ | ||
| { | ||
| "notebook": { | ||
| "path": "/Users/{{workspace_user_name}}/test_notebook" | ||
| } | ||
| } | ||
| ], | ||
| "name": "Test Pipeline run_as guard", | ||
| "pipeline_id": "[MY_PIPELINE_ID]", | ||
| "run_as": { | ||
| "user_name": "changed@example.test" | ||
| }, | ||
| "run_as_user_name": "[USERNAME]", | ||
| "serverless": true, | ||
| "state": "IDLE", | ||
| "storage": "dbfs:/pipelines/[MY_PIPELINE_ID]" | ||
| }, | ||
| "changes": { | ||
| "run_as.user_name": { | ||
| "action": "update", | ||
| "old": "original@example.test", | ||
| "new": "changed@example.test", | ||
| "remote": "changed@example.test" | ||
| }, | ||
| "storage": { | ||
| "action": "skip", | ||
| "reason": "backend_default", | ||
| "remote": "dbfs:/pipelines/[MY_PIPELINE_ID]" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "cli_version": "[CLI_VERSION]", | ||
| "plan": { | ||
| "resources.pipelines.my_pipeline": { | ||
| "action": "update" | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
|
|
||
| >>> [CLI] bundle deploy | ||
| Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... | ||
| Deploying resources... | ||
| Updating deployment state... | ||
| Deployment complete! | ||
|
|
||
| === Make local change: run_as user -> changed@example.test | ||
| === Force remote run_as to match the new config value | ||
| >>> [CLI] bundle plan | ||
| update pipelines.my_pipeline | ||
|
|
||
| Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # run_as is declared ignore_remote_changes: input_only. This test forces the situation where | ||
| # a genuine local change (state has run_as A, config changes it to B) coincides with remote | ||
| # already reading back B. Before the RemoteAlreadySet input-only fix, remote == new wrongly | ||
| # skipped the change (reason: remote_already_set); it must instead classify as an update. | ||
|
|
||
| trace $CLI bundle deploy | ||
| pipeline_id="$(read_id.py my_pipeline)" | ||
|
|
||
| title "Make local change: run_as user -> changed@example.test" | ||
| update_file.py databricks.yml "original@example.test" "changed@example.test" | ||
|
|
||
| title "Force remote run_as to match the new config value" | ||
| edit_resource.py pipelines "$pipeline_id" <<EOF | ||
| r["run_as"] = {"user_name": "changed@example.test"} | ||
| EOF | ||
|
|
||
| trace $CLI bundle plan | ||
| $CLI bundle plan -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json | ||
|
|
||
| rm out.requests.txt |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| RecordRequests = true | ||
| Ignore = [".databricks"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,17 +47,35 @@ If the API may return a slice's elements in a different order between calls (e.g | |
| The state struct is serialized to JSON and persisted between deploys. Backward incompatible changes will result in a drift, which depending | ||
| on field behaviour might result in recreate. See dstate/migrate.go on how to handle state migration. | ||
|
|
||
| ## RemapState and missing remote fields | ||
| ## RemapState is a dumb copy; DoRead owns all remapping | ||
|
|
||
| Do not populate a field in `RemapState` by mapping it from a differently-named field in `RemoteType`. When a field is absent from `RemoteType` the engine automatically suppresses remote drift for it (reason: `missing_in_remote`), which means any value `RemapState` sets there is invisible to drift detection — real remote changes go undetected. | ||
| `RemapState` converts `RemoteType` to `StateType` only because `StateType` is typically a | ||
| subset of `RemoteType`. It must be a field-by-field copy (or no-op), never a place for | ||
| logic. In particular, do not remap a differently-named field there (e.g. `state.x = remote.status.x`). | ||
| Any remapping the API requires belongs in `DoRead`: add `x` directly to `RemoteType` and | ||
| populate it from `status.x` inside `DoRead`. | ||
|
Comment on lines
+52
to
+56
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. did you check if the rest of the codebase complies to this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, secret_scopes do not, but they are more complex. |
||
|
|
||
| The correct pattern is: | ||
| This is not a style preference — the plan classifier depends on it. Because `RemapState` is a | ||
| dumb copy, a field absent from `RemoteType` is always nil/zero in the remapped remote state the | ||
| planner compares against: there is nothing to copy it from. So "path present in `StateType` but | ||
| absent from `RemoteType`" reliably means "field the API accepts on write but never echoes on | ||
| read," and its remote value is guaranteed meaningless (see `isFieldMissingInRemote` / | ||
| `missing_in_remote` in `bundle_plan.go`). | ||
|
|
||
| A `RemapState` that instead synthesizes `state.x = remote.status.x` breaks this: `x` is absent | ||
| from `RemoteType`, so the classifier treats it as input-only and skips it (`missing_in_remote`), | ||
| yet the field actually has a real remote value under `status.x`. Genuine remote drift is then | ||
| silently suppressed. The fix is to make the field genuinely present in `RemoteType`, not to | ||
| smuggle it in via `RemapState`. | ||
|
|
||
| So when a field comes back under a different path than `StateType` uses: | ||
|
|
||
| 1. Add the field to `RemoteType` (the struct returned by `DoRead`). | ||
| 2. Populate it in `DoRead` by mapping from whatever the API returns under the other name. | ||
| 3. Keep `RemapState` trivial (a direct struct copy or no-op). | ||
| 3. Keep `RemapState` a trivial subset copy. | ||
|
|
||
| This makes the field present in `InputType`, `StateType`, and `RemoteType`, so it participates in normal drift detection and is no longer subject to the `missing_in_remote` suppression. | ||
| This makes the field present in `InputType`, `StateType`, and `RemoteType`, so it participates | ||
| in normal drift detection and is no longer subject to the `missing_in_remote` suppression. | ||
|
|
||
| ## OverrideChangeDesc | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for my understanding, we do need an update to align old & new, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes. Normally that would be skipped, but run_as is in ignore_remote_changes, so here it triggers an update.