diff --git a/acceptance/bundle/telemetry/config-remote-sync-not-in-config/databricks.yml b/acceptance/bundle/telemetry/config-remote-sync-not-in-config/databricks.yml new file mode 100644 index 00000000000..cbfaea6e7eb --- /dev/null +++ b/acceptance/bundle/telemetry/config-remote-sync-not-in-config/databricks.yml @@ -0,0 +1,11 @@ +bundle: + name: config-remote-sync-not-in-config + +resources: + jobs: + foo: + name: test job + tasks: + - task_key: main + notebook_task: + notebook_path: /Workspace/Users/tester@databricks.com/notebook diff --git a/acceptance/bundle/telemetry/config-remote-sync-not-in-config/out.test.toml b/acceptance/bundle/telemetry/config-remote-sync-not-in-config/out.test.toml new file mode 100644 index 00000000000..aadf9e4a151 --- /dev/null +++ b/acceptance/bundle/telemetry/config-remote-sync-not-in-config/out.test.toml @@ -0,0 +1,3 @@ +Cloud = false +GOOS.windows = false +EnvMatrix.DMS = [""] diff --git a/acceptance/bundle/telemetry/config-remote-sync-not-in-config/output.txt b/acceptance/bundle/telemetry/config-remote-sync-not-in-config/output.txt new file mode 100644 index 00000000000..66f437595af --- /dev/null +++ b/acceptance/bundle/telemetry/config-remote-sync-not-in-config/output.txt @@ -0,0 +1,21 @@ + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/config-remote-sync-not-in-config/default/files... +Created jobs.foo +Files: 1 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +>>> [CLI] bundle config-remote-sync --select-ids jobs:[FOO_ID] -o json +{ + "status": "failed", + "error": "deployed resources missing from the bundle configuration (renamed or removed since the last deploy): resources.jobs.foo", + "errorCode": "RESOURCE_NOT_IN_CONFIG", + "files": null, + "changes": {} +} + +>>> cat [OUTPUT_DIR]/out.requests.txt +{ + "error_message": "deployed resources missing from the bundle configuration (renamed or removed since the last deploy): resources.jobs.foo", + "error_category": "RESOURCE_NOT_IN_CONFIG" +} diff --git a/acceptance/bundle/telemetry/config-remote-sync-not-in-config/script b/acceptance/bundle/telemetry/config-remote-sync-not-in-config/script new file mode 100644 index 00000000000..cfb98fa135a --- /dev/null +++ b/acceptance/bundle/telemetry/config-remote-sync-not-in-config/script @@ -0,0 +1,11 @@ +trace $CLI bundle deploy +job_id="$(read_id.py foo)" + +# The deployed job's key is renamed locally, so the config no longer has it. +update_file.py databricks.yml " foo:" " bar:" + +trace $CLI bundle config-remote-sync --select-ids "jobs:$job_id" -o json + +trace cat "$OUT_REQUESTS" | jq 'select(has("path") and .path == "/telemetry-ext") | .body.protoLogs[] | fromjson | .entry.databricks_cli_log.bundle_config_remote_sync_event | select(. != null) | {error_message, error_category}' + +rm "$OUT_REQUESTS" diff --git a/acceptance/bundle/telemetry/config-remote-sync-not-in-config/test.toml b/acceptance/bundle/telemetry/config-remote-sync-not-in-config/test.toml new file mode 100644 index 00000000000..97b2a8e4d49 --- /dev/null +++ b/acceptance/bundle/telemetry/config-remote-sync-not-in-config/test.toml @@ -0,0 +1,2 @@ +# config-remote-sync is not supported on Windows. +GOOS.windows = false diff --git a/bundle/configsync/output.go b/bundle/configsync/output.go index ac6e81bd66c..52fbeb4ab40 100644 --- a/bundle/configsync/output.go +++ b/bundle/configsync/output.go @@ -33,10 +33,11 @@ const ( // DiffOutput represents the complete output of the config-remote-sync command type DiffOutput struct { - Status SyncStatus `json:"status"` - Error string `json:"error,omitempty"` - Files []FileChange `json:"files"` - Changes Changes `json:"changes"` + Status SyncStatus `json:"status"` + Error string `json:"error,omitempty"` + ErrorCode protos.BundleConfigRemoteSyncErrorCategory `json:"errorCode,omitempty"` + Files []FileChange `json:"files"` + Changes Changes `json:"changes"` } // WriteResult renders the result and records the error payload on stats for @@ -45,6 +46,8 @@ type DiffOutput struct { // returns the error so the CLI still fails loudly for humans. func WriteResult(out io.Writer, jsonOutput bool, stats *Stats, files []FileChange, changes Changes, err error) error { status := StatusSuccess + // Read before the fallback below so unclassified errors carry no code. + errorCode := stats.ErrorCategory if err != nil { if stats.ErrorCategory == "" { stats.ErrorCategory = protos.BundleConfigRemoteSyncErrorCategoryBundleLoadFailed @@ -74,6 +77,7 @@ func WriteResult(out io.Writer, jsonOutput bool, stats *Stats, files []FileChang } if err != nil { output.Error = err.Error() + output.ErrorCode = errorCode } result, marshalErr := json.MarshalIndent(output, "", " ") if marshalErr != nil { diff --git a/cmd/bundle/config_remote_sync.go b/cmd/bundle/config_remote_sync.go index 51f418f9180..b49750ea5d7 100644 --- a/cmd/bundle/config_remote_sync.go +++ b/cmd/bundle/config_remote_sync.go @@ -7,9 +7,11 @@ import ( "maps" "runtime" "slices" + "strings" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/configsync" + "github.com/databricks/cli/bundle/deployplan" "github.com/databricks/cli/bundle/statemgmt" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" @@ -121,6 +123,18 @@ Examples: if err != nil { return err } + // A selected resource planned as a delete was renamed or removed in the + // config, so its remote changes have nowhere to be written back. + var missing []string + for _, key := range selected { + if plan.Plan[key].Action == deployplan.Delete { + missing = append(missing, key) + } + } + if len(missing) > 0 { + stats.ErrorCategory = protos.BundleConfigRemoteSyncErrorCategoryResourceNotInConfig + return fmt.Errorf("deployed resources missing from the bundle configuration (renamed or removed since the last deploy): %s", strings.Join(missing, ", ")) + } detected = configsync.FilterChanges(detected, selected) } diff --git a/libs/telemetry/protos/bundle_config_remote_sync.go b/libs/telemetry/protos/bundle_config_remote_sync.go index 40ff54ddd98..678198514bc 100644 --- a/libs/telemetry/protos/bundle_config_remote_sync.go +++ b/libs/telemetry/protos/bundle_config_remote_sync.go @@ -11,6 +11,7 @@ const ( BundleConfigRemoteSyncErrorCategoryYamlApplyFailed BundleConfigRemoteSyncErrorCategory = "YAML_APPLY_FAILED" BundleConfigRemoteSyncErrorCategorySaveFailed BundleConfigRemoteSyncErrorCategory = "SAVE_FAILED" BundleConfigRemoteSyncErrorCategoryOutputFailed BundleConfigRemoteSyncErrorCategory = "OUTPUT_FAILED" + BundleConfigRemoteSyncErrorCategoryResourceNotInConfig BundleConfigRemoteSyncErrorCategory = "RESOURCE_NOT_IN_CONFIG" ) // BundleConfigRemoteSyncEvent is emitted on every execution of the @@ -18,7 +19,7 @@ const ( // // All fields are aggregate counts, booleans, or system-defined categories. // No resource names, keys, field paths, file paths, or configuration values -// are logged. +// are logged outside ErrorMessage. type BundleConfigRemoteSyncEvent struct { // Whether the command was invoked with --save (config files written to // disk) as opposed to diff-only mode.