From 2a0904eac84208d2ecffd4a566ac51464e2fba9e Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 14 Sep 2026 22:27:48 +0200 Subject: [PATCH] Split dresources resources.yml into one file per resource resources.yml and resources.generated.yml become configs/.yml and configs/.generated.yml, omitted when they have no rules. The resource type comes from the file name, so each file holds the rules directly with no enclosing keys. The generator writes one file per resource, and the loader embeds configs/*.yml and keys each resource type by its file name. Placing them under configs/ scopes the //go:embed to lifecycle config only, so the files keep the .yml extension without colliding with the generator's apitypes*.yml inputs. Co-authored-by: Isaac --- .agents/rules/auto-generated-files.md | 4 +- .agents/skills/bump-sdk/SKILL.md | 4 +- Taskfile.yml | 4 +- .../empty_string_dropped/gen_empty_config.py | 15 +- .../drift/managed_properties/test.toml | 2 +- .../drift/managed_properties/test.toml | 2 +- bundle/configsync/defaults.go | 2 +- bundle/direct/bundle_plan_test.go | 8 +- bundle/direct/dresources/README.md | 19 +- bundle/direct/dresources/config.go | 72 +- bundle/direct/dresources/config_test.go | 43 +- .../dresources/configs/alerts.generated.yml | 19 + .../dresources/configs/apps.generated.yml | 54 ++ bundle/direct/dresources/configs/apps.yml | 20 + bundle/direct/dresources/configs/catalogs.yml | 21 + .../dresources/configs/cluster_policies.yml | 6 + bundle/direct/dresources/configs/clusters.yml | 57 ++ .../configs/dashboards.generated.yml | 17 + .../direct/dresources/configs/dashboards.yml | 12 + .../configs/database_catalogs.generated.yml | 8 + .../dresources/configs/database_catalogs.yml | 11 + .../configs/database_instances.generated.yml | 78 ++ .../dresources/configs/database_instances.yml | 3 + .../configs/experiments.generated.yml | 9 + .../direct/dresources/configs/experiments.yml | 15 + .../configs/external_locations.generated.yml | 19 + .../dresources/configs/external_locations.yml | 10 + .../configs/genie_spaces.generated.yml | 5 + .../dresources/configs/genie_spaces.yml | 5 + .../dresources/configs/instance_pools.yml | 24 + .../configs/internal_immutable_snapshots.yml | 2 + bundle/direct/dresources/configs/job_runs.yml | 42 + bundle/direct/dresources/configs/jobs.yml | 146 +++ .../configs/model_services.generated.yml | 15 + .../dresources/configs/model_services.yml | 10 + .../model_serving_endpoints.generated.yml | 34 + .../configs/model_serving_endpoints.yml | 59 ++ bundle/direct/dresources/configs/models.yml | 18 + .../configs/pipelines.generated.yml | 5 + .../direct/dresources/configs/pipelines.yml | 73 ++ .../configs/postgres_branches.generated.yml | 29 + .../dresources/configs/postgres_branches.yml | 10 + .../configs/postgres_catalogs.generated.yml | 13 + .../dresources/configs/postgres_catalogs.yml | 10 + .../configs/postgres_databases.generated.yml | 7 + .../dresources/configs/postgres_databases.yml | 10 + .../configs/postgres_endpoints.generated.yml | 23 + .../dresources/configs/postgres_endpoints.yml | 10 + .../configs/postgres_projects.generated.yml | 27 + .../dresources/configs/postgres_projects.yml | 4 + .../configs/postgres_roles.generated.yml | 13 + .../dresources/configs/postgres_roles.yml | 21 + .../configs/postgres_snapshot_schedules.yml | 6 + .../postgres_synced_tables.generated.yml | 41 + .../configs/postgres_synced_tables.yml | 12 + .../dresources/configs/quality_monitors.yml | 6 + .../configs/registered_models.generated.yml | 5 + .../dresources/configs/registered_models.yml | 37 + bundle/direct/dresources/configs/schemas.yml | 17 + .../configs/secret_scopes.permissions.yml | 6 + .../dresources/configs/secret_scopes.yml | 14 + .../dresources/configs/secrets.generated.yml | 32 + bundle/direct/dresources/configs/secrets.yml | 3 + .../dresources/configs/sql_warehouses.yml | 37 + .../synced_database_tables.generated.yml | 24 + .../configs/synced_database_tables.yml | 11 + .../configs/vector_search_endpoints.yml | 12 + .../configs/vector_search_indexes.yml | 31 + bundle/direct/dresources/configs/volumes.yml | 21 + bundle/direct/dresources/dashboard.go | 2 +- bundle/direct/dresources/job_run_test.go | 2 +- bundle/direct/dresources/model_service.go | 2 +- bundle/direct/dresources/postgres_branch.go | 2 +- bundle/direct/dresources/postgres_catalog.go | 2 +- bundle/direct/dresources/postgres_database.go | 2 +- bundle/direct/dresources/postgres_endpoint.go | 2 +- bundle/direct/dresources/postgres_project.go | 2 +- bundle/direct/dresources/postgres_role.go | 2 +- .../dresources/postgres_synced_table.go | 2 +- bundle/direct/dresources/registered_model.go | 2 +- .../direct/dresources/resources.generated.yml | 533 ---------- bundle/direct/dresources/resources.yml | 908 ------------------ .../direct/dresources/vector_search_index.go | 2 +- .../dresources/vector_search_index_test.go | 2 +- bundle/direct/tools/generate_resources.py | 84 +- libs/testserver/cluster_policies.go | 2 +- 86 files changed, 1471 insertions(+), 1546 deletions(-) create mode 100644 bundle/direct/dresources/configs/alerts.generated.yml create mode 100644 bundle/direct/dresources/configs/apps.generated.yml create mode 100644 bundle/direct/dresources/configs/apps.yml create mode 100644 bundle/direct/dresources/configs/catalogs.yml create mode 100644 bundle/direct/dresources/configs/cluster_policies.yml create mode 100644 bundle/direct/dresources/configs/clusters.yml create mode 100644 bundle/direct/dresources/configs/dashboards.generated.yml create mode 100644 bundle/direct/dresources/configs/dashboards.yml create mode 100644 bundle/direct/dresources/configs/database_catalogs.generated.yml create mode 100644 bundle/direct/dresources/configs/database_catalogs.yml create mode 100644 bundle/direct/dresources/configs/database_instances.generated.yml create mode 100644 bundle/direct/dresources/configs/database_instances.yml create mode 100644 bundle/direct/dresources/configs/experiments.generated.yml create mode 100644 bundle/direct/dresources/configs/experiments.yml create mode 100644 bundle/direct/dresources/configs/external_locations.generated.yml create mode 100644 bundle/direct/dresources/configs/external_locations.yml create mode 100644 bundle/direct/dresources/configs/genie_spaces.generated.yml create mode 100644 bundle/direct/dresources/configs/genie_spaces.yml create mode 100644 bundle/direct/dresources/configs/instance_pools.yml create mode 100644 bundle/direct/dresources/configs/internal_immutable_snapshots.yml create mode 100644 bundle/direct/dresources/configs/job_runs.yml create mode 100644 bundle/direct/dresources/configs/jobs.yml create mode 100644 bundle/direct/dresources/configs/model_services.generated.yml create mode 100644 bundle/direct/dresources/configs/model_services.yml create mode 100644 bundle/direct/dresources/configs/model_serving_endpoints.generated.yml create mode 100644 bundle/direct/dresources/configs/model_serving_endpoints.yml create mode 100644 bundle/direct/dresources/configs/models.yml create mode 100644 bundle/direct/dresources/configs/pipelines.generated.yml create mode 100644 bundle/direct/dresources/configs/pipelines.yml create mode 100644 bundle/direct/dresources/configs/postgres_branches.generated.yml create mode 100644 bundle/direct/dresources/configs/postgres_branches.yml create mode 100644 bundle/direct/dresources/configs/postgres_catalogs.generated.yml create mode 100644 bundle/direct/dresources/configs/postgres_catalogs.yml create mode 100644 bundle/direct/dresources/configs/postgres_databases.generated.yml create mode 100644 bundle/direct/dresources/configs/postgres_databases.yml create mode 100644 bundle/direct/dresources/configs/postgres_endpoints.generated.yml create mode 100644 bundle/direct/dresources/configs/postgres_endpoints.yml create mode 100644 bundle/direct/dresources/configs/postgres_projects.generated.yml create mode 100644 bundle/direct/dresources/configs/postgres_projects.yml create mode 100644 bundle/direct/dresources/configs/postgres_roles.generated.yml create mode 100644 bundle/direct/dresources/configs/postgres_roles.yml create mode 100644 bundle/direct/dresources/configs/postgres_snapshot_schedules.yml create mode 100644 bundle/direct/dresources/configs/postgres_synced_tables.generated.yml create mode 100644 bundle/direct/dresources/configs/postgres_synced_tables.yml create mode 100644 bundle/direct/dresources/configs/quality_monitors.yml create mode 100644 bundle/direct/dresources/configs/registered_models.generated.yml create mode 100644 bundle/direct/dresources/configs/registered_models.yml create mode 100644 bundle/direct/dresources/configs/schemas.yml create mode 100644 bundle/direct/dresources/configs/secret_scopes.permissions.yml create mode 100644 bundle/direct/dresources/configs/secret_scopes.yml create mode 100644 bundle/direct/dresources/configs/secrets.generated.yml create mode 100644 bundle/direct/dresources/configs/secrets.yml create mode 100644 bundle/direct/dresources/configs/sql_warehouses.yml create mode 100644 bundle/direct/dresources/configs/synced_database_tables.generated.yml create mode 100644 bundle/direct/dresources/configs/synced_database_tables.yml create mode 100644 bundle/direct/dresources/configs/vector_search_endpoints.yml create mode 100644 bundle/direct/dresources/configs/vector_search_indexes.yml create mode 100644 bundle/direct/dresources/configs/volumes.yml delete mode 100644 bundle/direct/dresources/resources.generated.yml delete mode 100644 bundle/direct/dresources/resources.yml diff --git a/.agents/rules/auto-generated-files.md b/.agents/rules/auto-generated-files.md index d79fec64759..c16d707eab6 100644 --- a/.agents/rules/auto-generated-files.md +++ b/.agents/rules/auto-generated-files.md @@ -15,7 +15,7 @@ globs: - "cmd/workspace/**/*.go" - "internal/genkit/tagging.py" - "internal/mocks/**/*.go" - - "bundle/direct/dresources/*.generated.yml" + - "bundle/direct/dresources/configs/*.generated.yml" - "bundle/internal/validation/generated/*.go" - "bundle/schema/jsonschema.json" - "python/databricks/bundles/version.py" @@ -36,7 +36,7 @@ paths: - "cmd/workspace/**/*.go" - "internal/genkit/tagging.py" - "internal/mocks/**/*.go" - - "bundle/direct/dresources/*.generated.yml" + - "bundle/direct/dresources/configs/*.generated.yml" - "bundle/internal/validation/generated/*.go" - "bundle/schema/jsonschema.json" - "python/databricks/bundles/version.py" diff --git a/.agents/skills/bump-sdk/SKILL.md b/.agents/skills/bump-sdk/SKILL.md index 998c18b57c7..a78e2a21781 100644 --- a/.agents/skills/bump-sdk/SKILL.md +++ b/.agents/skills/bump-sdk/SKILL.md @@ -9,7 +9,7 @@ allowed-tools: Read, Edit, Write, Bash, Glob, Grep, WebFetch, AskUserQuestion The SDK version lives in `go.mod` (`github.com/databricks/databricks-sdk-go`) and the pinned spec SHA lives in `.codegen/_openapi_sha`. These two move as a pair; everything else in this skill is regenerated from them or is fallout you fix by hand. -Do not hand-edit generated files (`.codegen/cli.json`, `cmd/workspace/*`, `cmd/account/*`, `bundle/schema/jsonschema.json`, `bundle/internal/validation/generated/*`, `bundle/direct/dresources/resources.generated.yml`, `bundle/terraform_dabs_map/generated.go`, `python/databricks/bundles/**`); regenerate them. +Do not hand-edit generated files (`.codegen/cli.json`, `cmd/workspace/*`, `cmd/account/*`, `bundle/schema/jsonschema.json`, `bundle/internal/validation/generated/*`, `bundle/direct/dresources/configs/*.generated.yml`, `bundle/terraform_dabs_map/generated.go`, `python/databricks/bundles/**`); regenerate them. The Python tasks (`pydabs-*`, and the `pydabs-codegen` step inside `generate-check`) all run through `uv`. If one fails because `uv` is missing or because the host's `python3` is too old (e.g. 3.9), install `uv` (`curl -LsSf https://astral.sh/uv/install.sh | sh`) rather than touching the system Python: `uv run` provisions the interpreter each package pins (`>=3.10`, and `==3.13.*` under `python/codegen/`) and downloads it if needed. Do not chase the system Python version. @@ -42,7 +42,7 @@ Run `go build ./...` and fix compile breakages before touching acceptance golden Read the SDK's `CHANGELOG.md` at the target version (in the module cache) to enumerate breaking changes before chasing compile errors. A removed struct field that the CLI used (e.g. `jobs.AiRuntimeTask.CodeSourcePath`) should have its usage temporarily disabled with a comment noting it returns in a later SDK bump, not deleted outright. A new struct field triggers an `exhaustruct` lint failure in `bundle/direct/dresources/*`. Run `./task lint` and (in case of issues) wire the field through `PrepareState` and `RemapState` when it exists on both the input and remote types. -A field the new spec now annotates as output-only may already be emitted into `resources.generated.yml`, making the manual entry in `resources.yml` redundant; `TestResourcesYMLNoRedundantRules` catches this, so remove the manual entry. +A field the new spec now annotates as output-only may already be emitted into `.generated.yml`, making the manual entry in `.yml` redundant; `TestResourcesYMLNoRedundantRules` catches this, so remove the manual entry. **6. Refresh goldens, then VERIFY.** diff --git a/Taskfile.yml b/Taskfile.yml index 72e8e258ca8..3cbe918c2b5 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1050,9 +1050,9 @@ tasks: - bundle/direct/dresources/apitypes.yml - acceptance/bundle/refschema/out.fields.txt generates: - - bundle/direct/dresources/resources.generated.yml + - bundle/direct/dresources/configs/*.generated.yml cmds: - - "sh -c 'uv run --script bundle/direct/tools/generate_resources.py .codegen/cli.json bundle/direct/dresources/apitypes.generated.yml bundle/direct/dresources/apitypes.yml acceptance/bundle/refschema/out.fields.txt > bundle/direct/dresources/resources.generated.yml'" + - "uv run --script bundle/direct/tools/generate_resources.py .codegen/cli.json bundle/direct/dresources/apitypes.generated.yml bundle/direct/dresources/apitypes.yml acceptance/bundle/refschema/out.fields.txt bundle/direct/dresources/configs" # pydabs-* tasks are defined in python/Taskfile.yml (included above). diff --git a/acceptance/bundle/empty_string_dropped/gen_empty_config.py b/acceptance/bundle/empty_string_dropped/gen_empty_config.py index 73d360d1e8b..e2e2cf79d38 100644 --- a/acceptance/bundle/empty_string_dropped/gen_empty_config.py +++ b/acceptance/bundle/empty_string_dropped/gen_empty_config.py @@ -11,8 +11,8 @@ A field is settable/eligible when out.fields.txt types it as `string` (this skips enums, which are named types) with flag ALL or INPUT, and it is not -output_only (resources.generated.yml), a bundle-framework field, or a known -terraform-erroring field. +output_only (.generated.yml), a bundle-framework field, or a +known terraform-erroring field. Run from the repo root; writes databricks.yml in the test directory: acceptance/bundle/empty_string_dropped/gen_empty_config.py @@ -24,7 +24,8 @@ import yaml FIELDS = Path("acceptance/bundle/refschema/out.fields.txt") -GENERATED = Path("bundle/direct/dresources/resources.generated.yml") +CONFIGS = Path("bundle/direct/dresources/configs") +GENERATED_SUFFIX = ".generated.yml" TESTDIR = Path("acceptance/bundle/empty_string_dropped") BASE = TESTDIR / "base.yml" @@ -79,14 +80,14 @@ def string_leaf_parents(): def output_only_fields(): """Map resource type -> {output_only field paths} (user cannot set).""" - gen = yaml.safe_load(GENERATED.read_text()) or {} result = {} - for rtype, spec in (gen.get("resources") or {}).items(): + for path in sorted(CONFIGS.glob("*" + GENERATED_SUFFIX)): + spec = yaml.safe_load(path.read_text()) or {} fields = set() - for entry in (spec or {}).get("ignore_remote_changes") or []: + for entry in spec.get("ignore_remote_changes") or []: if str(entry.get("reason", "")).startswith("spec:output_only"): fields.add(entry["field"]) - result[rtype] = fields + result[path.name.removesuffix(GENERATED_SUFFIX)] = fields return result diff --git a/acceptance/bundle/resources/catalogs/drift/managed_properties/test.toml b/acceptance/bundle/resources/catalogs/drift/managed_properties/test.toml index 5016e85b395..3780cb322a0 100644 --- a/acceptance/bundle/resources/catalogs/drift/managed_properties/test.toml +++ b/acceptance/bundle/resources/catalogs/drift/managed_properties/test.toml @@ -1,5 +1,5 @@ RecordRequests = true # Terraform issues a spurious PATCH for enable_predictive_optimization on every -# deploy, which is outside the scope of backend-default handling in resources.yml. +# deploy, which is outside the scope of backend-default handling in catalogs.yml. EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/schemas/drift/managed_properties/test.toml b/acceptance/bundle/resources/schemas/drift/managed_properties/test.toml index 5016e85b395..67819b3d322 100644 --- a/acceptance/bundle/resources/schemas/drift/managed_properties/test.toml +++ b/acceptance/bundle/resources/schemas/drift/managed_properties/test.toml @@ -1,5 +1,5 @@ RecordRequests = true # Terraform issues a spurious PATCH for enable_predictive_optimization on every -# deploy, which is outside the scope of backend-default handling in resources.yml. +# deploy, which is outside the scope of backend-default handling in schemas.yml. EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/bundle/configsync/defaults.go b/bundle/configsync/defaults.go index c4e38b81e55..0981b0625d6 100644 --- a/bundle/configsync/defaults.go +++ b/bundle/configsync/defaults.go @@ -63,7 +63,7 @@ var serverSideDefaults = map[string]any{ // custom_tags and cluster_log_conf are commonly injected by cluster policies // when the user omits them, so they exist only remotely. Syncing them back leaks // one environment's policy values into (often shared) config and breaks deploys in - // other environments. TODO: move to backend_defaults in resources.yml once + // other environments. TODO: move to backend_defaults in jobs.yml once // configsync filtering is migrated to the direct engine lifecycle metadata. "resources.jobs.*.tasks[*].new_cluster.custom_tags": backendDefault, "resources.jobs.*.tasks[*].new_cluster.cluster_log_conf": backendDefault, diff --git a/bundle/direct/bundle_plan_test.go b/bundle/direct/bundle_plan_test.go index d0333a09564..ccd7f827ad5 100644 --- a/bundle/direct/bundle_plan_test.go +++ b/bundle/direct/bundle_plan_test.go @@ -81,8 +81,8 @@ resources: } func TestShouldSkipBackendDefault_ManagedPropertiesOnly(t *testing.T) { - // Rules mirror the schemas backend_defaults in resources.yml, but the test is - // deliberately self-contained so that edits to resources.yml don't break it. + // Rules mirror the schemas backend_defaults in schemas.yml, but the test is + // deliberately self-contained so that edits to schemas.yml don't break it. // The real wiring is covered by acceptance/bundle/resources/schemas/drift. managedDefaults, err := structpath.ParsePattern("properties['unity.catalog.managed.*.defaults.*']") require.NoError(t, err) @@ -406,8 +406,8 @@ func bundleWithSkippedJobRun(t *testing.T, remote *dresources.JobRunRemote) *Dep } func TestShouldSkipRemoteAddition(t *testing.T) { - // Rules mirror clusters/jobs ignore_remote_additions in resources.yml, but the test is - // deliberately self-contained so edits to resources.yml don't break it. The real wiring + // Rules mirror clusters/jobs ignore_remote_additions in clusters.yml and jobs.yml, but the + // test is deliberately self-contained so edits to those files don't break it. The real wiring // is covered by acceptance/bundle/resources/cluster_policies/*. jobCluster, err := structpath.ParsePattern("job_clusters[*].new_cluster") require.NoError(t, err) diff --git a/bundle/direct/dresources/README.md b/bundle/direct/dresources/README.md index e93d5abcc10..8a745a3c6be 100644 --- a/bundle/direct/dresources/README.md +++ b/bundle/direct/dresources/README.md @@ -9,20 +9,25 @@ An exception could be made if default error message lacks the necessary context. - The arguments point to actual struct that will be persisted in state, any changes to it will affect what is stored in state. Usually there is no need to change it, but if there is, there should always be detailed explanation. - Each Create/Update/Delete method should correspond to one API call. We persist state right after, so there is minimum chance of having orphaned resources. - - We should calculate the update type during plan phase. This means it should be configured via resources.yml as much as possible, falling back to OverrideChangeDesc(). The DoUpdate() implementation should be as predictable as possible based on the plan. In particular, avoid reading remote state in DoUpdate() to decide what kind of update to dod. + - We should calculate the update type during plan phase. This means it should be configured via the resource's YAML file as much as possible, falling back to OverrideChangeDesc(). The DoUpdate() implementation should be as predictable as possible based on the plan. In particular, avoid reading remote state in DoUpdate() to decide what kind of update to dod. - Create/Update/Delete methods should not need to do read requests. They can read state passed to them via \*PlanEntry but that should be reserved for exceptional cases. Most resources should have 1-1 mapping to single SDK/API call. - - For update with complex logic, ensure that DoUpdate() never results in no-op. If certain fields could not be updated, they should be excluded at plan level in resources.yml. + - For update with complex logic, ensure that DoUpdate() never results in no-op. If certain fields could not be updated, they should be excluded at plan level in `.yml`. -## Field classification in resources.yml +## Field classification in `.yml` -Each field with special plan/deploy behavior must be declared in `resources.yml`. Choose the right category: +Each field with special plan/deploy behavior must be declared in the YAML file of its resource type, under `configs/`. Both files of a resource type are optional and are omitted when they would be empty: + + - `configs/.yml` (e.g. `configs/jobs.yml`) — hand-written rules. + - `configs/.generated.yml` — API field behaviors from the OpenAPI schema. Generated by `./task generate-direct-resources`, do not edit. + +Choose the right category: - **`backend_defaults`**: The backend may fill in a value when the user doesn't specify one. Suppresses the diff when the user's config is nil/empty but remote has a value. Optionally restrict to specific allowed remote values via `values:`. Use for fields the API fills in as defaults (e.g., `format`, `run_if`, `node_type_id`). Link to TF provider suppression comment in the same format as existing entries. - - **`ignore_remote_changes`**: Ignore changes the remote makes to this field. Use for fields the backend manages (e.g., cloud-provider attributes like `aws_attributes`, `gcp_attributes`) or fields not returned by the update endpoint. Do not zero out such fields in `RemapState` to hide them from diff computation: carry the real remote value through and declare the field here instead, since zeroing discards information and duplicates the suppression logic. For `output_only` fields this rule is often already produced by `resources.generated.yml` from the OpenAPI annotation. Reason codes: + - **`ignore_remote_changes`**: Ignore changes the remote makes to this field. Use for fields the backend manages (e.g., cloud-provider attributes like `aws_attributes`, `gcp_attributes`) or fields not returned by the update endpoint. Do not zero out such fields in `RemapState` to hide them from diff computation: carry the real remote value through and declare the field here instead, since zeroing discards information and duplicates the suppression logic. For `output_only` fields this rule is often already produced by `.generated.yml` from the OpenAPI annotation. Reason codes: - `output_only` — the field is computed by the backend; the user never sets it - `input_only` — accepted on create/update but not returned by GET (e.g., write-only tokens, flags) - `managed` — managed by the cloud provider or platform, not by the user config - - **`ignore_local_changes`**: Ignore changes the user makes to this field. Use for fields that cannot be updated via API — either they are immutable after creation or require a separate API that is not yet implemented. Must have a comment in resources.yml explaining why. + - **`ignore_local_changes`**: Ignore changes the user makes to this field. Use for fields that cannot be updated via API — either they are immutable after creation or require a separate API that is not yet implemented. Must have a comment explaining why. - **`recreate_on_changes`**: Changing this field requires delete + create. Use for truly immutable fields (name, type, location). The reason should reference API docs or TF provider. - **`updatable_id_fields`**: Changing this field changes the resource's ID. Requires `DoUpdateWithID` to be implemented. @@ -96,7 +101,7 @@ in normal drift detection and is no longer subject to the `missing_in_remote` su ## OverrideChangeDesc -Use `OverrideChangeDesc` only as a last resort when `resources.yml` settings cannot express the needed logic. Skipping an action with `change.Action = deployplan.Skip` in `OverrideChangeDesc` creates a silent no-op: the plan shows no change even if the user's config differs from remote. Document the skip reason clearly in both the comment and `change.Reason`. +Use `OverrideChangeDesc` only as a last resort when the `.yml` settings cannot express the needed logic. Skipping an action with `change.Action = deployplan.Skip` in `OverrideChangeDesc` creates a silent no-op: the plan shows no change even if the user's config differs from remote. Document the skip reason clearly in both the comment and `change.Reason`. ## Nice to have - Add link to corresponding API documentation before each method. diff --git a/bundle/direct/dresources/config.go b/bundle/direct/dresources/config.go index 0aa011605c0..69d1d80b955 100644 --- a/bundle/direct/dresources/config.go +++ b/bundle/direct/dresources/config.go @@ -1,8 +1,12 @@ package dresources import ( - _ "embed" + "embed" "encoding/json" + "fmt" + "io/fs" + "path" + "strings" "sync" "github.com/databricks/cli/libs/structs/structpath" @@ -105,11 +109,21 @@ type Config struct { Resources map[string]ResourceLifecycleConfig `yaml:"resources"` } -//go:embed resources.yml -var resourcesYAML []byte +// One file per resource type under configs/: .yml holds the +// hand-written rules and .generated.yml the ones derived from the +// OpenAPI spec. A resource type without rules has no file. The file name is the +// resource type, so each file holds the rules directly with no enclosing keys. +// +//go:embed configs/*.yml +var configFS embed.FS + +const ( + configDir = "configs" + ymlSuffix = ".yml" -//go:embed resources.generated.yml -var resourcesGeneratedYAML []byte + // generatedSuffix marks a generated file once ymlSuffix is trimmed. + generatedSuffix = ".generated" +) var empty = ResourceLifecycleConfig{ IgnoreRemoteChanges: nil, @@ -123,28 +137,50 @@ var empty = ResourceLifecycleConfig{ SensitiveFields: nil, } -func mustParseConfig(data []byte) func() *Config { - return sync.OnceValue(func() *Config { - c := &Config{Resources: nil} - if err := yaml.Unmarshal(data, c); err != nil { +// loadConfigs parses every embedded YAML file into the hand-written or the generated +// config, keyed by the resource type the file is named after. +var loadConfigs = sync.OnceValues(func() (*Config, *Config) { + handWritten := &Config{Resources: map[string]ResourceLifecycleConfig{}} + generated := &Config{Resources: map[string]ResourceLifecycleConfig{}} + + names, err := fs.Glob(configFS, configDir+"/*"+ymlSuffix) + if err != nil { + panic(err) + } + + for _, name := range names { + dst, resourceType := handWritten, strings.TrimSuffix(path.Base(name), ymlSuffix) + if trimmed, ok := strings.CutSuffix(resourceType, generatedSuffix); ok { + dst, resourceType = generated, trimmed + } + + data, err := configFS.ReadFile(name) + if err != nil { panic(err) } - return c - }) -} -var loadConfig = mustParseConfig(resourcesYAML) + var rc ResourceLifecycleConfig + if err := yaml.Unmarshal(data, &rc); err != nil { + panic(fmt.Errorf("%s: %w", name, err)) + } + + dst.Resources[resourceType] = rc + } -var loadGeneratedConfig = mustParseConfig(resourcesGeneratedYAML) + return handWritten, generated +}) -// MustLoadConfig returns the parsed resources.yml configuration. +// MustLoadConfig returns the configuration parsed from the configs/.yml files. func MustLoadConfig() *Config { - return loadConfig() + handWritten, _ := loadConfigs() + return handWritten } -// MustLoadGeneratedConfig returns the parsed resources.generated.yml configuration. +// MustLoadGeneratedConfig returns the configuration parsed from the +// configs/.generated.yml files. func MustLoadGeneratedConfig() *Config { - return loadGeneratedConfig() + _, generated := loadConfigs() + return generated } // GetResourceConfig returns the lifecycle config for a given resource type. diff --git a/bundle/direct/dresources/config_test.go b/bundle/direct/dresources/config_test.go index b50c633bb72..1446a14e8e2 100644 --- a/bundle/direct/dresources/config_test.go +++ b/bundle/direct/dresources/config_test.go @@ -1,6 +1,9 @@ package dresources import ( + "io/fs" + "path" + "strings" "testing" "github.com/databricks/cli/libs/structs/structaccess" @@ -15,6 +18,26 @@ func TestMustLoadConfig(t *testing.T) { assert.NotEmpty(t, cfg.Resources) } +// TestConfigFiles guards the file naming the loader derives resource types from: a +// misspelled name would silently drop every rule in the file. A file holding no rules +// is a mistake too — an empty resource has no file at all. +func TestConfigFiles(t *testing.T) { + names, err := fs.Glob(configFS, configDir+"/*"+ymlSuffix) + require.NoError(t, err) + require.NotEmpty(t, names) + + for _, name := range names { + resourceType := strings.TrimSuffix(strings.TrimSuffix(path.Base(name), ymlSuffix), generatedSuffix) + assert.Containsf(t, SupportedResources, resourceType, "%s: %q is not a supported resource type", name, resourceType) + } + + for _, cfg := range []*Config{MustLoadConfig(), MustLoadGeneratedConfig()} { + for resourceType, rc := range cfg.Resources { + assert.NotEqualf(t, empty, rc, "%s: declares no rules; delete the file", resourceType) + } + } +} + func TestGetResourceConfig(t *testing.T) { assert.NotEmpty(t, GetResourceConfig("volumes").RecreateOnChanges) assert.Empty(t, GetResourceConfig("nonexistent").RecreateOnChanges) @@ -54,10 +77,10 @@ func categoryRules(c ResourceLifecycleConfig) []struct { } } -// TestResourcesYMLNoRedundantRules guards against two redundancy classes in -// resources.yml: duplicate field entries within the same category of a -// resource, and entries that the autogenerated resources.generated.yml already -// produces from the OpenAPI schema. +// TestResourcesYMLNoRedundantRules guards against two redundancy classes in the +// hand-written configs/.yml files: duplicate field entries within the +// same category of a resource, and entries that the autogenerated +// configs/.generated.yml already produces from the OpenAPI schema. func TestResourcesYMLNoRedundantRules(t *testing.T) { handWritten := MustLoadConfig() generated := MustLoadGeneratedConfig() @@ -78,11 +101,11 @@ func TestResourcesYMLNoRedundantRules(t *testing.T) { for _, r := range c.rules { field := r.Field.String() if seen[field] { - t.Errorf("bundle/direct/dresources/resources.yml: %s.%s lists %q twice; remove the duplicate entry", resourceType, c.name, field) + t.Errorf("bundle/direct/dresources/configs/%s.yml: %s lists %q twice; remove the duplicate entry", resourceType, c.name, field) } seen[field] = true if genFields[c.name][field] { - t.Errorf("bundle/direct/dresources/resources.yml: %s.%s entry %q is already produced by resources.generated.yml; remove it from resources.yml", resourceType, c.name, field) + t.Errorf("bundle/direct/dresources/configs/%[1]s.yml: %[2]s entry %[3]q is already produced by %[1]s.generated.yml; remove it", resourceType, c.name, field) } } } @@ -90,7 +113,7 @@ func TestResourcesYMLNoRedundantRules(t *testing.T) { } // TestResourcesYMLNoRedundantMissingInRemote guards that ignore_remote_changes entries -// in resources.yml do not duplicate the automatic missing-in-remote suppression. A field +// in configs/.yml do not duplicate the automatic missing-in-remote suppression. A field // absent from RemoteType is already skipped automatically (reason: missing_in_remote) when // there is no local change, so a manual ignore_remote_changes entry for it is dead weight. // @@ -104,14 +127,14 @@ func TestResourcesYMLNoRedundantMissingInRemote(t *testing.T) { for resourceType, rc := range cfg.Resources { adapter, err := NewAdapter(SupportedResources[resourceType], resourceType, nil) if err != nil { - t.Errorf("resources.yml: %s: failed to create adapter: %v", resourceType, err) + t.Errorf("%s.yml: failed to create adapter: %v", resourceType, err) continue } for _, r := range rc.IgnoreRemoteChanges { inState := structaccess.ValidatePattern(adapter.StateType(), r.Field) == nil inRemote := structaccess.ValidatePattern(adapter.RemoteType(), r.Field) == nil if inState && !inRemote { - t.Errorf("resources.yml: %s.ignore_remote_changes entry %q is automatically handled (field absent from RemoteType); remove it", resourceType, r.Field) + t.Errorf("%s.yml: ignore_remote_changes entry %q is automatically handled (field absent from RemoteType); remove it", resourceType, r.Field) } } } @@ -140,7 +163,7 @@ func TestResourcesYMLActionCategoriesExclusive(t *testing.T) { for _, r := range c.rules { field := r.Field.String() if prev, ok := firstCat[field]; ok { - t.Errorf("bundle/direct/dresources/resources.yml: %s lists %q in both %s and %s; a field's action belongs to exactly one category", resourceType, field, prev, c.name) + t.Errorf("bundle/direct/dresources/configs/%s.yml: %q is listed in both %s and %s; a field's action belongs to exactly one category", resourceType, field, prev, c.name) } else { firstCat[field] = c.name } diff --git a/bundle/direct/dresources/configs/alerts.generated.yml b/bundle/direct/dresources/configs/alerts.generated.yml new file mode 100644 index 00000000000..8b9e18006be --- /dev/null +++ b/bundle/direct/dresources/configs/alerts.generated.yml @@ -0,0 +1,19 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: create_time + reason: spec:output_only + - field: effective_run_as + reason: spec:output_only + - field: evaluation.last_evaluated_at + reason: spec:output_only + - field: evaluation.state + reason: spec:output_only + - field: id + reason: spec:output_only + - field: lifecycle_state + reason: spec:output_only + - field: owner_user_name + reason: spec:output_only + - field: update_time + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/apps.generated.yml b/bundle/direct/dresources/configs/apps.generated.yml new file mode 100644 index 00000000000..5e204b2a15f --- /dev/null +++ b/bundle/direct/dresources/configs/apps.generated.yml @@ -0,0 +1,54 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: git_repository.caller_credential_id + reason: spec:input_only + - field: git_source + reason: spec:input_only + - field: source_code_path + reason: spec:input_only + + - field: active_deployment + reason: spec:output_only + - field: app_status + reason: spec:output_only + - field: compute_status + reason: spec:output_only + - field: create_time + reason: spec:output_only + - field: creator + reason: spec:output_only + - field: default_git_source + reason: spec:output_only + - field: default_source_code_path + reason: spec:output_only + - field: effective_budget_policy_id + reason: spec:output_only + - field: effective_usage_policy_id + reason: spec:output_only + - field: effective_user_api_scopes + reason: spec:output_only + - field: id + reason: spec:output_only + - field: oauth2_app_client_id + reason: spec:output_only + - field: oauth2_app_integration_id + reason: spec:output_only + - field: pending_deployment + reason: spec:output_only + - field: resources[*].uc_securable.securable_kind + reason: spec:output_only + - field: service_principal_client_id + reason: spec:output_only + - field: service_principal_id + reason: spec:output_only + - field: service_principal_name + reason: spec:output_only + - field: thumbnail_url + reason: spec:output_only + - field: update_time + reason: spec:output_only + - field: updater + reason: spec:output_only + - field: url + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/apps.yml b/bundle/direct/dresources/configs/apps.yml new file mode 100644 index 00000000000..a6725316bec --- /dev/null +++ b/bundle/direct/dresources/configs/apps.yml @@ -0,0 +1,20 @@ +provided_id_fields: + - field: name + reason: id_field +backend_defaults: + # Backend sets it "MEDIUM" when not specified in the config + - field: compute_size + # Backend enables token forwarding and reports true even when the config omits it, + # so an omitted field would otherwise plan an update on every deploy. An explicit + # value in the config still diffs normally. + - field: forward_user_access_token + # lifecycle.started is derived from remote compute status in RemapState, so the + # remote side always has a value. When the user omits lifecycle from config, + # both old and new are nil and backend_defaults correctly skips the remote value. + # When the user explicitly sets lifecycle.started, old/new are non-nil and normal + # drift detection applies (e.g. detecting out-of-band stop). + - field: lifecycle + - field: lifecycle.started +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 diff --git a/bundle/direct/dresources/configs/catalogs.yml b/bundle/direct/dresources/configs/catalogs.yml new file mode 100644 index 00000000000..143a74c447d --- /dev/null +++ b/bundle/direct/dresources/configs/catalogs.yml @@ -0,0 +1,21 @@ +recreate_on_changes: + - field: storage_root + reason: immutable + - field: connection_name + reason: immutable + - field: provider_name + reason: immutable + - field: share_name + reason: immutable +normalize_slash: + # UC strips a trailing slash from storage_root on create. The TF provider + # suppresses the same slash-only drift on this field (ucDirectoryPathSlashOnlySuppressDiff): + # https://github.com/databricks/terraform-provider-databricks/blob/main/catalog/resource_catalog.go + - field: storage_root + reason: uc_strips_trailing_slash +updatable_id_fields: + - field: name + reason: id_changes +backend_defaults: + # UC auto-populates unity.catalog.managed..defaults.* keys after create. + - field: properties['unity.catalog.managed.*.defaults.*'] diff --git a/bundle/direct/dresources/configs/cluster_policies.yml b/bundle/direct/dresources/configs/cluster_policies.yml new file mode 100644 index 00000000000..6f2ee1e1b9b --- /dev/null +++ b/bundle/direct/dresources/configs/cluster_policies.yml @@ -0,0 +1,6 @@ +backend_defaults: + # A policy authored with policy_family_id and no definition gets its definition + # computed from the policy family by the backend and returned on read. Config + # leaves definition empty, so old/new are nil and backend_defaults skips the + # server-computed remote value; without this every deploy sees drift and re-Edits. + - field: definition diff --git a/bundle/direct/dresources/configs/clusters.yml b/bundle/direct/dresources/configs/clusters.yml new file mode 100644 index 00000000000..b19d9c0483c --- /dev/null +++ b/bundle/direct/dresources/configs/clusters.yml @@ -0,0 +1,57 @@ +# A cluster policy supplies cluster settings server-side: "fixed" elements always, +# "defaultValue" elements when the request sets apply_policy_default_values. The bundle +# is not the source for those, so the remote spec is legitimately a superset of what the +# config declares and an added field is not drift. Measured backend behavior is pinned by +# acceptance/bundle/resources/jobs/cluster_policy (fixed_addition, default_flag). +# https://github.com/databricks/cli/issues/5179 +# https://github.com/databricks/cli/issues/6512 +ignore_remote_additions: + - when_set: policy_id + +ignore_remote_changes: + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L361-L363 + # s.SchemaPath("aws_attributes").SetSuppressDiff() + # s.SchemaPath("azure_attributes").SetSuppressDiff() + # s.SchemaPath("gcp_attributes").SetSuppressDiff() + - field: aws_attributes + reason: managed + - field: azure_attributes + reason: managed + - field: gcp_attributes + reason: managed +backend_defaults: + # lifecycle.started is derived from remote cluster state in RemapState, so the + # remote side always has a value. When the user omits lifecycle from config, + # both old and new are nil and backend_defaults correctly skips the remote value. + # When the user explicitly sets lifecycle.started, old/new are non-nil and normal + # drift detection applies (e.g. detecting out-of-band terminate). + - field: lifecycle + - field: lifecycle.started + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L331 + # s.SchemaPath("enable_elastic_disk").SetComputed() + - field: enable_elastic_disk + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L332 + # s.SchemaPath("enable_local_disk_encryption").SetComputed() + - field: enable_local_disk_encryption + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L333 + # s.SchemaPath("node_type_id").SetComputed() + - field: node_type_id + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L334 + # s.SchemaPath("driver_node_type_id").SetComputed() + - field: driver_node_type_id + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L335 + # s.SchemaPath("driver_instance_pool_id").SetComputed() + - field: driver_instance_pool_id + + # Terraform currently does not do this, but it is a field with backend default. + # See https://github.com/databricks/cli/issues/4418 + - field: single_user_name + +# We have custom handler for this in cluster.go +# https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L109-L118 +# DataSecurityModeDiffSuppressFunc: suppress when old != "" && new == "" +#- field: data_security_mode diff --git a/bundle/direct/dresources/configs/dashboards.generated.yml b/bundle/direct/dresources/configs/dashboards.generated.yml new file mode 100644 index 00000000000..936b6e7f1c6 --- /dev/null +++ b/bundle/direct/dresources/configs/dashboards.generated.yml @@ -0,0 +1,17 @@ +# Generated, do not edit. + +recreate_on_changes: + - field: parent_path + reason: spec:immutable + +ignore_remote_changes: + - field: create_time + reason: spec:output_only + - field: dashboard_id + reason: spec:output_only + - field: lifecycle_state + reason: spec:output_only + - field: path + reason: spec:output_only + - field: update_time + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/dashboards.yml b/bundle/direct/dresources/configs/dashboards.yml new file mode 100644 index 00000000000..8760a2de8c9 --- /dev/null +++ b/bundle/direct/dresources/configs/dashboards.yml @@ -0,0 +1,12 @@ +ignore_remote_changes: + # "serialized_dashboard" locally and remotely will have different contents + # We only need to rely on etag here, and can skip this field for diff computation. + - field: serialized_dashboard + reason: etag_based + + # "dataset_catalog" and "dataset_schema" are write-only fields that are not returned by the server. + # They will always differ between local config (which has values) and remote state (which has empty strings). + - field: dataset_catalog + reason: input_only + - field: dataset_schema + reason: input_only diff --git a/bundle/direct/dresources/configs/database_catalogs.generated.yml b/bundle/direct/dresources/configs/database_catalogs.generated.yml new file mode 100644 index 00000000000..5eddcfd2a48 --- /dev/null +++ b/bundle/direct/dresources/configs/database_catalogs.generated.yml @@ -0,0 +1,8 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: create_database_if_not_exists + reason: spec:input_only + + - field: uid + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/database_catalogs.yml b/bundle/direct/dresources/configs/database_catalogs.yml new file mode 100644 index 00000000000..26eda6c377f --- /dev/null +++ b/bundle/direct/dresources/configs/database_catalogs.yml @@ -0,0 +1,11 @@ +provided_id_fields: + - field: name + reason: id_field +# The Database API has no UpdateDatabaseCatalog endpoint (the generated SDK +# method is a stub that returns 501 NOT_IMPLEMENTED), so this resource +# implements no DoUpdate and every settable field must recreate. The +# complementary ignore_remote_changes block in database_catalogs.generated.yml +# handles the read side (create_database_if_not_exists is input-only, uid is +# output-only) so no-op deploys stay idempotent. +recreate_on_changes: + - reason: immutable diff --git a/bundle/direct/dresources/configs/database_instances.generated.yml b/bundle/direct/dresources/configs/database_instances.generated.yml new file mode 100644 index 00000000000..66fdebab914 --- /dev/null +++ b/bundle/direct/dresources/configs/database_instances.generated.yml @@ -0,0 +1,78 @@ +# Generated, do not edit. + +recreate_on_changes: + - field: parent_instance_ref + reason: spec:immutable + +ignore_remote_changes: + - field: custom_tags + reason: spec:input_only + - field: custom_tags[*].key + reason: spec:input_only + - field: custom_tags[*].value + reason: spec:input_only + - field: enable_pg_native_login + reason: spec:input_only + - field: enable_readable_secondaries + reason: spec:input_only + - field: node_count + reason: spec:input_only + - field: parent_instance_ref.lsn + reason: spec:input_only + - field: retention_window_in_days + reason: spec:input_only + - field: stopped + reason: spec:input_only + - field: usage_policy_id + reason: spec:input_only + + - field: child_instance_refs + reason: spec:output_only + - field: child_instance_refs[*].branch_time + reason: spec:output_only + - field: child_instance_refs[*].effective_lsn + reason: spec:output_only + - field: child_instance_refs[*].lsn + reason: spec:output_only + - field: child_instance_refs[*].name + reason: spec:output_only + - field: child_instance_refs[*].uid + reason: spec:output_only + - field: creation_time + reason: spec:output_only + - field: creator + reason: spec:output_only + - field: effective_capacity + reason: spec:output_only + - field: effective_custom_tags + reason: spec:output_only + - field: effective_custom_tags[*].key + reason: spec:output_only + - field: effective_custom_tags[*].value + reason: spec:output_only + - field: effective_enable_pg_native_login + reason: spec:output_only + - field: effective_enable_readable_secondaries + reason: spec:output_only + - field: effective_node_count + reason: spec:output_only + - field: effective_retention_window_in_days + reason: spec:output_only + - field: effective_stopped + reason: spec:output_only + - field: effective_usage_policy_id + reason: spec:output_only + - field: parent_instance_ref.effective_lsn + reason: spec:output_only + - field: parent_instance_ref.uid + reason: spec:output_only + - field: pg_version + reason: spec:output_only + - field: read_only_dns + reason: spec:output_only + - field: read_write_dns + reason: spec:output_only + - field: state + reason: spec:output_only + - field: uid + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/database_instances.yml b/bundle/direct/dresources/configs/database_instances.yml new file mode 100644 index 00000000000..b6ab8dc9e8a --- /dev/null +++ b/bundle/direct/dresources/configs/database_instances.yml @@ -0,0 +1,3 @@ +provided_id_fields: + - field: name + reason: id_field diff --git a/bundle/direct/dresources/configs/experiments.generated.yml b/bundle/direct/dresources/configs/experiments.generated.yml new file mode 100644 index 00000000000..afd372d58f2 --- /dev/null +++ b/bundle/direct/dresources/configs/experiments.generated.yml @@ -0,0 +1,9 @@ +# Generated, do not edit. + +recreate_on_changes: + - field: trace_location + reason: spec:immutable + +ignore_remote_changes: + - field: trace_location.uc_trace_location.effective_table_prefix + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/experiments.yml b/bundle/direct/dresources/configs/experiments.yml new file mode 100644 index 00000000000..200bf0ffbf4 --- /dev/null +++ b/bundle/direct/dresources/configs/experiments.yml @@ -0,0 +1,15 @@ +# TF implementation: https://github.com/databricks/terraform-provider-databricks/blob/6c106e8e7052bb2726148d66309fd460ed444236/mlflow/resource_mlflow_experiment.go#L22 +recreate_on_changes: + - field: artifact_location + reason: immutable +backend_defaults: + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/mlflow/resource_mlflow_experiment.go#L34 + # SetForceNew().SetSuppressDiff(): backend generates artifact_location when not set by user + - field: artifact_location +ignore_remote_changes: + # Tags updates are not supported by TF. This mirrors that behaviour. + - field: tags + reason: terraform_compat +ignore_local_changes: + - field: tags + reason: terraform_compat diff --git a/bundle/direct/dresources/configs/external_locations.generated.yml b/bundle/direct/dresources/configs/external_locations.generated.yml new file mode 100644 index 00000000000..df2846b0a2f --- /dev/null +++ b/bundle/direct/dresources/configs/external_locations.generated.yml @@ -0,0 +1,19 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: effective_enable_file_events + reason: spec:output_only + - field: effective_file_event_queue + reason: spec:output_only + - field: file_event_queue.managed_aqs.managed_resource_id + reason: spec:output_only + - field: file_event_queue.managed_pubsub.managed_resource_id + reason: spec:output_only + - field: file_event_queue.managed_sqs.managed_resource_id + reason: spec:output_only + - field: file_event_queue.provided_aqs.managed_resource_id + reason: spec:output_only + - field: file_event_queue.provided_pubsub.managed_resource_id + reason: spec:output_only + - field: file_event_queue.provided_sqs.managed_resource_id + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/external_locations.yml b/bundle/direct/dresources/configs/external_locations.yml new file mode 100644 index 00000000000..55612ef68a6 --- /dev/null +++ b/bundle/direct/dresources/configs/external_locations.yml @@ -0,0 +1,10 @@ +recreate_on_changes: + - field: credential_name + reason: immutable + - field: encryption_details + reason: immutable + - field: file_event_queue + reason: immutable +updatable_id_fields: + - field: name + reason: id_changes diff --git a/bundle/direct/dresources/configs/genie_spaces.generated.yml b/bundle/direct/dresources/configs/genie_spaces.generated.yml new file mode 100644 index 00000000000..d9c3e63803d --- /dev/null +++ b/bundle/direct/dresources/configs/genie_spaces.generated.yml @@ -0,0 +1,5 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: etag + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/genie_spaces.yml b/bundle/direct/dresources/configs/genie_spaces.yml new file mode 100644 index 00000000000..383a88a6364 --- /dev/null +++ b/bundle/direct/dresources/configs/genie_spaces.yml @@ -0,0 +1,5 @@ +ignore_remote_changes: + # serialized_space locally (structured YAML) and remotely (JSON string) will differ + # textually, so we cannot meaningfully compare them for drift. + - field: serialized_space + reason: etag_based diff --git a/bundle/direct/dresources/configs/instance_pools.yml b/bundle/direct/dresources/configs/instance_pools.yml new file mode 100644 index 00000000000..35fec4fbbb4 --- /dev/null +++ b/bundle/direct/dresources/configs/instance_pools.yml @@ -0,0 +1,24 @@ +# Field behaviors follow the TF provider tags cross-referenced with the edit API (compute.EditInstancePool): +# https://github.com/databricks/terraform-provider-databricks/blob/main/pools/resource_instance_pool.go +ignore_remote_changes: + # Backend fills cloud defaults the user omits; treated as managed like clusters above. + - field: aws_attributes + reason: managed + - field: azure_attributes + reason: managed + - field: gcp_attributes + reason: managed +recreate_on_changes: + # force_new and not accepted by /instance-pools/edit. + - field: disk_spec + - field: node_type_flexibility + - field: preloaded_spark_versions + - field: preloaded_docker_images +backend_defaults: + # Defaults to true server-side. + - field: enable_elastic_disk + # Backend applies a default of 60 minutes when the field is omitted. + - field: idle_instance_autotermination_minutes + values: [60] + # GCP seeds max_capacity (1000) when omitted; unconstrained as the default is cloud-dependent. + - field: max_capacity diff --git a/bundle/direct/dresources/configs/internal_immutable_snapshots.yml b/bundle/direct/dresources/configs/internal_immutable_snapshots.yml new file mode 100644 index 00000000000..7fcfd7cc35c --- /dev/null +++ b/bundle/direct/dresources/configs/internal_immutable_snapshots.yml @@ -0,0 +1,2 @@ +recreate_on_changes: + - reason: immutable diff --git a/bundle/direct/dresources/configs/job_runs.yml b/bundle/direct/dresources/configs/job_runs.yml new file mode 100644 index 00000000000..3fc5c0e74d5 --- /dev/null +++ b/bundle/direct/dresources/configs/job_runs.yml @@ -0,0 +1,42 @@ +# Every jobs.RunNow field is listed, so nothing the request carries is drift. +# TestJobRunIgnoresEveryRequestField keeps the list in step with the SDK. +# result_state is not in the list: comparing it against the remote is what +# makes a run that has not succeeded visible to the plan. +ignore_remote_changes: + - field: job_id + reason: immutable + # GetRun reports job_parameters resolved against the job's defaults, and + # nests the rest under overriding_parameters. + - field: dbt_commands + reason: effective_vs_requested + - field: jar_params + reason: effective_vs_requested + - field: job_parameters + reason: effective_vs_requested + - field: notebook_params + reason: effective_vs_requested + - field: pipeline_params + reason: effective_vs_requested + - field: python_named_params + reason: effective_vs_requested + - field: python_params + reason: effective_vs_requested + - field: spark_submit_params + reason: effective_vs_requested + - field: sql_params + reason: effective_vs_requested + # Fields the run-now request carries that GetRun never reports back. + - field: idempotency_token + reason: not_returned_by_api + - field: only + reason: not_returned_by_api + - field: performance_target + reason: not_returned_by_api + - field: queue + reason: not_returned_by_api +# A run is immutable and fire-once, so any change recreates it. Omitting +# `field` matches every field (root; see TestFieldRuleOmittedIsRoot). +# `field: ""` would instead match nothing. The one exception is a run that is +# still going, which ResourceJobRun.OverrideChangeDesc downgrades to skip. +recreate_on_changes: + - reason: immutable diff --git a/bundle/direct/dresources/configs/jobs.yml b/bundle/direct/dresources/configs/jobs.yml new file mode 100644 index 00000000000..c1a9b5a5790 --- /dev/null +++ b/bundle/direct/dresources/configs/jobs.yml @@ -0,0 +1,146 @@ +# version_id is set to the current DMS deployment version on every deploy, so +# it changes constantly. Ignoring it as a local and remote change keeps that +# churn from driving an update or showing as drift on its own; when the job is +# updated for any other reason, DoUpdate sends the full config via Reset, so +# the current version_id is still recorded. deployment_id is intentionally +# left out: it is stable across versions, so a change to it is worth showing. +ignore_local_changes: + - field: deployment.version_id + reason: auto + +ignore_remote_changes: + - field: deployment.version_id + reason: auto + + # Same as clusters.{aws,azure,gcp}_attributes — see clusters/resource_cluster.go#L361-L363 + # s.SchemaPath("aws_attributes").SetSuppressDiff() + # s.SchemaPath("azure_attributes").SetSuppressDiff() + # s.SchemaPath("gcp_attributes").SetSuppressDiff() + - field: tasks[*].new_cluster.aws_attributes + reason: managed + - field: tasks[*].new_cluster.azure_attributes + reason: managed + - field: tasks[*].new_cluster.gcp_attributes + reason: managed + - field: tasks[*].for_each_task.task.new_cluster.aws_attributes + reason: managed + - field: tasks[*].for_each_task.task.new_cluster.azure_attributes + reason: managed + - field: tasks[*].for_each_task.task.new_cluster.gcp_attributes + reason: managed + - field: job_clusters[*].new_cluster.aws_attributes + reason: managed + - field: job_clusters[*].new_cluster.azure_attributes + reason: managed + - field: job_clusters[*].new_cluster.gcp_attributes + reason: managed + + # The Jobs API accepts apply_policy_default_values but does not return it + # in GET responses, so the remote is always nil even when the user sets it. + # Ignore remote-side nil so old==new (no user change) doesn't trigger drift. + - field: tasks[*].new_cluster.apply_policy_default_values + reason: not_returned_by_api + - field: tasks[*].for_each_task.task.new_cluster.apply_policy_default_values + reason: not_returned_by_api + - field: job_clusters[*].new_cluster.apply_policy_default_values + reason: not_returned_by_api + + # The Jobs API accepts trigger.table_update.condition but never returns it + # in GET responses (verified for both ANY_UPDATED and ALL_UPDATED), so + # without this every plan after a successful deploy reports an update. + # https://github.com/databricks/cli/issues/6315 + - field: trigger.table_update.condition + reason: input_only + + # Same field via the multi-trigger list. Untested: creating a job with + # "triggers" fails with 404 FEATURE_DISABLED ("The 'triggers' field is in + # private preview and is not enabled for this workspace"), so there is no + # invariant config for it. Both shapes carry the same + # TableUpdateTriggerConfiguration message and the backend builds it from + # the same expression on read, so the field is dropped here too. + - field: triggers[*].table_update.condition + reason: input_only + +ignore_remote_additions: + # A cluster policy supplies cluster settings server-side: "fixed" elements always, + # "defaultValue" elements when the request sets apply_policy_default_values. The bundle + # is not the source for those, so the remote spec is legitimately a superset of what the + # config declares and an added field is not drift. Measured backend behavior is pinned by + # acceptance/bundle/resources/jobs/cluster_policy (fixed_addition, default_flag). + # https://github.com/databricks/cli/issues/5179 + # https://github.com/databricks/cli/issues/6512 + - field: tasks[*].new_cluster + when_set: policy_id + - field: tasks[*].for_each_task.task.new_cluster + when_set: policy_id + - field: job_clusters[*].new_cluster + when_set: policy_id + +backend_defaults: + # Same as clusters.enable_elastic_disk — see clusters/resource_cluster.go#L331 + # s.SchemaPath("enable_elastic_disk").SetComputed() + - field: tasks[*].new_cluster.enable_elastic_disk + - field: tasks[*].for_each_task.task.new_cluster.enable_elastic_disk + - field: job_clusters[*].new_cluster.enable_elastic_disk + + # Same as clusters.enable_local_disk_encryption — see clusters/resource_cluster.go#L332 + # s.SchemaPath("enable_local_disk_encryption").SetComputed() + - field: tasks[*].new_cluster.enable_local_disk_encryption + - field: tasks[*].for_each_task.task.new_cluster.enable_local_disk_encryption + - field: job_clusters[*].new_cluster.enable_local_disk_encryption + + # Same as clusters.node_type_id — see clusters/resource_cluster.go#L333 + # s.SchemaPath("node_type_id").SetComputed() + - field: tasks[*].new_cluster.node_type_id + - field: tasks[*].for_each_task.task.new_cluster.node_type_id + - field: job_clusters[*].new_cluster.node_type_id + + # Same as clusters.driver_node_type_id — see clusters/resource_cluster.go#L334 + # s.SchemaPath("driver_node_type_id").SetComputed() + - field: tasks[*].new_cluster.driver_node_type_id + - field: tasks[*].for_each_task.task.new_cluster.driver_node_type_id + - field: job_clusters[*].new_cluster.driver_node_type_id + + # Same as clusters.driver_instance_pool_id — see clusters/resource_cluster.go#L335 + # s.SchemaPath("driver_instance_pool_id").SetComputed() + - field: tasks[*].new_cluster.driver_instance_pool_id + - field: tasks[*].for_each_task.task.new_cluster.driver_instance_pool_id + - field: job_clusters[*].new_cluster.driver_instance_pool_id + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L531 + # s.SchemaPath("format").SetComputed() + - field: format + values: ["MULTI_TASK", "SINGLE_TASK"] + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L639 + # s.SchemaPath("task", "run_if").SetSuppressDiffWithDefault(jobs.RunIfAllSuccess) + - field: "tasks[*].run_if" + values: ["ALL_SUCCESS"] + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L640 + # s.SchemaPath("task", "for_each_task", "task", "run_if").SetSuppressDiffWithDefault(jobs.RunIfAllSuccess) + - field: "tasks[*].for_each_task.task.run_if" + values: ["ALL_SUCCESS"] + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L527 + # s.SchemaPath("run_as").SetComputed() + - field: run_as + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L521-L524 + # s.SchemaPath("task", "notebook_task", "source").SetSuppressDiff() + # s.SchemaPath("task", "spark_python_task", "source").SetSuppressDiff() + # s.SchemaPath("task", "sql_task", "file", "source").SetSuppressDiff() + # s.SchemaPath("task", "dbt_task", "source").SetSuppressDiff() + - field: tasks[*].notebook_task.source + - field: tasks[*].for_each_task.task.notebook_task.source + - field: tasks[*].spark_python_task.source + - field: tasks[*].for_each_task.task.spark_python_task.source + - field: tasks[*].sql_task.file.source + - field: tasks[*].for_each_task.task.sql_task.file.source + - field: tasks[*].dbt_task.source + - field: tasks[*].for_each_task.task.dbt_task.source + + # Same as clusters.data_security_mode: backend sets this when not specified + - field: tasks[*].new_cluster.data_security_mode + - field: tasks[*].for_each_task.task.new_cluster.data_security_mode + - field: job_clusters[*].new_cluster.data_security_mode diff --git a/bundle/direct/dresources/configs/model_services.generated.yml b/bundle/direct/dresources/configs/model_services.generated.yml new file mode 100644 index 00000000000..1d2a16b848f --- /dev/null +++ b/bundle/direct/dresources/configs/model_services.generated.yml @@ -0,0 +1,15 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: config.inference_table.is_deleted + reason: spec:output_only + - field: config.inference_table.table + reason: spec:output_only + - field: config.routing.destinations[*].is_deleted + reason: spec:output_only + - field: config.routing.destinations[*].provisioned_throughput_config.model + reason: spec:output_only + - field: config.routing.fallback.destinations[*].is_deleted + reason: spec:output_only + - field: config.routing.fallback.destinations[*].provisioned_throughput_config.model + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/model_services.yml b/bundle/direct/dresources/configs/model_services.yml new file mode 100644 index 00000000000..d1b7a91e6e5 --- /dev/null +++ b/bundle/direct/dresources/configs/model_services.yml @@ -0,0 +1,10 @@ +provided_id_fields: + # parent + model_service_id compose the server-derived resource name + # (model-services/{catalog}.{schema}.{model_service}), which is the ID the + # resource is fetched by. Both are immutable; a local change recreates. + # DoRead reconstructs them from the returned name, so a remote-only + # difference can only be normalization and is skipped. + - field: parent + reason: id_field + - field: model_service_id + reason: id_field diff --git a/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml b/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml new file mode 100644 index 00000000000..71df6d6f832 --- /dev/null +++ b/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml @@ -0,0 +1,34 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: config.served_entities[*].burst_scaling_enabled + reason: spec:input_only + - field: config.served_entities[*].external_model.ai21labs_config.ai21labs_api_key_plaintext + reason: spec:input_only + - field: config.served_entities[*].external_model.amazon_bedrock_config.aws_access_key_id_plaintext + reason: spec:input_only + - field: config.served_entities[*].external_model.amazon_bedrock_config.aws_secret_access_key_plaintext + reason: spec:input_only + - field: config.served_entities[*].external_model.anthropic_config.anthropic_api_key_plaintext + reason: spec:input_only + - field: config.served_entities[*].external_model.cohere_config.cohere_api_key_plaintext + reason: spec:input_only + - field: config.served_entities[*].external_model.custom_provider_config.api_key_auth.value_plaintext + reason: spec:input_only + - field: config.served_entities[*].external_model.custom_provider_config.bearer_token_auth.token_plaintext + reason: spec:input_only + - field: config.served_entities[*].external_model.databricks_model_serving_config.databricks_api_token_plaintext + reason: spec:input_only + - field: config.served_entities[*].external_model.google_cloud_vertex_ai_config.private_key_plaintext + reason: spec:input_only + - field: config.served_entities[*].external_model.openai_config.microsoft_entra_client_secret_plaintext + reason: spec:input_only + - field: config.served_entities[*].external_model.openai_config.openai_api_key_plaintext + reason: spec:input_only + - field: config.served_entities[*].external_model.palm_config.palm_api_key_plaintext + reason: spec:input_only + - field: config.served_models[*].burst_scaling_enabled + reason: spec:input_only + + - field: telemetry_config.inference_table_config.name + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/model_serving_endpoints.yml b/bundle/direct/dresources/configs/model_serving_endpoints.yml new file mode 100644 index 00000000000..c6fab67b059 --- /dev/null +++ b/bundle/direct/dresources/configs/model_serving_endpoints.yml @@ -0,0 +1,59 @@ +# TF implementation: https://github.com/databricks/terraform-provider-databricks/blob/6c106e8e7052bb2726148d66309fd460ed444236/mlflow/resource_mlflow_experiment.go#L22 +provided_id_fields: + - field: name + reason: id_field +recreate_on_changes: + # description is immutable, can't be updated via API + - field: description + reason: immutable + - field: config.auto_capture_config.catalog_name + reason: immutable + - field: config.auto_capture_config.schema_name + reason: immutable + - field: config.auto_capture_config.table_name_prefix + reason: immutable + - field: route_optimized + reason: immutable +ignore_remote_changes: + # budget_policy_id is in ServingEndpointDetailed but GET never populates it + # (the API returns effective_budget_policy_id instead), so the remote always + # reports empty. This is not a backend default, so suppress remote changes. + - field: budget_policy_id + reason: no_update_api + # table_names creates a profile and is not returned; inference_table_config round-trips. + # The spec annotated this input_only in the v0.171 spec but no longer does, so the rule + # has to be manual again. + - field: telemetry_config.table_names + reason: input_only +ignore_local_changes: + - field: budget_policy_id + reason: no_update_api + - field: rate_limits + reason: not_implemented +backend_defaults: + # Remote-only telemetry is reported as a change at this parent path. + - field: telemetry_config + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L383 + # common.CustomizeSchemaPath(m, "config", "served_entities", "name").SetComputed() + - field: config.served_entities[*].name + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L384 + # common.CustomizeSchemaPath(m, "config", "served_entities", "workload_type").SetComputed() + - field: config.served_entities[*].workload_type + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L372 + # common.CustomizeSchemaPath(m, "config", "auto_capture_config", "enabled").SetComputed() + - field: config.auto_capture_config.enabled + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L395-L396 + # route_optimized is ForceNew; backend returns false when not set by user. + - field: route_optimized + values: [false] + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L370 + # common.CustomizeSchemaPath(m, "config", "traffic_config").SetComputed() + # The backend defaults traffic_config (100% to the served entity) when the user + # does not specify one. Suppress only that default: if the user sets traffic_config + # and the remote diverges, normal drift detection still applies. + - field: config.traffic_config diff --git a/bundle/direct/dresources/configs/models.yml b/bundle/direct/dresources/configs/models.yml new file mode 100644 index 00000000000..15032fac685 --- /dev/null +++ b/bundle/direct/dresources/configs/models.yml @@ -0,0 +1,18 @@ +provided_id_fields: + # Recreate matches current behavior of Terraform. It is possible to rename without recreate + # but that would require dynamic select of the method during update since + # the ml.RenameModel needs to be called instead of ml.UpdateModel. + # We might reasonably choose to never fix this because this is a legacy resource. + - field: name + reason: terraform_compat +# Allowing updates for tags requires dynamic selection of the method since +# tags can only be updated by calling ml.SetModelTag or ml.DeleteModelTag methods. +# Skip annotation matches the current behavior of Terraform where tags changes are showed +# in plan but are just ignored / not applied. Since this is a legacy resource we might +# reasonably choose to not fix it here as well. +ignore_remote_changes: + - field: tags + reason: terraform_compat +ignore_local_changes: + - field: tags + reason: terraform_compat diff --git a/bundle/direct/dresources/configs/pipelines.generated.yml b/bundle/direct/dresources/configs/pipelines.generated.yml new file mode 100644 index 00000000000..60639caed59 --- /dev/null +++ b/bundle/direct/dresources/configs/pipelines.generated.yml @@ -0,0 +1,5 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: ingestion_definition.source_type + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/pipelines.yml b/bundle/direct/dresources/configs/pipelines.yml new file mode 100644 index 00000000000..8136212a623 --- /dev/null +++ b/bundle/direct/dresources/configs/pipelines.yml @@ -0,0 +1,73 @@ +recreate_on_changes: + - field: storage + reason: immutable + - field: ingestion_definition.connection_name + reason: immutable + - field: ingestion_definition.ingestion_gateway_id + reason: immutable + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L204 + - field: gateway_definition.connection_id + reason: immutable + - field: gateway_definition.connection_name + reason: immutable + - field: gateway_definition.gateway_storage_catalog + reason: immutable + - field: gateway_definition.gateway_storage_schema + reason: immutable + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L209 + - field: ingestion_definition.ingest_from_uc_foreign_catalog + reason: immutable + +# See jobs above: version_id is set on every deploy, so it is ignored as a +# local/remote change. deployment_id is left out so a change to it still shows. +ignore_remote_changes: + - field: deployment.version_id + reason: auto + # "id" is handled in a special way before any fields changed + # However, it is also part of RemotePipeline via CreatePipeline. + # Thus it shows up as a remote change since we don't set on the object. + - field: id + reason: "!drop" + # QQQ should this be here? When run_as is explicitly set, the GET response echoes it back + # as a structured run_as.user_name (verified on e2-dogfood with a real user), so it may not + # be truly input-only. The explicit-set case could not be confirmed on aws-cli, azure-cli, + # or gcp-cli: those envs authenticate as a service principal that lacks servicePrincipal.user + # on itself, so it can't self-bind run_as. In the default (unset) case on all three clouds, + # GET returns only the flat run_as_user_name and no structured run_as. + - field: run_as + reason: input_only + # Carried by CreatePipeline/EditPipeline but never returned by GET, so remote + # always reads back false and a config value of true never converges. + - field: allow_duplicate_names + reason: input_only + +ignore_local_changes: + - field: deployment.version_id + reason: auto + # "id" is output-only, providing it in config would be a mistake + - field: id + reason: "!drop" + +backend_defaults: + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L238 + # s.SchemaPath("storage").SetCustomSuppressDiff(suppressStorageDiff) + # Backend generates storage path like dbfs:/pipelines/ when not set by user. + - field: storage + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L218 + # s.SchemaPath("cluster", "node_type_id").SetComputed() + - field: clusters[*].node_type_id + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L219 + # s.SchemaPath("cluster", "driver_node_type_id").SetComputed() + - field: clusters[*].driver_node_type_id + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L220 + # s.SchemaPath("cluster", "enable_local_disk_encryption").SetComputed() + - field: clusters[*].enable_local_disk_encryption + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L229-L230 + # s.SchemaPath("event_log", "catalog").SetComputed() + # s.SchemaPath("event_log", "schema").SetComputed() + - field: event_log.catalog + - field: event_log.schema diff --git a/bundle/direct/dresources/configs/postgres_branches.generated.yml b/bundle/direct/dresources/configs/postgres_branches.generated.yml new file mode 100644 index 00000000000..07e938094ce --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_branches.generated.yml @@ -0,0 +1,29 @@ +# Generated, do not edit. + +recreate_on_changes: + - field: source_branch + reason: spec:immutable + - field: source_branch_lsn + reason: spec:immutable + - field: source_branch_time + reason: spec:immutable + - field: source_snapshot + reason: spec:immutable + +ignore_remote_changes: + - field: expire_time + reason: spec:input_only + - field: is_protected + reason: spec:input_only + - field: no_expiry + reason: spec:input_only + - field: source_branch + reason: spec:input_only + - field: source_branch_lsn + reason: spec:input_only + - field: source_branch_time + reason: spec:input_only + - field: source_snapshot + reason: spec:input_only + - field: ttl + reason: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_branches.yml b/bundle/direct/dresources/configs/postgres_branches.yml new file mode 100644 index 00000000000..bbd344f4fdc --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_branches.yml @@ -0,0 +1,10 @@ +provided_id_fields: + # parent and branch_id are immutable (part of hierarchical name, not in API spec) + - field: parent + reason: id_field + - field: branch_id + reason: id_field +ignore_local_changes: + # replace_existing only takes effect on create; toggling it later is a no-op. + - field: replace_existing + reason: "input_only; cannot be updated after create" diff --git a/bundle/direct/dresources/configs/postgres_catalogs.generated.yml b/bundle/direct/dresources/configs/postgres_catalogs.generated.yml new file mode 100644 index 00000000000..63f0569c669 --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_catalogs.generated.yml @@ -0,0 +1,13 @@ +# Generated, do not edit. + +recreate_on_changes: + - field: postgres_database + reason: spec:immutable + +ignore_remote_changes: + - field: branch + reason: spec:input_only + - field: create_database_if_missing + reason: spec:input_only + - field: postgres_database + reason: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_catalogs.yml b/bundle/direct/dresources/configs/postgres_catalogs.yml new file mode 100644 index 00000000000..c4f6945071f --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_catalogs.yml @@ -0,0 +1,10 @@ +provided_id_fields: + # catalog_id is part of the hierarchical name and immutable. + - field: catalog_id + reason: id_field +recreate_on_changes: + # The Postgres SDK has no UpdateCatalog endpoint, so any local change + # requires delete+create. The OpenAPI spec only marks postgres_database + # as IMMUTABLE (handled by autogen); branch and create_database_if_missing + # need explicit entries here. + - reason: immutable diff --git a/bundle/direct/dresources/configs/postgres_databases.generated.yml b/bundle/direct/dresources/configs/postgres_databases.generated.yml new file mode 100644 index 00000000000..9a9eb664188 --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_databases.generated.yml @@ -0,0 +1,7 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: postgres_database + reason: spec:input_only + - field: role + reason: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_databases.yml b/bundle/direct/dresources/configs/postgres_databases.yml new file mode 100644 index 00000000000..504f0962174 --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_databases.yml @@ -0,0 +1,10 @@ +provided_id_fields: + # parent and database_id are immutable (part of hierarchical name, not in API spec) + - field: parent + reason: id_field + - field: database_id + reason: id_field +ignore_local_changes: + # replace_existing only takes effect on create; toggling it later is a no-op. + - field: replace_existing + reason: "input_only; cannot be updated after create" diff --git a/bundle/direct/dresources/configs/postgres_endpoints.generated.yml b/bundle/direct/dresources/configs/postgres_endpoints.generated.yml new file mode 100644 index 00000000000..2560c7f5ef7 --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_endpoints.generated.yml @@ -0,0 +1,23 @@ +# Generated, do not edit. + +recreate_on_changes: + - field: endpoint_type + reason: spec:immutable + +ignore_remote_changes: + - field: autoscaling_limit_max_cu + reason: spec:input_only + - field: autoscaling_limit_min_cu + reason: spec:input_only + - field: disabled + reason: spec:input_only + - field: endpoint_type + reason: spec:input_only + - field: group + reason: spec:input_only + - field: no_suspension + reason: spec:input_only + - field: settings + reason: spec:input_only + - field: suspend_timeout_duration + reason: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_endpoints.yml b/bundle/direct/dresources/configs/postgres_endpoints.yml new file mode 100644 index 00000000000..f5f09c323fe --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_endpoints.yml @@ -0,0 +1,10 @@ +provided_id_fields: + # parent and endpoint_id are immutable (part of hierarchical name, not in API spec) + - field: parent + reason: id_field + - field: endpoint_id + reason: id_field +ignore_local_changes: + # replace_existing only takes effect on create; toggling it later is a no-op. + - field: replace_existing + reason: "input_only; cannot be updated after create" diff --git a/bundle/direct/dresources/configs/postgres_projects.generated.yml b/bundle/direct/dresources/configs/postgres_projects.generated.yml new file mode 100644 index 00000000000..cf29cbf156a --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_projects.generated.yml @@ -0,0 +1,27 @@ +# Generated, do not edit. + +recreate_on_changes: + - field: pg_version + reason: spec:immutable + +ignore_remote_changes: + - field: budget_policy_id + reason: spec:input_only + - field: custom_tags + reason: spec:input_only + - field: custom_tags[*].key + reason: spec:input_only + - field: custom_tags[*].value + reason: spec:input_only + - field: default_branch + reason: spec:input_only + - field: default_endpoint_settings + reason: spec:input_only + - field: display_name + reason: spec:input_only + - field: enable_pg_native_login + reason: spec:input_only + - field: history_retention_duration + reason: spec:input_only + - field: pg_version + reason: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_projects.yml b/bundle/direct/dresources/configs/postgres_projects.yml new file mode 100644 index 00000000000..4ac5657a4b4 --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_projects.yml @@ -0,0 +1,4 @@ +provided_id_fields: + # project_id is immutable (part of hierarchical name, not in API spec) + - field: project_id + reason: id_field diff --git a/bundle/direct/dresources/configs/postgres_roles.generated.yml b/bundle/direct/dresources/configs/postgres_roles.generated.yml new file mode 100644 index 00000000000..d5b37104344 --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_roles.generated.yml @@ -0,0 +1,13 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: attributes + reason: spec:input_only + - field: auth_method + reason: spec:input_only + - field: identity_type + reason: spec:input_only + - field: membership_roles + reason: spec:input_only + - field: postgres_role + reason: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_roles.yml b/bundle/direct/dresources/configs/postgres_roles.yml new file mode 100644 index 00000000000..4a5a4715415 --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_roles.yml @@ -0,0 +1,21 @@ +ignore_local_changes: + # replace_existing only takes effect on create; toggling it later is a no-op. + - field: replace_existing + reason: "input_only; cannot be updated after create" +recreate_on_changes: + # parent and role_id are immutable (together they form the hierarchical name). + - field: parent + reason: immutable + - field: role_id + reason: immutable + # The PATCH update_mask only accepts spec.attributes and spec.membership_roles; + # the backend rejects spec.postgres_role, spec.auth_method, and spec.identity_type + # with 400 INVALID_PARAMETER_VALUE "Unknown field path in update_mask". These spec + # fields are not marked immutable in the OpenAPI definition yet, so the generator + # doesn't catch them — declare the constraint manually until upstream is fixed. + - field: postgres_role + reason: immutable + - field: auth_method + reason: immutable + - field: identity_type + reason: immutable diff --git a/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml b/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml new file mode 100644 index 00000000000..16cd441c48f --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml @@ -0,0 +1,6 @@ +provided_id_fields: + # branch composes the schedule's hierarchical name + # ("{branch}/snapshot-schedule"); changing it targets a different branch's + # schedule, so it recreates (delete + create). + - field: branch + reason: id_field diff --git a/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml b/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml new file mode 100644 index 00000000000..082f93bd66e --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml @@ -0,0 +1,41 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: accelerated_sync + reason: spec:input_only + - field: branch + reason: spec:input_only + - field: create_database_objects_if_missing + reason: spec:input_only + - field: existing_pipeline_id + reason: spec:input_only + - field: extra_columns + reason: spec:input_only + - field: extra_columns[*].column_name + reason: spec:input_only + - field: extra_columns[*].column_type + reason: spec:input_only + - field: extra_columns[*].compute + reason: spec:input_only + - field: extra_columns[*].maintenance + reason: spec:input_only + - field: new_pipeline_spec + reason: spec:input_only + - field: postgres_database + reason: spec:input_only + - field: primary_key_columns + reason: spec:input_only + - field: scheduling_policy + reason: spec:input_only + - field: source_table_full_name + reason: spec:input_only + - field: timeseries_key + reason: spec:input_only + - field: type_overrides + reason: spec:input_only + - field: type_overrides[*].column_name + reason: spec:input_only + - field: type_overrides[*].pg_type + reason: spec:input_only + - field: type_overrides[*].size + reason: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_synced_tables.yml b/bundle/direct/dresources/configs/postgres_synced_tables.yml new file mode 100644 index 00000000000..2a76ea3a253 --- /dev/null +++ b/bundle/direct/dresources/configs/postgres_synced_tables.yml @@ -0,0 +1,12 @@ +# The Postgres API has no UpdateSyncedTable endpoint, so every settable +# field is recreate-only on the intent side (local YAML edit -> delete + +# create). The complementary ignore_remote_changes block for this resource +# lives in postgres_synced_tables.generated.yml and handles the read side: it suppresses +# drift for the same fields because the GET API does not echo back the +# spec. Together they make no-op deploys idempotent while a real config +# edit still triggers a recreate. Same pattern as secret_scopes. +provided_id_fields: + - field: synced_table_id + reason: id_field +recreate_on_changes: + - reason: immutable diff --git a/bundle/direct/dresources/configs/quality_monitors.yml b/bundle/direct/dresources/configs/quality_monitors.yml new file mode 100644 index 00000000000..98f5b059016 --- /dev/null +++ b/bundle/direct/dresources/configs/quality_monitors.yml @@ -0,0 +1,6 @@ +provided_id_fields: + - field: table_name + reason: id_field +recreate_on_changes: + - field: assets_dir + reason: immutable diff --git a/bundle/direct/dresources/configs/registered_models.generated.yml b/bundle/direct/dresources/configs/registered_models.generated.yml new file mode 100644 index 00000000000..9e376d45246 --- /dev/null +++ b/bundle/direct/dresources/configs/registered_models.generated.yml @@ -0,0 +1,5 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: browse_only + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/registered_models.yml b/bundle/direct/dresources/configs/registered_models.yml new file mode 100644 index 00000000000..f5800e34dfa --- /dev/null +++ b/bundle/direct/dresources/configs/registered_models.yml @@ -0,0 +1,37 @@ +ignore_remote_changes: + # Output-only timestamp/user fields populated by the backend on read. + # The user never sets them, so remote-only differences are ignored here + # rather than zeroed in RemapState. + - field: created_at + reason: output_only + - field: created_by + reason: output_only + - field: updated_at + reason: output_only + - field: updated_by + reason: output_only + # Aliases are managed on model versions through a separate API, and DoRead + # passes IncludeAliases=false, so GET never echoes them back. Without this a + # config that sets aliases reports a perpetual update (remote stays empty). + - field: aliases + reason: input_only +provided_id_fields: + # The name can technically be updated without recreate. We recreate for now though + # to match TF implementation. + - field: name + reason: terraform_compat + - field: catalog_name + reason: id_field + - field: schema_name + reason: id_field +recreate_on_changes: + - field: storage_location + reason: immutable +backend_defaults: + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/catalog/resource_registered_model.go#L28 + # m["storage_location"].Computed = true + - field: storage_location + # owner, full_name, metastore_id are Computed in TF (backend-set output fields). + - field: owner + - field: full_name + - field: metastore_id diff --git a/bundle/direct/dresources/configs/schemas.yml b/bundle/direct/dresources/configs/schemas.yml new file mode 100644 index 00000000000..8ef27e71d15 --- /dev/null +++ b/bundle/direct/dresources/configs/schemas.yml @@ -0,0 +1,17 @@ +provided_id_fields: + # UC lowercases identifier names; remote returns "myschema" for config "MySchema". + - field: name + reason: id_field + - field: catalog_name + reason: id_field +recreate_on_changes: + - field: storage_root + reason: immutable +normalize_slash: + - field: storage_root + reason: uc_strips_trailing_slash +backend_defaults: + # UC auto-populates unity.catalog.managed..defaults.* keys after create. + # Without this, every subsequent plan produces an Update whose payload is empty, + # and UC rejects it with "UpdateSchema Nothing to update". + - field: properties['unity.catalog.managed.*.defaults.*'] diff --git a/bundle/direct/dresources/configs/secret_scopes.permissions.yml b/bundle/direct/dresources/configs/secret_scopes.permissions.yml new file mode 100644 index 00000000000..61c734eb8c1 --- /dev/null +++ b/bundle/direct/dresources/configs/secret_scopes.permissions.yml @@ -0,0 +1,6 @@ +# Permissions for secret scopes use ResourceSecretScopeAcls. +updatable_id_fields: + # When scope name changes, we need UpdateWithID trigger. This is necessary so that subsequent + # DoRead operations use the correct ID and we do not end up with a persistent drift. + - field: scope_name + reason: id_changes diff --git a/bundle/direct/dresources/configs/secret_scopes.yml b/bundle/direct/dresources/configs/secret_scopes.yml new file mode 100644 index 00000000000..ffde4b45f7e --- /dev/null +++ b/bundle/direct/dresources/configs/secret_scopes.yml @@ -0,0 +1,14 @@ +backend_defaults: + # The Secrets API defaults scope_backend_type to DATABRICKS when not specified. + - field: scope_backend_type + values: ["DATABRICKS"] +provided_id_fields: + - field: scope + reason: id_field +recreate_on_changes: + - field: scope_backend_type + reason: immutable + - field: backend_azure_keyvault + reason: immutable + - field: initial_manage_principal + reason: immutable diff --git a/bundle/direct/dresources/configs/secrets.generated.yml b/bundle/direct/dresources/configs/secrets.generated.yml new file mode 100644 index 00000000000..34877e8fb10 --- /dev/null +++ b/bundle/direct/dresources/configs/secrets.generated.yml @@ -0,0 +1,32 @@ +# Generated, do not edit. + +recreate_on_changes: + - field: catalog_name + reason: spec:immutable + - field: name + reason: spec:immutable + - field: schema_name + reason: spec:immutable + +ignore_remote_changes: + - field: owner + reason: spec:input_only + - field: value + reason: spec:input_only + + - field: create_time + reason: spec:output_only + - field: created_by + reason: spec:output_only + - field: effective_owner + reason: spec:output_only + - field: effective_value + reason: spec:output_only + - field: full_name + reason: spec:output_only + - field: metastore_id + reason: spec:output_only + - field: update_time + reason: spec:output_only + - field: updated_by + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/secrets.yml b/bundle/direct/dresources/configs/secrets.yml new file mode 100644 index 00000000000..22ca3d7f49f --- /dev/null +++ b/bundle/direct/dresources/configs/secrets.yml @@ -0,0 +1,3 @@ +sensitive_fields: + - field: value + - field: effective_value diff --git a/bundle/direct/dresources/configs/sql_warehouses.yml b/bundle/direct/dresources/configs/sql_warehouses.yml new file mode 100644 index 00000000000..582126d2d89 --- /dev/null +++ b/bundle/direct/dresources/configs/sql_warehouses.yml @@ -0,0 +1,37 @@ +ignore_remote_changes: + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L62 + # common.CustomizeSchemaPath(m, "channel").SetSuppressDiff() + - field: channel + reason: managed + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L82 + # common.CustomizeSchemaPath(m, "tags").SetSuppressDiff() + - field: tags + reason: managed + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L85-L87 + # common.CustomizeSchemaPath(m, "warehouse_type").SetSuppressDiff() + - field: warehouse_type + reason: managed + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L75 + # common.CustomizeSchemaPath(m, "min_num_clusters").SetSuppressDiff() + - field: min_num_clusters + reason: managed + + # creator_name is readonly, can't be updated via API + - field: creator_name + reason: output_only + +backend_defaults: + # lifecycle.started is derived from remote warehouse state in RemapState, so the + # remote side always has a value. When the user omits lifecycle from config, + # both old and new are nil and backend_defaults correctly skips the remote value. + # When the user explicitly sets lifecycle.started, old/new are non-nil and normal + # drift detection applies (e.g. detecting out-of-band stop). + - field: lifecycle + - field: lifecycle.started + + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L69 + # m["enable_serverless_compute"].Computed = true + - field: enable_serverless_compute diff --git a/bundle/direct/dresources/configs/synced_database_tables.generated.yml b/bundle/direct/dresources/configs/synced_database_tables.generated.yml new file mode 100644 index 00000000000..2958becd442 --- /dev/null +++ b/bundle/direct/dresources/configs/synced_database_tables.generated.yml @@ -0,0 +1,24 @@ +# Generated, do not edit. + +ignore_remote_changes: + - field: database_instance_name + reason: spec:input_only + - field: logical_database_name + reason: spec:input_only + - field: spec.accelerated_sync + reason: spec:input_only + - field: spec.create_database_objects_if_missing + reason: spec:input_only + - field: spec.existing_pipeline_id + reason: spec:input_only + - field: spec.new_pipeline_spec + reason: spec:input_only + + - field: data_synchronization_status + reason: spec:output_only + - field: effective_database_instance_name + reason: spec:output_only + - field: effective_logical_database_name + reason: spec:output_only + - field: unity_catalog_provisioning_state + reason: spec:output_only diff --git a/bundle/direct/dresources/configs/synced_database_tables.yml b/bundle/direct/dresources/configs/synced_database_tables.yml new file mode 100644 index 00000000000..deb5126e6e1 --- /dev/null +++ b/bundle/direct/dresources/configs/synced_database_tables.yml @@ -0,0 +1,11 @@ +provided_id_fields: + - field: name + reason: id_field +# The Database API has no UpdateSyncedDatabaseTable endpoint (the generated +# SDK method is a stub that returns 501 NOT_IMPLEMENTED), so this resource +# implements no DoUpdate and every settable field must recreate. The +# complementary ignore_remote_changes block in synced_database_tables.generated.yml +# handles the read side (input-only and output-only fields) so no-op deploys +# stay idempotent. Same pattern as postgres_synced_tables. +recreate_on_changes: + - reason: immutable diff --git a/bundle/direct/dresources/configs/vector_search_endpoints.yml b/bundle/direct/dresources/configs/vector_search_endpoints.yml new file mode 100644 index 00000000000..34fe753bd2a --- /dev/null +++ b/bundle/direct/dresources/configs/vector_search_endpoints.yml @@ -0,0 +1,12 @@ +provided_id_fields: + # The endpoint API has no rename; the endpoint is fetched by name. + - field: name + reason: id_field +recreate_on_changes: + - field: endpoint_type + reason: immutable +ignore_remote_changes: + # The API returns effective_budget_policy_id which may include inherited workspace policies, + # not the user-set budget_policy_id. Ignore until the API exposes the user-set value directly. + - field: budget_policy_id + reason: effective_vs_requested diff --git a/bundle/direct/dresources/configs/vector_search_indexes.yml b/bundle/direct/dresources/configs/vector_search_indexes.yml new file mode 100644 index 00000000000..e57c9d6f125 --- /dev/null +++ b/bundle/direct/dresources/configs/vector_search_indexes.yml @@ -0,0 +1,31 @@ +provided_id_fields: + - field: name + reason: id_field +recreate_on_changes: + # The index API has no rename or update path, so every config change + # has to go through delete + create. + - field: endpoint_name + reason: immutable + - field: index_type + reason: immutable + - field: index_subtype + reason: immutable + - field: primary_key + reason: immutable + - field: delta_sync_index_spec + reason: immutable + - field: direct_access_index_spec + reason: immutable +ignore_remote_changes: + # The backend rewrites schema_json on create: user-facing type names + # ("integer", "long", "short", "byte") are stored in Unity Catalog as + # Spark type names ("int", "bigint", "smallint", "tinyint") and the + # columns come back in sorted key order, so GET never echoes the user's + # literal input. Without this rule the rewrite reads as a change to the + # immutable direct_access_index_spec and plans a destructive recreate + # that drops all upserted vectors. + - field: direct_access_index_spec.schema_json + reason: normalized_by_backend +backend_defaults: + # The Vector Search API assigns index_subtype when the config omits it + - field: index_subtype diff --git a/bundle/direct/dresources/configs/volumes.yml b/bundle/direct/dresources/configs/volumes.yml new file mode 100644 index 00000000000..2968d978a83 --- /dev/null +++ b/bundle/direct/dresources/configs/volumes.yml @@ -0,0 +1,21 @@ +provided_id_fields: + - field: catalog_name + reason: id_field + - field: schema_name + reason: id_field +recreate_on_changes: + - field: storage_location + reason: immutable + - field: volume_type + reason: immutable +updatable_id_fields: + - field: name + reason: id_changes +normalize_slash: + # UC strips trailing slashes on create; matches the Terraform provider's suppressLocationDiff. + # https://github.com/databricks/terraform-provider-databricks/blob/v1.65.1/catalog/resource_volume.go#L25 + - field: storage_location + reason: uc_strips_trailing_slash +backend_defaults: + # storage_location is Computed; backend generates it for managed volumes. + - field: storage_location diff --git a/bundle/direct/dresources/dashboard.go b/bundle/direct/dresources/dashboard.go index 5103858974a..cdfd57c5623 100644 --- a/bundle/direct/dresources/dashboard.go +++ b/bundle/direct/dresources/dashboard.go @@ -106,7 +106,7 @@ func (r *ResourceDashboard) RemapState(state *DashboardState) *DashboardState { ForceSendFields: forceSendFields, // Output only fields. Remote changes to these are ignored via - // ignore_remote_changes in resources.yml rather than zeroed here. + // ignore_remote_changes in dashboards.yml rather than zeroed here. CreateTime: state.CreateTime, DashboardId: state.DashboardId, LifecycleState: state.LifecycleState, diff --git a/bundle/direct/dresources/job_run_test.go b/bundle/direct/dresources/job_run_test.go index 128256ecdd5..5da8feb39e0 100644 --- a/bundle/direct/dresources/job_run_test.go +++ b/bundle/direct/dresources/job_run_test.go @@ -310,7 +310,7 @@ func TestJobRunRemapStateCarriesTheOutcome(t *testing.T) { } } -// resources.yml ignores remote drift on everything the RunNow request carries, +// job_runs.yml ignores remote drift on everything the RunNow request carries, // since GetRun does not echo it back faithfully, and leaves result_state alone. func TestJobRunIgnoresEveryRequestField(t *testing.T) { adapters, err := InitAll(nil) diff --git a/bundle/direct/dresources/model_service.go b/bundle/direct/dresources/model_service.go index 9d9e0829662..7eb3860d191 100644 --- a/bundle/direct/dresources/model_service.go +++ b/bundle/direct/dresources/model_service.go @@ -112,7 +112,7 @@ func (r *ResourceModelService) DoCreate(ctx context.Context, config *resources.M } // DoUpdate sends update_mask "*" on every update. name, parent and -// model_service_id are immutable (recreate_on_changes in resources.yml), so the +// model_service_id are immutable (recreate_on_changes in model_services.yml), so the // wildcard replaces every client-settable field (comment + a full config // replace), matching the mask the Terraform provider generates. // diff --git a/bundle/direct/dresources/postgres_branch.go b/bundle/direct/dresources/postgres_branch.go index 96629c222ab..57bba56707d 100644 --- a/bundle/direct/dresources/postgres_branch.go +++ b/bundle/direct/dresources/postgres_branch.go @@ -79,7 +79,7 @@ func (*ResourcePostgresBranch) RemapState(remote *PostgresBranchRemote) *Postgre // makePostgresBranchRemote converts the SDK Branch into the embedded remote shape. // GET does not echo spec today (only status is returned); the embedded spec fields -// stay at their zero values, and resources.yml suppresses phantom drift via +// stay at their zero values, and postgres_branches.yml suppresses phantom drift via // ignore_remote_changes with reason spec:input_only. func makePostgresBranchRemote(branch *postgres.Branch) *PostgresBranchRemote { var spec postgres.BranchSpec diff --git a/bundle/direct/dresources/postgres_catalog.go b/bundle/direct/dresources/postgres_catalog.go index 2a35b9e7064..348ba88960b 100644 --- a/bundle/direct/dresources/postgres_catalog.go +++ b/bundle/direct/dresources/postgres_catalog.go @@ -62,7 +62,7 @@ func (*ResourcePostgresCatalog) RemapState(remote *PostgresCatalogRemote) *Postg // makePostgresCatalogRemote converts the SDK Catalog into the embedded remote shape. // GET does not echo spec today (only status is returned); the embedded spec fields -// stay at their zero values, and resources.yml suppresses phantom drift via +// stay at their zero values, and postgres_catalogs.yml suppresses phantom drift via // ignore_remote_changes with reason spec:input_only. // // The user-facing catalog id only appears as the trailing component of diff --git a/bundle/direct/dresources/postgres_database.go b/bundle/direct/dresources/postgres_database.go index fb5927b728d..613f5d90270 100644 --- a/bundle/direct/dresources/postgres_database.go +++ b/bundle/direct/dresources/postgres_database.go @@ -71,7 +71,7 @@ func (*ResourcePostgresDatabase) RemapState(remote *PostgresDatabaseRemote) *Pos // makePostgresDatabaseRemote converts the SDK Database into the embedded remote // shape. GET does not echo spec today (only status is returned); the embedded -// spec fields stay at their zero values, and resources.yml suppresses phantom +// spec fields stay at their zero values, and postgres_databases.yml suppresses phantom // drift via ignore_remote_changes with reason spec:input_only. func makePostgresDatabaseRemote(database *postgres.Database) *PostgresDatabaseRemote { var spec postgres.DatabaseDatabaseSpec diff --git a/bundle/direct/dresources/postgres_endpoint.go b/bundle/direct/dresources/postgres_endpoint.go index fbc0849401e..786f52b8b21 100644 --- a/bundle/direct/dresources/postgres_endpoint.go +++ b/bundle/direct/dresources/postgres_endpoint.go @@ -80,7 +80,7 @@ func (*ResourcePostgresEndpoint) RemapState(remote *PostgresEndpointRemote) *Pos // makePostgresEndpointRemote converts the SDK Endpoint into the embedded remote shape. // GET does not echo spec today (only status is returned); the embedded spec fields -// stay at their zero values, and resources.yml suppresses phantom drift via +// stay at their zero values, and postgres_endpoints.yml suppresses phantom drift via // ignore_remote_changes with reason spec:input_only. func makePostgresEndpointRemote(endpoint *postgres.Endpoint) *PostgresEndpointRemote { var spec postgres.EndpointSpec diff --git a/bundle/direct/dresources/postgres_project.go b/bundle/direct/dresources/postgres_project.go index 909e90cf972..bdb886e9437 100644 --- a/bundle/direct/dresources/postgres_project.go +++ b/bundle/direct/dresources/postgres_project.go @@ -73,7 +73,7 @@ func (*ResourcePostgresProject) RemapState(remote *PostgresProjectRemote) *Postg // makePostgresProjectRemote converts the SDK Project into the embedded remote shape. // GET does not echo spec today (only status is returned); the embedded spec fields -// stay at their zero values, and resources.yml suppresses phantom drift via +// stay at their zero values, and postgres_projects.yml suppresses phantom drift via // ignore_remote_changes with reason spec:input_only. func makePostgresProjectRemote(project *postgres.Project) *PostgresProjectRemote { var spec postgres.ProjectSpec diff --git a/bundle/direct/dresources/postgres_role.go b/bundle/direct/dresources/postgres_role.go index a0f72f29aa2..10ef250e37c 100644 --- a/bundle/direct/dresources/postgres_role.go +++ b/bundle/direct/dresources/postgres_role.go @@ -96,7 +96,7 @@ func (*ResourcePostgresRole) RemapState(remote *PostgresRoleRemote) *PostgresRol // makePostgresRoleRemote converts the SDK Role into the embedded remote shape. // GET does not echo spec today (only status is returned); the embedded spec fields -// stay at their zero values, and resources.yml suppresses phantom drift via +// stay at their zero values, and postgres_roles.yml suppresses phantom drift via // ignore_remote_changes with reason spec:input_only. func makePostgresRoleRemote(role *postgres.Role) *PostgresRoleRemote { var spec postgres.RoleRoleSpec diff --git a/bundle/direct/dresources/postgres_synced_table.go b/bundle/direct/dresources/postgres_synced_table.go index 0f07c33293e..a100fb54a43 100644 --- a/bundle/direct/dresources/postgres_synced_table.go +++ b/bundle/direct/dresources/postgres_synced_table.go @@ -62,7 +62,7 @@ func (*ResourcePostgresSyncedTable) RemapState(remote *PostgresSyncedTableRemote // makePostgresSyncedTableRemote converts the SDK SyncedTable into the embedded // remote shape. GET does not echo spec today (only status is returned); the -// embedded spec fields stay at their zero values, and resources.yml suppresses +// embedded spec fields stay at their zero values, and postgres_synced_tables.yml suppresses // phantom drift via ignore_remote_changes with reason spec:input_only. // // The synced-table API doesn't expose the user-facing id as a named field. It diff --git a/bundle/direct/dresources/registered_model.go b/bundle/direct/dresources/registered_model.go index e3c67b3d8cf..1a2fd468ebb 100644 --- a/bundle/direct/dresources/registered_model.go +++ b/bundle/direct/dresources/registered_model.go @@ -40,7 +40,7 @@ func (*ResourceRegisteredModel) RemapState(model *catalog.RegisteredModelInfo) * Owner: model.Owner, // Output only fields. Remote changes to these are ignored via - // ignore_remote_changes in resources.yml rather than zeroed here. + // ignore_remote_changes in registered_models.yml rather than zeroed here. CreatedAt: model.CreatedAt, CreatedBy: model.CreatedBy, UpdatedAt: model.UpdatedAt, diff --git a/bundle/direct/dresources/resources.generated.yml b/bundle/direct/dresources/resources.generated.yml deleted file mode 100644 index 5f7ee08501e..00000000000 --- a/bundle/direct/dresources/resources.generated.yml +++ /dev/null @@ -1,533 +0,0 @@ -# Generated, do not edit. API field behaviors from OpenAPI schema. -# -# For manual edits and schema description, see resources.yml. - -resources: - - alerts: - - ignore_remote_changes: - - field: create_time - reason: spec:output_only - - field: effective_run_as - reason: spec:output_only - - field: evaluation.last_evaluated_at - reason: spec:output_only - - field: evaluation.state - reason: spec:output_only - - field: id - reason: spec:output_only - - field: lifecycle_state - reason: spec:output_only - - field: owner_user_name - reason: spec:output_only - - field: update_time - reason: spec:output_only - - apps: - - ignore_remote_changes: - - field: git_repository.caller_credential_id - reason: spec:input_only - - field: git_source - reason: spec:input_only - - field: source_code_path - reason: spec:input_only - - - field: active_deployment - reason: spec:output_only - - field: app_status - reason: spec:output_only - - field: compute_status - reason: spec:output_only - - field: create_time - reason: spec:output_only - - field: creator - reason: spec:output_only - - field: default_git_source - reason: spec:output_only - - field: default_source_code_path - reason: spec:output_only - - field: effective_budget_policy_id - reason: spec:output_only - - field: effective_usage_policy_id - reason: spec:output_only - - field: effective_user_api_scopes - reason: spec:output_only - - field: id - reason: spec:output_only - - field: oauth2_app_client_id - reason: spec:output_only - - field: oauth2_app_integration_id - reason: spec:output_only - - field: pending_deployment - reason: spec:output_only - - field: resources[*].uc_securable.securable_kind - reason: spec:output_only - - field: service_principal_client_id - reason: spec:output_only - - field: service_principal_id - reason: spec:output_only - - field: service_principal_name - reason: spec:output_only - - field: thumbnail_url - reason: spec:output_only - - field: update_time - reason: spec:output_only - - field: updater - reason: spec:output_only - - field: url - reason: spec:output_only - - # catalogs: no api field behaviors - - # cluster_policies: no api field behaviors - - # clusters: no api field behaviors - - dashboards: - - recreate_on_changes: - - field: parent_path - reason: spec:immutable - - ignore_remote_changes: - - field: create_time - reason: spec:output_only - - field: dashboard_id - reason: spec:output_only - - field: lifecycle_state - reason: spec:output_only - - field: path - reason: spec:output_only - - field: update_time - reason: spec:output_only - - database_catalogs: - - ignore_remote_changes: - - field: create_database_if_not_exists - reason: spec:input_only - - - field: uid - reason: spec:output_only - - database_instances: - - recreate_on_changes: - - field: parent_instance_ref - reason: spec:immutable - - ignore_remote_changes: - - field: custom_tags - reason: spec:input_only - - field: custom_tags[*].key - reason: spec:input_only - - field: custom_tags[*].value - reason: spec:input_only - - field: enable_pg_native_login - reason: spec:input_only - - field: enable_readable_secondaries - reason: spec:input_only - - field: node_count - reason: spec:input_only - - field: parent_instance_ref.lsn - reason: spec:input_only - - field: retention_window_in_days - reason: spec:input_only - - field: stopped - reason: spec:input_only - - field: usage_policy_id - reason: spec:input_only - - - field: child_instance_refs - reason: spec:output_only - - field: child_instance_refs[*].branch_time - reason: spec:output_only - - field: child_instance_refs[*].effective_lsn - reason: spec:output_only - - field: child_instance_refs[*].lsn - reason: spec:output_only - - field: child_instance_refs[*].name - reason: spec:output_only - - field: child_instance_refs[*].uid - reason: spec:output_only - - field: creation_time - reason: spec:output_only - - field: creator - reason: spec:output_only - - field: effective_capacity - reason: spec:output_only - - field: effective_custom_tags - reason: spec:output_only - - field: effective_custom_tags[*].key - reason: spec:output_only - - field: effective_custom_tags[*].value - reason: spec:output_only - - field: effective_enable_pg_native_login - reason: spec:output_only - - field: effective_enable_readable_secondaries - reason: spec:output_only - - field: effective_node_count - reason: spec:output_only - - field: effective_retention_window_in_days - reason: spec:output_only - - field: effective_stopped - reason: spec:output_only - - field: effective_usage_policy_id - reason: spec:output_only - - field: parent_instance_ref.effective_lsn - reason: spec:output_only - - field: parent_instance_ref.uid - reason: spec:output_only - - field: pg_version - reason: spec:output_only - - field: read_only_dns - reason: spec:output_only - - field: read_write_dns - reason: spec:output_only - - field: state - reason: spec:output_only - - field: uid - reason: spec:output_only - - experiments: - - recreate_on_changes: - - field: trace_location - reason: spec:immutable - - ignore_remote_changes: - - field: trace_location.uc_trace_location.effective_table_prefix - reason: spec:output_only - - external_locations: - - ignore_remote_changes: - - field: effective_enable_file_events - reason: spec:output_only - - field: effective_file_event_queue - reason: spec:output_only - - field: file_event_queue.managed_aqs.managed_resource_id - reason: spec:output_only - - field: file_event_queue.managed_pubsub.managed_resource_id - reason: spec:output_only - - field: file_event_queue.managed_sqs.managed_resource_id - reason: spec:output_only - - field: file_event_queue.provided_aqs.managed_resource_id - reason: spec:output_only - - field: file_event_queue.provided_pubsub.managed_resource_id - reason: spec:output_only - - field: file_event_queue.provided_sqs.managed_resource_id - reason: spec:output_only - - genie_spaces: - - ignore_remote_changes: - - field: etag - reason: spec:output_only - - # instance_pools: no api field behaviors - - # job_runs: no api field behaviors - - # jobs: no api field behaviors - - model_services: - - ignore_remote_changes: - - field: config.inference_table.is_deleted - reason: spec:output_only - - field: config.inference_table.table - reason: spec:output_only - - field: config.routing.destinations[*].is_deleted - reason: spec:output_only - - field: config.routing.destinations[*].provisioned_throughput_config.model - reason: spec:output_only - - field: config.routing.fallback.destinations[*].is_deleted - reason: spec:output_only - - field: config.routing.fallback.destinations[*].provisioned_throughput_config.model - reason: spec:output_only - - model_serving_endpoints: - - ignore_remote_changes: - - field: config.served_entities[*].burst_scaling_enabled - reason: spec:input_only - - field: config.served_entities[*].external_model.ai21labs_config.ai21labs_api_key_plaintext - reason: spec:input_only - - field: config.served_entities[*].external_model.amazon_bedrock_config.aws_access_key_id_plaintext - reason: spec:input_only - - field: config.served_entities[*].external_model.amazon_bedrock_config.aws_secret_access_key_plaintext - reason: spec:input_only - - field: config.served_entities[*].external_model.anthropic_config.anthropic_api_key_plaintext - reason: spec:input_only - - field: config.served_entities[*].external_model.cohere_config.cohere_api_key_plaintext - reason: spec:input_only - - field: config.served_entities[*].external_model.custom_provider_config.api_key_auth.value_plaintext - reason: spec:input_only - - field: config.served_entities[*].external_model.custom_provider_config.bearer_token_auth.token_plaintext - reason: spec:input_only - - field: config.served_entities[*].external_model.databricks_model_serving_config.databricks_api_token_plaintext - reason: spec:input_only - - field: config.served_entities[*].external_model.google_cloud_vertex_ai_config.private_key_plaintext - reason: spec:input_only - - field: config.served_entities[*].external_model.openai_config.microsoft_entra_client_secret_plaintext - reason: spec:input_only - - field: config.served_entities[*].external_model.openai_config.openai_api_key_plaintext - reason: spec:input_only - - field: config.served_entities[*].external_model.palm_config.palm_api_key_plaintext - reason: spec:input_only - - field: config.served_models[*].burst_scaling_enabled - reason: spec:input_only - - - field: telemetry_config.inference_table_config.name - reason: spec:output_only - - # models: no api field behaviors - - pipelines: - - ignore_remote_changes: - - field: ingestion_definition.source_type - reason: spec:output_only - - postgres_branches: - - recreate_on_changes: - - field: source_branch - reason: spec:immutable - - field: source_branch_lsn - reason: spec:immutable - - field: source_branch_time - reason: spec:immutable - - field: source_snapshot - reason: spec:immutable - - ignore_remote_changes: - - field: expire_time - reason: spec:input_only - - field: is_protected - reason: spec:input_only - - field: no_expiry - reason: spec:input_only - - field: source_branch - reason: spec:input_only - - field: source_branch_lsn - reason: spec:input_only - - field: source_branch_time - reason: spec:input_only - - field: source_snapshot - reason: spec:input_only - - field: ttl - reason: spec:input_only - - postgres_catalogs: - - recreate_on_changes: - - field: postgres_database - reason: spec:immutable - - ignore_remote_changes: - - field: branch - reason: spec:input_only - - field: create_database_if_missing - reason: spec:input_only - - field: postgres_database - reason: spec:input_only - - postgres_databases: - - ignore_remote_changes: - - field: postgres_database - reason: spec:input_only - - field: role - reason: spec:input_only - - postgres_endpoints: - - recreate_on_changes: - - field: endpoint_type - reason: spec:immutable - - ignore_remote_changes: - - field: autoscaling_limit_max_cu - reason: spec:input_only - - field: autoscaling_limit_min_cu - reason: spec:input_only - - field: disabled - reason: spec:input_only - - field: endpoint_type - reason: spec:input_only - - field: group - reason: spec:input_only - - field: no_suspension - reason: spec:input_only - - field: settings - reason: spec:input_only - - field: suspend_timeout_duration - reason: spec:input_only - - postgres_projects: - - recreate_on_changes: - - field: pg_version - reason: spec:immutable - - ignore_remote_changes: - - field: budget_policy_id - reason: spec:input_only - - field: custom_tags - reason: spec:input_only - - field: custom_tags[*].key - reason: spec:input_only - - field: custom_tags[*].value - reason: spec:input_only - - field: default_branch - reason: spec:input_only - - field: default_endpoint_settings - reason: spec:input_only - - field: display_name - reason: spec:input_only - - field: enable_pg_native_login - reason: spec:input_only - - field: history_retention_duration - reason: spec:input_only - - field: pg_version - reason: spec:input_only - - postgres_roles: - - ignore_remote_changes: - - field: attributes - reason: spec:input_only - - field: auth_method - reason: spec:input_only - - field: identity_type - reason: spec:input_only - - field: membership_roles - reason: spec:input_only - - field: postgres_role - reason: spec:input_only - - # postgres_snapshot_schedules: no api field behaviors - - postgres_synced_tables: - - ignore_remote_changes: - - field: accelerated_sync - reason: spec:input_only - - field: branch - reason: spec:input_only - - field: create_database_objects_if_missing - reason: spec:input_only - - field: existing_pipeline_id - reason: spec:input_only - - field: extra_columns - reason: spec:input_only - - field: extra_columns[*].column_name - reason: spec:input_only - - field: extra_columns[*].column_type - reason: spec:input_only - - field: extra_columns[*].compute - reason: spec:input_only - - field: extra_columns[*].maintenance - reason: spec:input_only - - field: new_pipeline_spec - reason: spec:input_only - - field: postgres_database - reason: spec:input_only - - field: primary_key_columns - reason: spec:input_only - - field: scheduling_policy - reason: spec:input_only - - field: source_table_full_name - reason: spec:input_only - - field: timeseries_key - reason: spec:input_only - - field: type_overrides - reason: spec:input_only - - field: type_overrides[*].column_name - reason: spec:input_only - - field: type_overrides[*].pg_type - reason: spec:input_only - - field: type_overrides[*].size - reason: spec:input_only - - # quality_monitors: no api field behaviors - - registered_models: - - ignore_remote_changes: - - field: browse_only - reason: spec:output_only - - # schemas: no api field behaviors - - # secret_scopes: no api field behaviors - - secrets: - - recreate_on_changes: - - field: catalog_name - reason: spec:immutable - - field: name - reason: spec:immutable - - field: schema_name - reason: spec:immutable - - ignore_remote_changes: - - field: owner - reason: spec:input_only - - field: value - reason: spec:input_only - - - field: create_time - reason: spec:output_only - - field: created_by - reason: spec:output_only - - field: effective_owner - reason: spec:output_only - - field: effective_value - reason: spec:output_only - - field: full_name - reason: spec:output_only - - field: metastore_id - reason: spec:output_only - - field: update_time - reason: spec:output_only - - field: updated_by - reason: spec:output_only - - # sql_warehouses: no api field behaviors - - synced_database_tables: - - ignore_remote_changes: - - field: database_instance_name - reason: spec:input_only - - field: logical_database_name - reason: spec:input_only - - field: spec.accelerated_sync - reason: spec:input_only - - field: spec.create_database_objects_if_missing - reason: spec:input_only - - field: spec.existing_pipeline_id - reason: spec:input_only - - field: spec.new_pipeline_spec - reason: spec:input_only - - - field: data_synchronization_status - reason: spec:output_only - - field: effective_database_instance_name - reason: spec:output_only - - field: effective_logical_database_name - reason: spec:output_only - - field: unity_catalog_provisioning_state - reason: spec:output_only - - # vector_search_endpoints: no api field behaviors - - # vector_search_indexes: no api field behaviors - - # volumes: no api field behaviors diff --git a/bundle/direct/dresources/resources.yml b/bundle/direct/dresources/resources.yml deleted file mode 100644 index 1352b9c7491..00000000000 --- a/bundle/direct/dresources/resources.yml +++ /dev/null @@ -1,908 +0,0 @@ -# Resource lifecycle configuration for Declarative Automation Bundles. -# This file defines how field changes affect resource operations. -# -# Available options: -# recreate_on_changes: fields that trigger delete + create -# provided_id_fields: fields composing the name-based ID the resource is fetched by. -# Local changes trigger delete + create. Remote-only differences are skipped: -# a successful get-by-ID means a differing remote value can only be backend -# normalization; a real out-of-band rename would 404 (handled as resource-gone). -# updatable_id_fields: like provided_id_fields, but a local change triggers -# UpdateWithID (rename; the ID changes) instead of delete + create. Remote-only -# differences are skipped the same way (only local changes act). -# ignore_remote_changes: fields where remote changes are ignored -# ignore_local_changes: fields where local changes are ignored (can't be updated via API) -# backend_defaults: fields where the backend may set defaults (skipped when old/new are nil but remote is set) -# Optional "values" list constrains which remote values are allowed (as JSON-compatible literals). -# -# Each field entry has: -# field: the field path -# reason: why this field is in this category (immutable, input_only, output_only, or descriptive text) - -resources: - - jobs: - # version_id is set to the current DMS deployment version on every deploy, so - # it changes constantly. Ignoring it as a local and remote change keeps that - # churn from driving an update or showing as drift on its own; when the job is - # updated for any other reason, DoUpdate sends the full config via Reset, so - # the current version_id is still recorded. deployment_id is intentionally - # left out: it is stable across versions, so a change to it is worth showing. - ignore_local_changes: - - field: deployment.version_id - reason: auto - - ignore_remote_changes: - - field: deployment.version_id - reason: auto - - # Same as clusters.{aws,azure,gcp}_attributes — see clusters/resource_cluster.go#L361-L363 - # s.SchemaPath("aws_attributes").SetSuppressDiff() - # s.SchemaPath("azure_attributes").SetSuppressDiff() - # s.SchemaPath("gcp_attributes").SetSuppressDiff() - - field: tasks[*].new_cluster.aws_attributes - reason: managed - - field: tasks[*].new_cluster.azure_attributes - reason: managed - - field: tasks[*].new_cluster.gcp_attributes - reason: managed - - field: tasks[*].for_each_task.task.new_cluster.aws_attributes - reason: managed - - field: tasks[*].for_each_task.task.new_cluster.azure_attributes - reason: managed - - field: tasks[*].for_each_task.task.new_cluster.gcp_attributes - reason: managed - - field: job_clusters[*].new_cluster.aws_attributes - reason: managed - - field: job_clusters[*].new_cluster.azure_attributes - reason: managed - - field: job_clusters[*].new_cluster.gcp_attributes - reason: managed - - # The Jobs API accepts apply_policy_default_values but does not return it - # in GET responses, so the remote is always nil even when the user sets it. - # Ignore remote-side nil so old==new (no user change) doesn't trigger drift. - - field: tasks[*].new_cluster.apply_policy_default_values - reason: not_returned_by_api - - field: tasks[*].for_each_task.task.new_cluster.apply_policy_default_values - reason: not_returned_by_api - - field: job_clusters[*].new_cluster.apply_policy_default_values - reason: not_returned_by_api - - # The Jobs API accepts trigger.table_update.condition but never returns it - # in GET responses (verified for both ANY_UPDATED and ALL_UPDATED), so - # without this every plan after a successful deploy reports an update. - # https://github.com/databricks/cli/issues/6315 - - field: trigger.table_update.condition - reason: input_only - - # Same field via the multi-trigger list. Untested: creating a job with - # "triggers" fails with 404 FEATURE_DISABLED ("The 'triggers' field is in - # private preview and is not enabled for this workspace"), so there is no - # invariant config for it. Both shapes carry the same - # TableUpdateTriggerConfiguration message and the backend builds it from - # the same expression on read, so the field is dropped here too. - - field: triggers[*].table_update.condition - reason: input_only - - ignore_remote_additions: - # A cluster policy supplies cluster settings server-side: "fixed" elements always, - # "defaultValue" elements when the request sets apply_policy_default_values. The bundle - # is not the source for those, so the remote spec is legitimately a superset of what the - # config declares and an added field is not drift. Measured backend behavior is pinned by - # acceptance/bundle/resources/jobs/cluster_policy (fixed_addition, default_flag). - # https://github.com/databricks/cli/issues/5179 - # https://github.com/databricks/cli/issues/6512 - - field: tasks[*].new_cluster - when_set: policy_id - - field: tasks[*].for_each_task.task.new_cluster - when_set: policy_id - - field: job_clusters[*].new_cluster - when_set: policy_id - - backend_defaults: - # Same as clusters.enable_elastic_disk — see clusters/resource_cluster.go#L331 - # s.SchemaPath("enable_elastic_disk").SetComputed() - - field: tasks[*].new_cluster.enable_elastic_disk - - field: tasks[*].for_each_task.task.new_cluster.enable_elastic_disk - - field: job_clusters[*].new_cluster.enable_elastic_disk - - # Same as clusters.enable_local_disk_encryption — see clusters/resource_cluster.go#L332 - # s.SchemaPath("enable_local_disk_encryption").SetComputed() - - field: tasks[*].new_cluster.enable_local_disk_encryption - - field: tasks[*].for_each_task.task.new_cluster.enable_local_disk_encryption - - field: job_clusters[*].new_cluster.enable_local_disk_encryption - - # Same as clusters.node_type_id — see clusters/resource_cluster.go#L333 - # s.SchemaPath("node_type_id").SetComputed() - - field: tasks[*].new_cluster.node_type_id - - field: tasks[*].for_each_task.task.new_cluster.node_type_id - - field: job_clusters[*].new_cluster.node_type_id - - # Same as clusters.driver_node_type_id — see clusters/resource_cluster.go#L334 - # s.SchemaPath("driver_node_type_id").SetComputed() - - field: tasks[*].new_cluster.driver_node_type_id - - field: tasks[*].for_each_task.task.new_cluster.driver_node_type_id - - field: job_clusters[*].new_cluster.driver_node_type_id - - # Same as clusters.driver_instance_pool_id — see clusters/resource_cluster.go#L335 - # s.SchemaPath("driver_instance_pool_id").SetComputed() - - field: tasks[*].new_cluster.driver_instance_pool_id - - field: tasks[*].for_each_task.task.new_cluster.driver_instance_pool_id - - field: job_clusters[*].new_cluster.driver_instance_pool_id - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L531 - # s.SchemaPath("format").SetComputed() - - field: format - values: ["MULTI_TASK", "SINGLE_TASK"] - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L639 - # s.SchemaPath("task", "run_if").SetSuppressDiffWithDefault(jobs.RunIfAllSuccess) - - field: "tasks[*].run_if" - values: ["ALL_SUCCESS"] - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L640 - # s.SchemaPath("task", "for_each_task", "task", "run_if").SetSuppressDiffWithDefault(jobs.RunIfAllSuccess) - - field: "tasks[*].for_each_task.task.run_if" - values: ["ALL_SUCCESS"] - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L527 - # s.SchemaPath("run_as").SetComputed() - - field: run_as - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L521-L524 - # s.SchemaPath("task", "notebook_task", "source").SetSuppressDiff() - # s.SchemaPath("task", "spark_python_task", "source").SetSuppressDiff() - # s.SchemaPath("task", "sql_task", "file", "source").SetSuppressDiff() - # s.SchemaPath("task", "dbt_task", "source").SetSuppressDiff() - - field: tasks[*].notebook_task.source - - field: tasks[*].for_each_task.task.notebook_task.source - - field: tasks[*].spark_python_task.source - - field: tasks[*].for_each_task.task.spark_python_task.source - - field: tasks[*].sql_task.file.source - - field: tasks[*].for_each_task.task.sql_task.file.source - - field: tasks[*].dbt_task.source - - field: tasks[*].for_each_task.task.dbt_task.source - - # Same as clusters.data_security_mode: backend sets this when not specified - - field: tasks[*].new_cluster.data_security_mode - - field: tasks[*].for_each_task.task.new_cluster.data_security_mode - - field: job_clusters[*].new_cluster.data_security_mode - - job_runs: - # Every jobs.RunNow field is listed, so nothing the request carries is drift. - # TestJobRunIgnoresEveryRequestField keeps the list in step with the SDK. - # result_state is not in the list: comparing it against the remote is what - # makes a run that has not succeeded visible to the plan. - ignore_remote_changes: - - field: job_id - reason: immutable - # GetRun reports job_parameters resolved against the job's defaults, and - # nests the rest under overriding_parameters. - - field: dbt_commands - reason: effective_vs_requested - - field: jar_params - reason: effective_vs_requested - - field: job_parameters - reason: effective_vs_requested - - field: notebook_params - reason: effective_vs_requested - - field: pipeline_params - reason: effective_vs_requested - - field: python_named_params - reason: effective_vs_requested - - field: python_params - reason: effective_vs_requested - - field: spark_submit_params - reason: effective_vs_requested - - field: sql_params - reason: effective_vs_requested - # Fields the run-now request carries that GetRun never reports back. - - field: idempotency_token - reason: not_returned_by_api - - field: only - reason: not_returned_by_api - - field: performance_target - reason: not_returned_by_api - - field: queue - reason: not_returned_by_api - # A run is immutable and fire-once, so any change recreates it. Omitting - # `field` matches every field (root; see TestFieldRuleOmittedIsRoot). - # `field: ""` would instead match nothing. The one exception is a run that is - # still going, which ResourceJobRun.OverrideChangeDesc downgrades to skip. - recreate_on_changes: - - reason: immutable - - pipelines: - recreate_on_changes: - - field: storage - reason: immutable - - field: ingestion_definition.connection_name - reason: immutable - - field: ingestion_definition.ingestion_gateway_id - reason: immutable - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L204 - - field: gateway_definition.connection_id - reason: immutable - - field: gateway_definition.connection_name - reason: immutable - - field: gateway_definition.gateway_storage_catalog - reason: immutable - - field: gateway_definition.gateway_storage_schema - reason: immutable - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L209 - - field: ingestion_definition.ingest_from_uc_foreign_catalog - reason: immutable - - # See jobs above: version_id is set on every deploy, so it is ignored as a - # local/remote change. deployment_id is left out so a change to it still shows. - ignore_remote_changes: - - field: deployment.version_id - reason: auto - # "id" is handled in a special way before any fields changed - # However, it is also part of RemotePipeline via CreatePipeline. - # Thus it shows up as a remote change since we don't set on the object. - - field: id - reason: "!drop" - # QQQ should this be here? When run_as is explicitly set, the GET response echoes it back - # as a structured run_as.user_name (verified on e2-dogfood with a real user), so it may not - # be truly input-only. The explicit-set case could not be confirmed on aws-cli, azure-cli, - # or gcp-cli: those envs authenticate as a service principal that lacks servicePrincipal.user - # on itself, so it can't self-bind run_as. In the default (unset) case on all three clouds, - # GET returns only the flat run_as_user_name and no structured run_as. - - field: run_as - reason: input_only - # Carried by CreatePipeline/EditPipeline but never returned by GET, so remote - # always reads back false and a config value of true never converges. - - field: allow_duplicate_names - reason: input_only - - ignore_local_changes: - - field: deployment.version_id - reason: auto - # "id" is output-only, providing it in config would be a mistake - - field: id - reason: "!drop" - - backend_defaults: - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L238 - # s.SchemaPath("storage").SetCustomSuppressDiff(suppressStorageDiff) - # Backend generates storage path like dbfs:/pipelines/ when not set by user. - - field: storage - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L218 - # s.SchemaPath("cluster", "node_type_id").SetComputed() - - field: clusters[*].node_type_id - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L219 - # s.SchemaPath("cluster", "driver_node_type_id").SetComputed() - - field: clusters[*].driver_node_type_id - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L220 - # s.SchemaPath("cluster", "enable_local_disk_encryption").SetComputed() - - field: clusters[*].enable_local_disk_encryption - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L229-L230 - # s.SchemaPath("event_log", "catalog").SetComputed() - # s.SchemaPath("event_log", "schema").SetComputed() - - field: event_log.catalog - - field: event_log.schema - - models: - provided_id_fields: - # Recreate matches current behavior of Terraform. It is possible to rename without recreate - # but that would require dynamic select of the method during update since - # the ml.RenameModel needs to be called instead of ml.UpdateModel. - # We might reasonably choose to never fix this because this is a legacy resource. - - field: name - reason: terraform_compat - # Allowing updates for tags requires dynamic selection of the method since - # tags can only be updated by calling ml.SetModelTag or ml.DeleteModelTag methods. - # Skip annotation matches the current behavior of Terraform where tags changes are showed - # in plan but are just ignored / not applied. Since this is a legacy resource we might - # reasonably choose to not fix it here as well. - ignore_remote_changes: - - field: tags - reason: terraform_compat - ignore_local_changes: - - field: tags - reason: terraform_compat - - # TF implementation: https://github.com/databricks/terraform-provider-databricks/blob/6c106e8e7052bb2726148d66309fd460ed444236/mlflow/resource_mlflow_experiment.go#L22 - experiments: - recreate_on_changes: - - field: artifact_location - reason: immutable - backend_defaults: - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/mlflow/resource_mlflow_experiment.go#L34 - # SetForceNew().SetSuppressDiff(): backend generates artifact_location when not set by user - - field: artifact_location - ignore_remote_changes: - # Tags updates are not supported by TF. This mirrors that behaviour. - - field: tags - reason: terraform_compat - ignore_local_changes: - - field: tags - reason: terraform_compat - - # TF implementation: https://github.com/databricks/terraform-provider-databricks/blob/6c106e8e7052bb2726148d66309fd460ed444236/mlflow/resource_mlflow_experiment.go#L22 - model_serving_endpoints: - provided_id_fields: - - field: name - reason: id_field - recreate_on_changes: - # description is immutable, can't be updated via API - - field: description - reason: immutable - - field: config.auto_capture_config.catalog_name - reason: immutable - - field: config.auto_capture_config.schema_name - reason: immutable - - field: config.auto_capture_config.table_name_prefix - reason: immutable - - field: route_optimized - reason: immutable - ignore_remote_changes: - # budget_policy_id is in ServingEndpointDetailed but GET never populates it - # (the API returns effective_budget_policy_id instead), so the remote always - # reports empty. This is not a backend default, so suppress remote changes. - - field: budget_policy_id - reason: no_update_api - # table_names creates a profile and is not returned; inference_table_config round-trips. - # The spec annotated this input_only in the v0.171 spec but no longer does, so the rule - # has to be manual again. - - field: telemetry_config.table_names - reason: input_only - ignore_local_changes: - - field: budget_policy_id - reason: no_update_api - - field: rate_limits - reason: not_implemented - backend_defaults: - # Remote-only telemetry is reported as a change at this parent path. - - field: telemetry_config - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L383 - # common.CustomizeSchemaPath(m, "config", "served_entities", "name").SetComputed() - - field: config.served_entities[*].name - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L384 - # common.CustomizeSchemaPath(m, "config", "served_entities", "workload_type").SetComputed() - - field: config.served_entities[*].workload_type - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L372 - # common.CustomizeSchemaPath(m, "config", "auto_capture_config", "enabled").SetComputed() - - field: config.auto_capture_config.enabled - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L395-L396 - # route_optimized is ForceNew; backend returns false when not set by user. - - field: route_optimized - values: [false] - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L370 - # common.CustomizeSchemaPath(m, "config", "traffic_config").SetComputed() - # The backend defaults traffic_config (100% to the served entity) when the user - # does not specify one. Suppress only that default: if the user sets traffic_config - # and the remote diverges, normal drift detection still applies. - - field: config.traffic_config - - model_services: - provided_id_fields: - # parent + model_service_id compose the server-derived resource name - # (model-services/{catalog}.{schema}.{model_service}), which is the ID the - # resource is fetched by. Both are immutable; a local change recreates. - # DoRead reconstructs them from the returned name, so a remote-only - # difference can only be normalization and is skipped. - - field: parent - reason: id_field - - field: model_service_id - reason: id_field - - registered_models: - ignore_remote_changes: - # Output-only timestamp/user fields populated by the backend on read. - # The user never sets them, so remote-only differences are ignored here - # rather than zeroed in RemapState. - - field: created_at - reason: output_only - - field: created_by - reason: output_only - - field: updated_at - reason: output_only - - field: updated_by - reason: output_only - # Aliases are managed on model versions through a separate API, and DoRead - # passes IncludeAliases=false, so GET never echoes them back. Without this a - # config that sets aliases reports a perpetual update (remote stays empty). - - field: aliases - reason: input_only - provided_id_fields: - # The name can technically be updated without recreate. We recreate for now though - # to match TF implementation. - - field: name - reason: terraform_compat - - field: catalog_name - reason: id_field - - field: schema_name - reason: id_field - recreate_on_changes: - - field: storage_location - reason: immutable - backend_defaults: - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/catalog/resource_registered_model.go#L28 - # m["storage_location"].Computed = true - - field: storage_location - # owner, full_name, metastore_id are Computed in TF (backend-set output fields). - - field: owner - - field: full_name - - field: metastore_id - - quality_monitors: - provided_id_fields: - - field: table_name - reason: id_field - recreate_on_changes: - - field: assets_dir - reason: immutable - - catalogs: - recreate_on_changes: - - field: storage_root - reason: immutable - - field: connection_name - reason: immutable - - field: provider_name - reason: immutable - - field: share_name - reason: immutable - normalize_slash: - # UC strips a trailing slash from storage_root on create. The TF provider - # suppresses the same slash-only drift on this field (ucDirectoryPathSlashOnlySuppressDiff): - # https://github.com/databricks/terraform-provider-databricks/blob/main/catalog/resource_catalog.go - - field: storage_root - reason: uc_strips_trailing_slash - updatable_id_fields: - - field: name - reason: id_changes - backend_defaults: - # UC auto-populates unity.catalog.managed..defaults.* keys after create. - - field: properties['unity.catalog.managed.*.defaults.*'] - - schemas: - provided_id_fields: - # UC lowercases identifier names; remote returns "myschema" for config "MySchema". - - field: name - reason: id_field - - field: catalog_name - reason: id_field - recreate_on_changes: - - field: storage_root - reason: immutable - normalize_slash: - - field: storage_root - reason: uc_strips_trailing_slash - backend_defaults: - # UC auto-populates unity.catalog.managed..defaults.* keys after create. - # Without this, every subsequent plan produces an Update whose payload is empty, - # and UC rejects it with "UpdateSchema Nothing to update". - - field: properties['unity.catalog.managed.*.defaults.*'] - - external_locations: - recreate_on_changes: - - field: credential_name - reason: immutable - - field: encryption_details - reason: immutable - - field: file_event_queue - reason: immutable - updatable_id_fields: - - field: name - reason: id_changes - - volumes: - provided_id_fields: - - field: catalog_name - reason: id_field - - field: schema_name - reason: id_field - recreate_on_changes: - - field: storage_location - reason: immutable - - field: volume_type - reason: immutable - updatable_id_fields: - - field: name - reason: id_changes - normalize_slash: - # UC strips trailing slashes on create; matches the Terraform provider's suppressLocationDiff. - # https://github.com/databricks/terraform-provider-databricks/blob/v1.65.1/catalog/resource_volume.go#L25 - - field: storage_location - reason: uc_strips_trailing_slash - backend_defaults: - # storage_location is Computed; backend generates it for managed volumes. - - field: storage_location - - dashboards: - ignore_remote_changes: - # "serialized_dashboard" locally and remotely will have different contents - # We only need to rely on etag here, and can skip this field for diff computation. - - field: serialized_dashboard - reason: etag_based - - # "dataset_catalog" and "dataset_schema" are write-only fields that are not returned by the server. - # They will always differ between local config (which has values) and remote state (which has empty strings). - - field: dataset_catalog - reason: input_only - - field: dataset_schema - reason: input_only - - genie_spaces: - ignore_remote_changes: - # serialized_space locally (structured YAML) and remotely (JSON string) will differ - # textually, so we cannot meaningfully compare them for drift. - - field: serialized_space - reason: etag_based - - database_instances: - provided_id_fields: - - field: name - reason: id_field - - database_catalogs: - provided_id_fields: - - field: name - reason: id_field - # The Database API has no UpdateDatabaseCatalog endpoint (the generated SDK - # method is a stub that returns 501 NOT_IMPLEMENTED), so this resource - # implements no DoUpdate and every settable field must recreate. The - # complementary ignore_remote_changes block in resources.generated.yml - # handles the read side (create_database_if_not_exists is input-only, uid is - # output-only) so no-op deploys stay idempotent. - recreate_on_changes: - - reason: immutable - - synced_database_tables: - provided_id_fields: - - field: name - reason: id_field - # The Database API has no UpdateSyncedDatabaseTable endpoint (the generated - # SDK method is a stub that returns 501 NOT_IMPLEMENTED), so this resource - # implements no DoUpdate and every settable field must recreate. The - # complementary ignore_remote_changes block in resources.generated.yml - # handles the read side (input-only and output-only fields) so no-op deploys - # stay idempotent. Same pattern as postgres_synced_tables. - recreate_on_changes: - - reason: immutable - - apps: - provided_id_fields: - - field: name - reason: id_field - backend_defaults: - # Backend sets it "MEDIUM" when not specified in the config - - field: compute_size - # Backend enables token forwarding and reports true even when the config omits it, - # so an omitted field would otherwise plan an update on every deploy. An explicit - # value in the config still diffs normally. - - field: forward_user_access_token - # lifecycle.started is derived from remote compute status in RemapState, so the - # remote side always has a value. When the user omits lifecycle from config, - # both old and new are nil and backend_defaults correctly skips the remote value. - # When the user explicitly sets lifecycle.started, old/new are non-nil and normal - # drift detection applies (e.g. detecting out-of-band stop). - - field: lifecycle - - field: lifecycle.started - 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 - - secret_scopes: - backend_defaults: - # The Secrets API defaults scope_backend_type to DATABRICKS when not specified. - - field: scope_backend_type - values: ["DATABRICKS"] - provided_id_fields: - - field: scope - reason: id_field - recreate_on_changes: - - field: scope_backend_type - reason: immutable - - field: backend_azure_keyvault - reason: immutable - - field: initial_manage_principal - reason: immutable - - # Permissions for secret scopes use ResourceSecretScopeAcls. - secret_scopes.permissions: - updatable_id_fields: - # When scope name changes, we need UpdateWithID trigger. This is necessary so that subsequent - # DoRead operations use the correct ID and we do not end up with a persistent drift. - - field: scope_name - reason: id_changes - - secrets: - sensitive_fields: - - field: value - - field: effective_value - - clusters: - # A cluster policy supplies cluster settings server-side: "fixed" elements always, - # "defaultValue" elements when the request sets apply_policy_default_values. The bundle - # is not the source for those, so the remote spec is legitimately a superset of what the - # config declares and an added field is not drift. Measured backend behavior is pinned by - # acceptance/bundle/resources/jobs/cluster_policy (fixed_addition, default_flag). - # https://github.com/databricks/cli/issues/5179 - # https://github.com/databricks/cli/issues/6512 - ignore_remote_additions: - - when_set: policy_id - - ignore_remote_changes: - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L361-L363 - # s.SchemaPath("aws_attributes").SetSuppressDiff() - # s.SchemaPath("azure_attributes").SetSuppressDiff() - # s.SchemaPath("gcp_attributes").SetSuppressDiff() - - field: aws_attributes - reason: managed - - field: azure_attributes - reason: managed - - field: gcp_attributes - reason: managed - backend_defaults: - # lifecycle.started is derived from remote cluster state in RemapState, so the - # remote side always has a value. When the user omits lifecycle from config, - # both old and new are nil and backend_defaults correctly skips the remote value. - # When the user explicitly sets lifecycle.started, old/new are non-nil and normal - # drift detection applies (e.g. detecting out-of-band terminate). - - field: lifecycle - - field: lifecycle.started - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L331 - # s.SchemaPath("enable_elastic_disk").SetComputed() - - field: enable_elastic_disk - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L332 - # s.SchemaPath("enable_local_disk_encryption").SetComputed() - - field: enable_local_disk_encryption - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L333 - # s.SchemaPath("node_type_id").SetComputed() - - field: node_type_id - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L334 - # s.SchemaPath("driver_node_type_id").SetComputed() - - field: driver_node_type_id - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L335 - # s.SchemaPath("driver_instance_pool_id").SetComputed() - - field: driver_instance_pool_id - - # Terraform currently does not do this, but it is a field with backend default. - # See https://github.com/databricks/cli/issues/4418 - - field: single_user_name - - # We have custom handler for this in cluster.go - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L109-L118 - # DataSecurityModeDiffSuppressFunc: suppress when old != "" && new == "" - #- field: data_security_mode - - cluster_policies: - backend_defaults: - # A policy authored with policy_family_id and no definition gets its definition - # computed from the policy family by the backend and returned on read. Config - # leaves definition empty, so old/new are nil and backend_defaults skips the - # server-computed remote value; without this every deploy sees drift and re-Edits. - - field: definition - - instance_pools: - # Field behaviors follow the TF provider tags cross-referenced with the edit API (compute.EditInstancePool): - # https://github.com/databricks/terraform-provider-databricks/blob/main/pools/resource_instance_pool.go - ignore_remote_changes: - # Backend fills cloud defaults the user omits; treated as managed like clusters above. - - field: aws_attributes - reason: managed - - field: azure_attributes - reason: managed - - field: gcp_attributes - reason: managed - recreate_on_changes: - # force_new and not accepted by /instance-pools/edit. - - field: disk_spec - - field: node_type_flexibility - - field: preloaded_spark_versions - - field: preloaded_docker_images - backend_defaults: - # Defaults to true server-side. - - field: enable_elastic_disk - # Backend applies a default of 60 minutes when the field is omitted. - - field: idle_instance_autotermination_minutes - values: [60] - # GCP seeds max_capacity (1000) when omitted; unconstrained as the default is cloud-dependent. - - field: max_capacity - - sql_warehouses: - ignore_remote_changes: - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L62 - # common.CustomizeSchemaPath(m, "channel").SetSuppressDiff() - - field: channel - reason: managed - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L82 - # common.CustomizeSchemaPath(m, "tags").SetSuppressDiff() - - field: tags - reason: managed - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L85-L87 - # common.CustomizeSchemaPath(m, "warehouse_type").SetSuppressDiff() - - field: warehouse_type - reason: managed - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L75 - # common.CustomizeSchemaPath(m, "min_num_clusters").SetSuppressDiff() - - field: min_num_clusters - reason: managed - - # creator_name is readonly, can't be updated via API - - field: creator_name - reason: output_only - - backend_defaults: - # lifecycle.started is derived from remote warehouse state in RemapState, so the - # remote side always has a value. When the user omits lifecycle from config, - # both old and new are nil and backend_defaults correctly skips the remote value. - # When the user explicitly sets lifecycle.started, old/new are non-nil and normal - # drift detection applies (e.g. detecting out-of-band stop). - - field: lifecycle - - field: lifecycle.started - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L69 - # m["enable_serverless_compute"].Computed = true - - field: enable_serverless_compute - - postgres_projects: - provided_id_fields: - # project_id is immutable (part of hierarchical name, not in API spec) - - field: project_id - reason: id_field - - postgres_branches: - provided_id_fields: - # parent and branch_id are immutable (part of hierarchical name, not in API spec) - - field: parent - reason: id_field - - field: branch_id - reason: id_field - ignore_local_changes: - # replace_existing only takes effect on create; toggling it later is a no-op. - - field: replace_existing - reason: "input_only; cannot be updated after create" - - postgres_databases: - provided_id_fields: - # parent and database_id are immutable (part of hierarchical name, not in API spec) - - field: parent - reason: id_field - - field: database_id - reason: id_field - ignore_local_changes: - # replace_existing only takes effect on create; toggling it later is a no-op. - - field: replace_existing - reason: "input_only; cannot be updated after create" - - postgres_endpoints: - provided_id_fields: - # parent and endpoint_id are immutable (part of hierarchical name, not in API spec) - - field: parent - reason: id_field - - field: endpoint_id - reason: id_field - ignore_local_changes: - # replace_existing only takes effect on create; toggling it later is a no-op. - - field: replace_existing - reason: "input_only; cannot be updated after create" - - postgres_catalogs: - provided_id_fields: - # catalog_id is part of the hierarchical name and immutable. - - field: catalog_id - reason: id_field - recreate_on_changes: - # The Postgres SDK has no UpdateCatalog endpoint, so any local change - # requires delete+create. The OpenAPI spec only marks postgres_database - # as IMMUTABLE (handled by autogen); branch and create_database_if_missing - # need explicit entries here. - - reason: immutable - - postgres_synced_tables: - # The Postgres API has no UpdateSyncedTable endpoint, so every settable - # field is recreate-only on the intent side (local YAML edit -> delete + - # create). The complementary ignore_remote_changes block for this resource - # lives in resources.generated.yml and handles the read side: it suppresses - # drift for the same fields because the GET API does not echo back the - # spec. Together they make no-op deploys idempotent while a real config - # edit still triggers a recreate. Same pattern as secret_scopes. - provided_id_fields: - - field: synced_table_id - reason: id_field - recreate_on_changes: - - reason: immutable - - postgres_roles: - ignore_local_changes: - # replace_existing only takes effect on create; toggling it later is a no-op. - - field: replace_existing - reason: "input_only; cannot be updated after create" - recreate_on_changes: - # parent and role_id are immutable (together they form the hierarchical name). - - field: parent - reason: immutable - - field: role_id - reason: immutable - # The PATCH update_mask only accepts spec.attributes and spec.membership_roles; - # the backend rejects spec.postgres_role, spec.auth_method, and spec.identity_type - # with 400 INVALID_PARAMETER_VALUE "Unknown field path in update_mask". These spec - # fields are not marked immutable in the OpenAPI definition yet, so the generator - # doesn't catch them — declare the constraint manually until upstream is fixed. - - field: postgres_role - reason: immutable - - field: auth_method - reason: immutable - - field: identity_type - reason: immutable - - postgres_snapshot_schedules: - provided_id_fields: - # branch composes the schedule's hierarchical name - # ("{branch}/snapshot-schedule"); changing it targets a different branch's - # schedule, so it recreates (delete + create). - - field: branch - reason: id_field - - vector_search_endpoints: - provided_id_fields: - # The endpoint API has no rename; the endpoint is fetched by name. - - field: name - reason: id_field - recreate_on_changes: - - field: endpoint_type - reason: immutable - ignore_remote_changes: - # The API returns effective_budget_policy_id which may include inherited workspace policies, - # not the user-set budget_policy_id. Ignore until the API exposes the user-set value directly. - - field: budget_policy_id - reason: effective_vs_requested - - vector_search_indexes: - provided_id_fields: - - field: name - reason: id_field - recreate_on_changes: - # The index API has no rename or update path, so every config change - # has to go through delete + create. - - field: endpoint_name - reason: immutable - - field: index_type - reason: immutable - - field: index_subtype - reason: immutable - - field: primary_key - reason: immutable - - field: delta_sync_index_spec - reason: immutable - - field: direct_access_index_spec - reason: immutable - ignore_remote_changes: - # The backend rewrites schema_json on create: user-facing type names - # ("integer", "long", "short", "byte") are stored in Unity Catalog as - # Spark type names ("int", "bigint", "smallint", "tinyint") and the - # columns come back in sorted key order, so GET never echoes the user's - # literal input. Without this rule the rewrite reads as a change to the - # immutable direct_access_index_spec and plans a destructive recreate - # that drops all upserted vectors. - - field: direct_access_index_spec.schema_json - reason: normalized_by_backend - backend_defaults: - # The Vector Search API assigns index_subtype when the config omits it - - field: index_subtype - - internal_immutable_snapshots: - recreate_on_changes: - - reason: immutable diff --git a/bundle/direct/dresources/vector_search_index.go b/bundle/direct/dresources/vector_search_index.go index f7bd2f60f9f..39e29cb7b8e 100644 --- a/bundle/direct/dresources/vector_search_index.go +++ b/bundle/direct/dresources/vector_search_index.go @@ -182,7 +182,7 @@ func isIndexPendingDeletion(err error) bool { } // No DoUpdate: vector search indexes have no update API. All SDK fields are -// declared in resources.yml under recreate_on_changes or ignore_remote_changes. +// declared in vector_search_indexes.yml under recreate_on_changes or ignore_remote_changes. // If a future SDK bump adds a new field that isn't classified, the framework // rejects the resulting Update plan at bundle_plan.go (see also the reflection // test in vector_search_index_test.go which catches it earlier at unit-test time). diff --git a/bundle/direct/dresources/vector_search_index_test.go b/bundle/direct/dresources/vector_search_index_test.go index 55fe35c2287..4bb49eb18d9 100644 --- a/bundle/direct/dresources/vector_search_index_test.go +++ b/bundle/direct/dresources/vector_search_index_test.go @@ -40,7 +40,7 @@ func TestVectorSearchIndexAllSDKFieldsAreClassified(t *testing.T) { } jsonTag = strings.TrimSuffix(jsonTag, ",omitempty") assert.Truef(t, classified[jsonTag], - "field %q is not declared in resources.yml under vector_search_indexes; "+ + "field %q is not declared in vector_search_indexes.yml; "+ "vector_search_indexes has no update API, so every SDK field must be in "+ "recreate_on_changes, provided_id_fields or ignore_remote_changes", jsonTag, diff --git a/bundle/direct/tools/generate_resources.py b/bundle/direct/tools/generate_resources.py index edf9ce5910f..01302fedc79 100644 --- a/bundle/direct/tools/generate_resources.py +++ b/bundle/direct/tools/generate_resources.py @@ -5,7 +5,10 @@ # ] # /// """ -Generate resources.generated.yml from cli.json field behaviors. +Generate per-resource .generated.yml files from cli.json field behaviors. + +A resource with no field behaviors gets no file; a stale file left over from a +previous run is removed. """ import argparse @@ -140,7 +143,9 @@ def filter_prefixes(fields): def write_field_group(lines, header, fields): """Write a group of fields with field and reason, grouped by behavior.""" - lines.append(f"\n {header}:") + if lines: + lines.append("") + lines.append(f"{header}:") # Group by behavior by_behavior = {} for field, behavior in fields: @@ -152,51 +157,57 @@ def write_field_group(lines, header, fields): first = False reason = f"spec:{behavior.lower()}" for field in by_behavior[behavior]: - lines.append(f" - field: {field}") - lines.append(f" reason: {reason}") + lines.append(f" - field: {field}") + lines.append(f" reason: {reason}") -def generate(resource_behaviors): - """Generate resources.yml.""" - lines = [ - """# Generated, do not edit. API field behaviors from OpenAPI schema. -# -# For manual edits and schema description, see resources.yml. +GENERATED_SUFFIX = ".generated.yml" -resources:""" - ] +HEADER = "# Generated, do not edit." - for resource in sorted(resource_behaviors): - behaviors = resource_behaviors[resource] - ignore_remote, recreate = [], [] - for field, fb in sorted(behaviors.items()): - if "OUTPUT_ONLY" in fb: - ignore_remote.append((field, "OUTPUT_ONLY")) - elif "INPUT_ONLY" in fb: - ignore_remote.append((field, "INPUT_ONLY")) - if "IMMUTABLE" in fb: - recreate.append((field, "IMMUTABLE")) +def generate(behaviors): + """Render one resource's field behaviors, or "" if it has none.""" + ignore_remote, recreate = [], [] + for field, fb in sorted(behaviors.items()): + if "OUTPUT_ONLY" in fb: + ignore_remote.append((field, "OUTPUT_ONLY")) + elif "INPUT_ONLY" in fb: + ignore_remote.append((field, "INPUT_ONLY")) + if "IMMUTABLE" in fb: + recreate.append((field, "IMMUTABLE")) - ignore_remote = filter_prefixes(ignore_remote) - recreate = filter_prefixes(recreate) + ignore_remote = filter_prefixes(ignore_remote) + recreate = filter_prefixes(recreate) - if not ignore_remote and not recreate: - lines.append(f"\n # {resource}: no api field behaviors") - continue + if not ignore_remote and not recreate: + return "" - lines.append(f"\n {resource}:") + lines = [] + if recreate: + write_field_group(lines, "recreate_on_changes", recreate) + if ignore_remote: + write_field_group(lines, "ignore_remote_changes", ignore_remote) - if recreate: - write_field_group(lines, "recreate_on_changes", recreate) + return HEADER + "\n\n" + "\n".join(lines) + "\n" - if ignore_remote: - write_field_group(lines, "ignore_remote_changes", ignore_remote) - while lines and lines[-1] == "": - lines.pop() +def write_files(outdir, resource_behaviors): + """Write .generated.yml per resource, pruning files that are now empty.""" + written = set() + for resource, behaviors in sorted(resource_behaviors.items()): + content = generate(behaviors) + if not content: + continue + path = outdir / (resource + GENERATED_SUFFIX) + path.write_text(content) + written.add(path) + + for path in sorted(outdir.glob("*" + GENERATED_SUFFIX)): + if path not in written: + path.unlink() - return "\n".join(lines) + print(f"wrote {len(written)} files to {outdir}", file=sys.stderr) def main(): @@ -205,6 +216,7 @@ def main(): parser.add_argument("apitypes", type=Path, help="Path to apitypes.generated.yml file") parser.add_argument("apitypes_override", type=Path, help="Path to apitypes.yml override file") parser.add_argument("out_fields", type=Path, help="Path to out.fields.txt file") + parser.add_argument("outdir", type=Path, help="Directory to write .generated.yml files to") args = parser.parse_args() resource_types = parse_apitypes(args.apitypes, args.apitypes_override) @@ -222,7 +234,7 @@ def main(): print(f" {field}: {all_behaviors[field]}", file=sys.stderr) resource_behaviors[resource] = {f: b for f, b in all_behaviors.items() if f in fields} - print(generate(resource_behaviors)) + write_files(args.outdir, resource_behaviors) if __name__ == "__main__": diff --git a/libs/testserver/cluster_policies.go b/libs/testserver/cluster_policies.go index d18aee74fc3..ce29464a9e5 100644 --- a/libs/testserver/cluster_policies.go +++ b/libs/testserver/cluster_policies.go @@ -16,7 +16,7 @@ import ( // family has its definition computed from the family and returned on read, even // though the config never sets definition. One fixed key is enough to reproduce a // non-empty server-computed definition so tests exercise the backend_defaults -// suppression for definition (see resources.yml cluster_policies). +// suppression for definition (see cluster_policies.yml). func policyFamilyDefinition(familyID string) string { return fmt.Sprintf(`{"policy_family":{"type":"fixed","value":%q}}`, familyID) }