From 8ae0d23e4c87562719580c6ce895e2f7704059ca Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 29 Sep 2026 13:55:39 +0200 Subject: [PATCH 1/8] dresources: shorten over-long lifecycle-config reason strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A FieldRule `reason:` is a short code-pointer token (word or word_word), not a full explanation — the explanation belongs in the `#` comment above the field. Four postgres configs spelled the `replace_existing` reason out as a sentence; shorten to `input_only`. Co-authored-by: Isaac --- bundle/direct/dresources/configs/postgres_branches.yml | 2 +- bundle/direct/dresources/configs/postgres_databases.yml | 2 +- bundle/direct/dresources/configs/postgres_endpoints.yml | 2 +- bundle/direct/dresources/configs/postgres_roles.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bundle/direct/dresources/configs/postgres_branches.yml b/bundle/direct/dresources/configs/postgres_branches.yml index e256c724cd2..ff15d1428d5 100644 --- a/bundle/direct/dresources/configs/postgres_branches.yml +++ b/bundle/direct/dresources/configs/postgres_branches.yml @@ -7,7 +7,7 @@ provided_id_fields: 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" + reason: input_only stable_output_fields: # name ("projects/{project_id}/branches/{branch_id}") is backend-assigned at # creation and immutable; see the stable_output_fields doc in config.go and ES-2202624. diff --git a/bundle/direct/dresources/configs/postgres_databases.yml b/bundle/direct/dresources/configs/postgres_databases.yml index f9d65fbc398..3e6095b97ee 100644 --- a/bundle/direct/dresources/configs/postgres_databases.yml +++ b/bundle/direct/dresources/configs/postgres_databases.yml @@ -7,7 +7,7 @@ provided_id_fields: 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" + reason: input_only stable_output_fields: # name ("projects/{p}/branches/{b}/databases/{database_id}") is backend-assigned # at creation and immutable; see the stable_output_fields doc in config.go and ES-2202624. diff --git a/bundle/direct/dresources/configs/postgres_endpoints.yml b/bundle/direct/dresources/configs/postgres_endpoints.yml index a9da4a06c9c..f1a08c18e7d 100644 --- a/bundle/direct/dresources/configs/postgres_endpoints.yml +++ b/bundle/direct/dresources/configs/postgres_endpoints.yml @@ -7,7 +7,7 @@ provided_id_fields: 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" + reason: input_only stable_output_fields: # name ("projects/{p}/branches/{b}/endpoints/{endpoint_id}") is backend-assigned # at creation and immutable; see the stable_output_fields doc in config.go and ES-2202624. diff --git a/bundle/direct/dresources/configs/postgres_roles.yml b/bundle/direct/dresources/configs/postgres_roles.yml index 2096b0d3d47..f7346b98f4e 100644 --- a/bundle/direct/dresources/configs/postgres_roles.yml +++ b/bundle/direct/dresources/configs/postgres_roles.yml @@ -1,7 +1,7 @@ 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" + reason: input_only stable_output_fields: # name ("projects/{project_id}/branches/{branch_id}/roles/{role_id}") is # backend-assigned at creation and immutable; see the stable_output_fields doc From e356ec63733a4ae35ffe46c82a2561c5c6012fce Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 29 Sep 2026 13:55:49 +0200 Subject: [PATCH 2/8] dresources: prototype field-keyed lifecycle config format Author a field's lifecycle policy as one action plus orthogonal modifiers, keyed by field prefix, instead of the flat per-behaviour lists (recreate_on_changes, provided_id_fields, ignore_local_changes, ...) where one field can land in several and contradict itself. Lower() translates to the existing ResourceLifecycleConfig, so the plan ladder and every consumer are unchanged. configs_v2/ migrates 9 resources; TestFieldPolicyLowersToLiveConfig proves each lowers to the identical live flat config. A duplicate field key (yaml load error) and an unknown action (parse-time enum validation) are both unrepresentable, so the per-config exclusivity/redundancy validation tests become unnecessary. Co-authored-by: Isaac --- .../direct/dresources/configs_v2/catalogs.yml | 13 + .../dresources/configs_v2/dashboards.yml | 10 + .../dresources/configs_v2/experiments.yml | 6 + .../configs_v2/postgres_branches.yml | 11 + .../configs_v2/postgres_projects.yml | 7 + .../dresources/configs_v2/postgres_roles.yml | 21 ++ .../direct/dresources/configs_v2/schemas.yml | 11 + .../direct/dresources/configs_v2/secrets.yml | 5 + .../direct/dresources/configs_v2/volumes.yml | 9 + bundle/direct/dresources/field_policy.go | 248 ++++++++++++++++++ bundle/direct/dresources/field_policy_test.go | 94 +++++++ 11 files changed, 435 insertions(+) create mode 100644 bundle/direct/dresources/configs_v2/catalogs.yml create mode 100644 bundle/direct/dresources/configs_v2/dashboards.yml create mode 100644 bundle/direct/dresources/configs_v2/experiments.yml create mode 100644 bundle/direct/dresources/configs_v2/postgres_branches.yml create mode 100644 bundle/direct/dresources/configs_v2/postgres_projects.yml create mode 100644 bundle/direct/dresources/configs_v2/postgres_roles.yml create mode 100644 bundle/direct/dresources/configs_v2/schemas.yml create mode 100644 bundle/direct/dresources/configs_v2/secrets.yml create mode 100644 bundle/direct/dresources/configs_v2/volumes.yml create mode 100644 bundle/direct/dresources/field_policy.go create mode 100644 bundle/direct/dresources/field_policy_test.go diff --git a/bundle/direct/dresources/configs_v2/catalogs.yml b/bundle/direct/dresources/configs_v2/catalogs.yml new file mode 100644 index 00000000000..01e1c52bbc3 --- /dev/null +++ b/bundle/direct/dresources/configs_v2/catalogs.yml @@ -0,0 +1,13 @@ +fields: + name: {action: id_renameable} + connection_name: {action: immutable} + provider_name: {action: immutable} + share_name: {action: immutable} + + # 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 + storage_root: {action: immutable, compare: trim_slash} + + # UC auto-populates unity.catalog.managed..defaults.* keys after create. + properties['unity.catalog.managed.*.defaults.*']: {action: backend_owned} diff --git a/bundle/direct/dresources/configs_v2/dashboards.yml b/bundle/direct/dresources/configs_v2/dashboards.yml new file mode 100644 index 00000000000..e9abb11659d --- /dev/null +++ b/bundle/direct/dresources/configs_v2/dashboards.yml @@ -0,0 +1,10 @@ +fields: + # "serialized_dashboard" holds inlined dashboard JSON that differs textually + # between local config and remote (we rely on etag for drift), and is never read + # back from state, so we persist only its content hash. + serialized_dashboard: {action: ignore_remote, reason: etag_based, hashed: true} + + # "dataset_catalog" and "dataset_schema" are write-only fields the server does not + # return, so local config (values) and remote state (empty) always differ. + dataset_catalog: {action: ignore_remote, reason: input_only} + dataset_schema: {action: ignore_remote, reason: input_only} diff --git a/bundle/direct/dresources/configs_v2/experiments.yml b/bundle/direct/dresources/configs_v2/experiments.yml new file mode 100644 index 00000000000..7d95707c3ba --- /dev/null +++ b/bundle/direct/dresources/configs_v2/experiments.yml @@ -0,0 +1,6 @@ +fields: + # TF implementation: https://github.com/databricks/terraform-provider-databricks/blob/6c106e8e7052bb2726148d66309fd460ed444236/mlflow/resource_mlflow_experiment.go#L22 + artifact_location: {action: immutable} + + # Tags updates are not supported by TF. Skip both local edits and remote drift. + tags: {action: ignore, reason: terraform_compat} diff --git a/bundle/direct/dresources/configs_v2/postgres_branches.yml b/bundle/direct/dresources/configs_v2/postgres_branches.yml new file mode 100644 index 00000000000..425b6b6f024 --- /dev/null +++ b/bundle/direct/dresources/configs_v2/postgres_branches.yml @@ -0,0 +1,11 @@ +fields: + # parent and branch_id are immutable (part of hierarchical name, not in API spec). + parent: {action: id} + branch_id: {action: id} + + # name ("projects/{project_id}/branches/{branch_id}") is backend-assigned at + # creation and immutable; see the stable_output_fields doc in config.go and ES-2202624. + name: {stable_output: true} + + # replace_existing only takes effect on create; toggling it later is a no-op. + replace_existing: {action: ignore_local, reason: input_only} diff --git a/bundle/direct/dresources/configs_v2/postgres_projects.yml b/bundle/direct/dresources/configs_v2/postgres_projects.yml new file mode 100644 index 00000000000..6039875c077 --- /dev/null +++ b/bundle/direct/dresources/configs_v2/postgres_projects.yml @@ -0,0 +1,7 @@ +fields: + # project_id is immutable (part of hierarchical name, not in API spec). + project_id: {action: id} + + # name ("projects/{project_id}") is backend-assigned at creation and immutable; + # see the stable_output_fields doc in config.go and ES-2202624. + name: {stable_output: true} diff --git a/bundle/direct/dresources/configs_v2/postgres_roles.yml b/bundle/direct/dresources/configs_v2/postgres_roles.yml new file mode 100644 index 00000000000..ee4fd763546 --- /dev/null +++ b/bundle/direct/dresources/configs_v2/postgres_roles.yml @@ -0,0 +1,21 @@ +fields: + # name ("projects/{project_id}/branches/{branch_id}/roles/{role_id}") is + # backend-assigned at creation and immutable; see the stable_output_fields doc + # in config.go and ES-2202624. + name: {stable_output: true} + + # parent and role_id are immutable (together they form the hierarchical name). + parent: {action: immutable} + role_id: {action: 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. + postgres_role: {action: immutable} + auth_method: {action: immutable} + identity_type: {action: immutable} + + # replace_existing only takes effect on create; toggling it later is a no-op. + replace_existing: {action: ignore_local, reason: input_only} diff --git a/bundle/direct/dresources/configs_v2/schemas.yml b/bundle/direct/dresources/configs_v2/schemas.yml new file mode 100644 index 00000000000..5bb78f3fda8 --- /dev/null +++ b/bundle/direct/dresources/configs_v2/schemas.yml @@ -0,0 +1,11 @@ +fields: + # UC lowercases identifier names; remote returns "myschema" for config "MySchema". + name: {action: id} + catalog_name: {action: id} + + storage_root: {action: immutable, compare: trim_slash} + + # 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". + properties['unity.catalog.managed.*.defaults.*']: {action: backend_owned} diff --git a/bundle/direct/dresources/configs_v2/secrets.yml b/bundle/direct/dresources/configs_v2/secrets.yml new file mode 100644 index 00000000000..66d5675bdcd --- /dev/null +++ b/bundle/direct/dresources/configs_v2/secrets.yml @@ -0,0 +1,5 @@ +fields: + # Modifier-only: no action, just redaction. The immutable/input-only rules for + # these fields come from secrets.generated.yml. + value: {sensitive: true} + effective_value: {sensitive: true} diff --git a/bundle/direct/dresources/configs_v2/volumes.yml b/bundle/direct/dresources/configs_v2/volumes.yml new file mode 100644 index 00000000000..10db253d747 --- /dev/null +++ b/bundle/direct/dresources/configs_v2/volumes.yml @@ -0,0 +1,9 @@ +fields: + catalog_name: {action: id} + schema_name: {action: id} + name: {action: id_renameable} + volume_type: {action: immutable} + + # 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 + storage_location: {action: immutable, compare: trim_slash} diff --git a/bundle/direct/dresources/field_policy.go b/bundle/direct/dresources/field_policy.go new file mode 100644 index 00000000000..c3d921e2a5f --- /dev/null +++ b/bundle/direct/dresources/field_policy.go @@ -0,0 +1,248 @@ +package dresources + +import ( + "embed" + "fmt" + "io/fs" + "maps" + "path" + "slices" + "strings" + "sync" + + "github.com/databricks/cli/libs/structs/structpath" + "go.yaml.in/yaml/v3" +) + +// FieldPolicy is the field-keyed alternative to the flat per-behaviour lists in +// ResourceLifecycleConfig. A field prefix names exactly one Action plus optional +// orthogonal modifiers, so a field cannot be handed two contradictory actions: +// the config is a map keyed by field, and the YAML loader rejects a duplicate key. +// That makes the action-exclusivity checks in config_test.go unnecessary — the +// redundancy is unrepresentable rather than merely tested against. +// +// Action decides what happens when the field differs. The three modifiers +// (Compare, Hashed, Sensitive) live on independent axes — comparison, state +// storage, redaction — so they legitimately combine with any Action and can +// never contradict it. +type FieldPolicy struct { + // Action is the single lifecycle decision for the field. Empty means the + // default (a normal in-place update). Its UnmarshalYAML rejects any value + // outside validActions, so an unknown action fails at parse time. + Action action `yaml:"action,omitempty"` + + // Reason overrides the default reason string surfaced in the plan. Actions + // with an obvious reason (id, immutable, ...) supply a default; ignore* + // actions require it because the justification is resource-specific. + Reason string `yaml:"reason,omitempty"` + + // Compare customizes equality before the action is decided. A string (not a + // bool) because comparison modes can grow and could combine — "" | "trim_slash" + // today, "ci" etc. later. + Compare string `yaml:"compare,omitempty"` + + // Hashed persists the value to state as a content hash instead of the raw + // value. For large, equality-only fields never read back from state. + Hashed bool `yaml:"hashed,omitempty"` + + // Sensitive marks the field for redaction in logs and state. + Sensitive bool `yaml:"sensitive,omitempty"` + + // StableOutput marks an output-only field the backend assigns at creation and + // never changes, so a cross-resource reference to it resolves from the remote + // cache during a keeps-ID update instead of being delayed. See + // StableOutputFields in config.go. + StableOutput bool `yaml:"stable_output,omitempty"` +} + +// action is the lifecycle decision for a field. It is a defined type with an +// UnmarshalYAML that validates against validActions, so an unknown action is a +// parse-time error (with file/line) rather than a failure discovered in Lower. +type action string + +// Action values. Each maps to one action rung of the plan ladder; see the table +// in README.md. immutable/id/id_renameable all recreate-or-rename on a local +// change and skip a remote-only diff; ignore* skip; backend_owned tolerates a +// value the backend populated. +const ( + actionMutable action = "" // default: a normal in-place update + actionImmutable action = "immutable" // recreate on local change + actionID action = "id" // provided id: recreate on change, skip remote drift + actionIDRenameable action = "id_renameable" // updatable id: rename in place on change + actionIgnore action = "ignore" // skip both local edits and remote drift + actionIgnoreLocal action = "ignore_local" // skip local edits only + actionIgnoreRemote action = "ignore_remote" // skip remote drift only + actionBackendOwned action = "backend_owned" // skip a config-absent value the backend set +) + +// validActions is the single source of truth for which actions parse. The +// lowerAction switch must handle each; TestFieldPolicyEveryActionLowers keeps the +// two in sync. +var validActions = map[action]bool{ + actionMutable: true, + actionImmutable: true, + actionID: true, + actionIDRenameable: true, + actionIgnore: true, + actionIgnoreLocal: true, + actionIgnoreRemote: true, + actionBackendOwned: true, +} + +func (a *action) UnmarshalYAML(unmarshal func(any) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + if !validActions[action(s)] { + return fmt.Errorf("unknown action %q", s) + } + *a = action(s) + return nil +} + +const ( + compareTrimSlash = "trim_slash" + + // Reason strings the flat format spells out per entry; the field-keyed format + // derives them from the action / modifier so the common case needs no reason. + reasonIDField = "id_field" + reasonIDChanges = "id_changes" + reasonImmutable = "immutable" + reasonTrimSlash = "uc_strips_trailing_slash" +) + +// FieldPolicyConfig is one resource's field-keyed lifecycle config. +type FieldPolicyConfig struct { + Fields map[string]FieldPolicy `yaml:"fields"` +} + +// Lower translates the field-keyed config into the existing ResourceLifecycleConfig +// so the plan ladder and every consumer keep working unchanged. It is the seam that +// lets the two formats coexist during migration. +func (c FieldPolicyConfig) Lower() (ResourceLifecycleConfig, error) { + var out ResourceLifecycleConfig + + // Deterministic order keeps the lowered lists stable for goldens and diffs. + for _, field := range slices.Sorted(maps.Keys(c.Fields)) { + p := c.Fields[field] + pattern, err := structpath.ParsePattern(field) + if err != nil { + return out, fmt.Errorf("field %q: %w", field, err) + } + + if err := lowerAction(&out, pattern, p); err != nil { + return out, fmt.Errorf("field %q: %w", field, err) + } + if err := lowerModifiers(&out, pattern, field, p); err != nil { + return out, fmt.Errorf("field %q: %w", field, err) + } + } + + return out, nil +} + +func lowerAction(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, p FieldPolicy) error { + rule := FieldRule{Field: pattern, Reason: p.Reason} + switch p.Action { + case actionMutable: + if p.Reason != "" { + return fmt.Errorf("reason %q set without an action", p.Reason) + } + case actionID: + out.ProvidedIDFields = append(out.ProvidedIDFields, withDefaultReason(rule, reasonIDField)) + case actionIDRenameable: + out.UpdatableIDFields = append(out.UpdatableIDFields, withDefaultReason(rule, reasonIDChanges)) + case actionImmutable: + out.RecreateOnChanges = append(out.RecreateOnChanges, withDefaultReason(rule, reasonImmutable)) + case actionIgnore: + out.IgnoreLocalChanges = append(out.IgnoreLocalChanges, rule) + out.IgnoreRemoteChanges = append(out.IgnoreRemoteChanges, rule) + case actionIgnoreLocal: + out.IgnoreLocalChanges = append(out.IgnoreLocalChanges, rule) + case actionIgnoreRemote: + out.IgnoreRemoteChanges = append(out.IgnoreRemoteChanges, rule) + case actionBackendOwned: + out.BackendDefaults = append(out.BackendDefaults, BackendDefaultRule{Field: pattern}) + default: + // Unreachable via YAML (UnmarshalYAML validates); guards a value added to + // validActions but not handled here. + return fmt.Errorf("unhandled action %q", p.Action) + } + return nil +} + +func lowerModifiers(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, field string, p FieldPolicy) error { + switch p.Compare { + case "": + case compareTrimSlash: + out.NormalizeSlash = append(out.NormalizeSlash, FieldRule{Field: pattern, Reason: reasonTrimSlash}) + default: + return fmt.Errorf("unknown compare %q", p.Compare) + } + + if p.Hashed { + out.HashedFields = append(out.HashedFields, field) + } + + if p.Sensitive { + out.SensitiveFields = append(out.SensitiveFields, FieldRule{Field: pattern}) + } + + if p.StableOutput { + out.StableOutputFields = append(out.StableOutputFields, FieldRule{Field: pattern}) + } + return nil +} + +func withDefaultReason(rule FieldRule, def string) FieldRule { + if rule.Reason == "" { + rule.Reason = def + } + return rule +} + +//go:embed configs_v2/*.yml +var fieldPolicyFS embed.FS + +const configV2Dir = "configs_v2" + +// loadFieldPolicyConfigs parses every configs_v2/.yml and lowers it +// to a ResourceLifecycleConfig, keyed by resource type. +var loadFieldPolicyConfigs = sync.OnceValue(func() map[string]ResourceLifecycleConfig { + names, err := fs.Glob(fieldPolicyFS, configV2Dir+"/*"+ymlSuffix) + if err != nil { + panic(err) + } + + result := map[string]ResourceLifecycleConfig{} + for _, name := range names { + resourceType := strings.TrimSuffix(path.Base(name), ymlSuffix) + + data, err := fieldPolicyFS.ReadFile(name) + if err != nil { + panic(err) + } + + var fpc FieldPolicyConfig + if err := yaml.Unmarshal(data, &fpc); err != nil { + panic(fmt.Errorf("%s: %w", name, err)) + } + + lowered, err := fpc.Lower() + if err != nil { + panic(fmt.Errorf("%s: %w", name, err)) + } + result[resourceType] = lowered + } + return result +}) + +// GetFieldPolicyConfig returns the lowered lifecycle config authored in the +// field-keyed configs_v2/ format, or nil if the resource has no such file. +func GetFieldPolicyConfig(resourceType string) *ResourceLifecycleConfig { + if rc, ok := loadFieldPolicyConfigs()[resourceType]; ok { + return &rc + } + return nil +} diff --git a/bundle/direct/dresources/field_policy_test.go b/bundle/direct/dresources/field_policy_test.go new file mode 100644 index 00000000000..5f7b4ef87cc --- /dev/null +++ b/bundle/direct/dresources/field_policy_test.go @@ -0,0 +1,94 @@ +package dresources + +import ( + "slices" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" +) + +// normalizeConfig projects a ResourceLifecycleConfig into an order-independent +// "category -> sorted rules" shape so two configs can be compared for behavioural +// equivalence regardless of the order fields were authored in. +func normalizeConfig(c ResourceLifecycleConfig) map[string][]string { + fieldRules := func(rules []FieldRule) []string { + out := make([]string, 0, len(rules)) + for _, r := range rules { + out = append(out, r.Field.String()+"|"+r.Reason) + } + slices.Sort(out) + return out + } + + backend := make([]string, 0, len(c.BackendDefaults)) + for _, r := range c.BackendDefaults { + backend = append(backend, r.Field.String()) + } + slices.Sort(backend) + + hashed := slices.Clone(c.HashedFields) + slices.Sort(hashed) + + return map[string][]string{ + "ignore_remote_changes": fieldRules(c.IgnoreRemoteChanges), + "ignore_local_changes": fieldRules(c.IgnoreLocalChanges), + "recreate_on_changes": fieldRules(c.RecreateOnChanges), + "provided_id_fields": fieldRules(c.ProvidedIDFields), + "updatable_id_fields": fieldRules(c.UpdatableIDFields), + "normalize_slash": fieldRules(c.NormalizeSlash), + "sensitive_fields": fieldRules(c.SensitiveFields), + "stable_output_fields": fieldRules(c.StableOutputFields), + "backend_defaults": backend, + "hashed_fields": hashed, + } +} + +// TestFieldPolicyLowersToLiveConfig proves the field-keyed configs_v2/.yml files +// lower to exactly the same lifecycle behaviour as the flat hand-written configs/.yml +// they replace. As long as this passes, the plan ladder and every other consumer are +// unaffected, so the migration is behaviour-preserving. +func TestFieldPolicyLowersToLiveConfig(t *testing.T) { + migrated := []string{ + "volumes", "schemas", "catalogs", "experiments", + "postgres_roles", "postgres_projects", "postgres_branches", + "dashboards", "secrets", + } + for _, resourceType := range migrated { + t.Run(resourceType, func(t *testing.T) { + v2 := GetFieldPolicyConfig(resourceType) + require.NotNil(t, v2, "no configs_v2/%s.yml", resourceType) + live := GetResourceConfig(resourceType) + assert.Equal(t, normalizeConfig(*live), normalizeConfig(*v2)) + }) + } +} + +// TestFieldPolicyRejectsDuplicateField shows the redundancy class the flat format +// needed a dedicated exclusivity test for is now unrepresentable: a field can carry +// only one policy, and a repeated key is a load error, not a silently-shadowed rule. +func TestFieldPolicyRejectsDuplicateField(t *testing.T) { + var fpc FieldPolicyConfig + err := yaml.Unmarshal([]byte("fields:\n name: { action: id }\n name: { action: immutable }\n"), &fpc) + require.Error(t, err) + assert.Contains(t, err.Error(), `already defined`) +} + +// TestFieldPolicyRejectsUnknownAction proves the action enum is enforced by the +// parser: an unknown value fails at yaml.Unmarshal, not later in Lower. +func TestFieldPolicyRejectsUnknownAction(t *testing.T) { + var fpc FieldPolicyConfig + err := yaml.Unmarshal([]byte("fields:\n name: { action: teleport }\n"), &fpc) + require.Error(t, err) + assert.Contains(t, err.Error(), `unknown action "teleport"`) +} + +// TestFieldPolicyEveryActionLowers keeps validActions and the lowerAction switch in +// sync: every value the parser accepts must lower without hitting the default guard. +func TestFieldPolicyEveryActionLowers(t *testing.T) { + for a := range validActions { + _, err := FieldPolicyConfig{Fields: map[string]FieldPolicy{"name": {Action: a}}}.Lower() + require.NoErrorf(t, err, "action %q", a) + } +} From 65d334c626555041faf3b52c366c04658d20fe5d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 29 Sep 2026 14:01:38 +0200 Subject: [PATCH 3/8] dresources: set all struct fields in field_policy literals for exhaustruct golangci-lint's exhaustruct (root module) requires every field of FieldRule and BackendDefaultRule to be set explicitly in a literal. Co-authored-by: Isaac --- bundle/direct/dresources/field_policy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bundle/direct/dresources/field_policy.go b/bundle/direct/dresources/field_policy.go index c3d921e2a5f..8b39bd17177 100644 --- a/bundle/direct/dresources/field_policy.go +++ b/bundle/direct/dresources/field_policy.go @@ -163,7 +163,7 @@ func lowerAction(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, case actionIgnoreRemote: out.IgnoreRemoteChanges = append(out.IgnoreRemoteChanges, rule) case actionBackendOwned: - out.BackendDefaults = append(out.BackendDefaults, BackendDefaultRule{Field: pattern}) + out.BackendDefaults = append(out.BackendDefaults, BackendDefaultRule{Field: pattern, Values: nil}) default: // Unreachable via YAML (UnmarshalYAML validates); guards a value added to // validActions but not handled here. @@ -186,11 +186,11 @@ func lowerModifiers(out *ResourceLifecycleConfig, pattern *structpath.PatternNod } if p.Sensitive { - out.SensitiveFields = append(out.SensitiveFields, FieldRule{Field: pattern}) + out.SensitiveFields = append(out.SensitiveFields, FieldRule{Field: pattern, Reason: ""}) } if p.StableOutput { - out.StableOutputFields = append(out.StableOutputFields, FieldRule{Field: pattern}) + out.StableOutputFields = append(out.StableOutputFields, FieldRule{Field: pattern, Reason: ""}) } return nil } From cf42f3aa3688b07271b702b627db5d63cdac4196 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 30 Sep 2026 10:50:44 +0200 Subject: [PATCH 4/8] dresources: replace flat lifecycle configs with the field-keyed format Author each field's lifecycle policy as one action (the local-change decision: id / id_renameable / immutable / ignore_local, or a resource-root action) plus independent modifiers on other axes (ignore_remote, compare, backend_default / backend_values, hashed, sensitive, stable_output, remote_additions_when). The loader parses this via FieldPolicyConfig and Lower()s it to the existing ResourceLifecycleConfig, so the planner and every other consumer are unchanged. Keying by field puts a field's whole policy in one place and makes a duplicate field a YAML load error; an unknown action fails at parse time. The redundancy the flat format needed exclusivity/redundancy tests to catch is now unrepresentable. All 62 configs (hand-written and generated) are converted; generate_resources.py emits the new format. Verified lossless: every resource lowers to a byte-identical ResourceLifecycleConfig, except four instance_pools recreate fields whose empty reason is normalized to "immutable" (not surfaced in any plan output). Co-authored-by: Isaac --- bundle/direct/dresources/config.go | 8 +- .../dresources/configs/alerts.generated.yml | 34 +-- .../dresources/configs/apps.generated.yml | 103 ++++---- bundle/direct/dresources/configs/apps.yml | 24 +- bundle/direct/dresources/configs/catalogs.yml | 31 ++- .../dresources/configs/cluster_policies.yml | 5 +- bundle/direct/dresources/configs/clusters.yml | 53 ++-- .../configs/dashboards.generated.yml | 27 +- .../direct/dresources/configs/dashboards.yml | 23 +- .../configs/database_catalogs.generated.yml | 11 +- .../dresources/configs/database_catalogs.yml | 9 +- .../configs/database_instances.generated.yml | 148 ++++++----- .../dresources/configs/database_instances.yml | 6 +- .../configs/experiments.generated.yml | 11 +- .../direct/dresources/configs/experiments.yml | 14 +- .../configs/external_locations.generated.yml | 34 +-- .../dresources/configs/external_locations.yml | 19 +- .../configs/genie_spaces.generated.yml | 6 +- .../dresources/configs/genie_spaces.yml | 14 +- .../dresources/configs/instance_pools.yml | 39 +-- .../configs/internal_immutable_snapshots.yml | 4 +- bundle/direct/dresources/configs/job_runs.yml | 69 +++--- bundle/direct/dresources/configs/jobs.yml | 189 +++++++------- .../configs/mcp_services.generated.yml | 10 +- .../dresources/configs/mcp_services.yml | 10 +- .../model_provider_services.generated.yml | 56 ++--- .../configs/model_provider_services.yml | 10 +- .../configs/model_services.generated.yml | 26 +- .../dresources/configs/model_services.yml | 10 +- .../model_serving_endpoints.generated.yml | 63 +++-- .../configs/model_serving_endpoints.yml | 62 +++-- bundle/direct/dresources/configs/models.yml | 23 +- .../configs/pipelines.generated.yml | 6 +- .../direct/dresources/configs/pipelines.yml | 89 ++++--- .../configs/postgres_branches.generated.yml | 44 ++-- .../dresources/configs/postgres_branches.yml | 18 +- .../configs/postgres_catalogs.generated.yml | 18 +- .../dresources/configs/postgres_catalogs.yml | 17 +- .../configs/postgres_databases.generated.yml | 10 +- .../dresources/configs/postgres_databases.yml | 18 +- .../configs/postgres_endpoints.generated.yml | 38 ++- .../dresources/configs/postgres_endpoints.yml | 18 +- .../configs/postgres_projects.generated.yml | 46 ++-- .../dresources/configs/postgres_projects.yml | 10 +- .../configs/postgres_roles.generated.yml | 22 +- .../dresources/configs/postgres_roles.yml | 30 +-- .../configs/postgres_snapshot_schedules.yml | 10 +- .../postgres_synced_tables.generated.yml | 78 +++--- .../configs/postgres_synced_tables.yml | 9 +- .../dresources/configs/quality_monitors.yml | 11 +- .../configs/registered_models.generated.yml | 6 +- .../dresources/configs/registered_models.yml | 49 ++-- bundle/direct/dresources/configs/schemas.yml | 23 +- .../configs/secret_scopes.permissions.yml | 6 +- .../dresources/configs/secret_scopes.yml | 19 +- .../dresources/configs/secrets.generated.yml | 54 ++-- bundle/direct/dresources/configs/secrets.yml | 8 +- .../dresources/configs/sql_warehouses.yml | 38 ++- .../synced_database_tables.generated.yml | 43 ++-- .../configs/synced_database_tables.yml | 9 +- .../configs/vector_search_endpoints.yml | 16 +- .../configs/vector_search_indexes.yml | 36 ++- bundle/direct/dresources/configs/volumes.yml | 28 +-- .../direct/dresources/configs_v2/catalogs.yml | 13 - .../dresources/configs_v2/dashboards.yml | 10 - .../dresources/configs_v2/experiments.yml | 6 - .../configs_v2/postgres_branches.yml | 11 - .../configs_v2/postgres_projects.yml | 7 - .../dresources/configs_v2/postgres_roles.yml | 21 -- .../direct/dresources/configs_v2/schemas.yml | 11 - .../direct/dresources/configs_v2/secrets.yml | 5 - .../direct/dresources/configs_v2/volumes.yml | 9 - bundle/direct/dresources/field_policy.go | 233 ++++++++---------- bundle/direct/dresources/field_policy_test.go | 74 ++---- bundle/direct/tools/generate_resources.py | 46 ++-- 75 files changed, 1114 insertions(+), 1310 deletions(-) delete mode 100644 bundle/direct/dresources/configs_v2/catalogs.yml delete mode 100644 bundle/direct/dresources/configs_v2/dashboards.yml delete mode 100644 bundle/direct/dresources/configs_v2/experiments.yml delete mode 100644 bundle/direct/dresources/configs_v2/postgres_branches.yml delete mode 100644 bundle/direct/dresources/configs_v2/postgres_projects.yml delete mode 100644 bundle/direct/dresources/configs_v2/postgres_roles.yml delete mode 100644 bundle/direct/dresources/configs_v2/schemas.yml delete mode 100644 bundle/direct/dresources/configs_v2/secrets.yml delete mode 100644 bundle/direct/dresources/configs_v2/volumes.yml diff --git a/bundle/direct/dresources/config.go b/bundle/direct/dresources/config.go index cc54d60a678..57372db9fe1 100644 --- a/bundle/direct/dresources/config.go +++ b/bundle/direct/dresources/config.go @@ -186,8 +186,12 @@ var loadConfigs = sync.OnceValues(func() (*Config, *Config) { panic(err) } - var rc ResourceLifecycleConfig - if err := yaml.Unmarshal(data, &rc); err != nil { + var fpc FieldPolicyConfig + if err := yaml.Unmarshal(data, &fpc); err != nil { + panic(fmt.Errorf("%s: %w", name, err)) + } + rc, err := fpc.Lower() + if err != nil { panic(fmt.Errorf("%s: %w", name, err)) } diff --git a/bundle/direct/dresources/configs/alerts.generated.yml b/bundle/direct/dresources/configs/alerts.generated.yml index 8b9e18006be..73daaf25c00 100644 --- a/bundle/direct/dresources/configs/alerts.generated.yml +++ b/bundle/direct/dresources/configs/alerts.generated.yml @@ -1,19 +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 +fields: + create_time: + ignore_remote: spec:output_only + effective_run_as: + ignore_remote: spec:output_only + evaluation.last_evaluated_at: + ignore_remote: spec:output_only + evaluation.state: + ignore_remote: spec:output_only + id: + ignore_remote: spec:output_only + lifecycle_state: + ignore_remote: spec:output_only + owner_user_name: + ignore_remote: spec:output_only + update_time: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/apps.generated.yml b/bundle/direct/dresources/configs/apps.generated.yml index 5e204b2a15f..c2438a91919 100644 --- a/bundle/direct/dresources/configs/apps.generated.yml +++ b/bundle/direct/dresources/configs/apps.generated.yml @@ -1,54 +1,53 @@ # 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 +fields: + active_deployment: + ignore_remote: spec:output_only + app_status: + ignore_remote: spec:output_only + compute_status: + ignore_remote: spec:output_only + create_time: + ignore_remote: spec:output_only + creator: + ignore_remote: spec:output_only + default_git_source: + ignore_remote: spec:output_only + default_source_code_path: + ignore_remote: spec:output_only + effective_budget_policy_id: + ignore_remote: spec:output_only + effective_usage_policy_id: + ignore_remote: spec:output_only + effective_user_api_scopes: + ignore_remote: spec:output_only + git_repository.caller_credential_id: + ignore_remote: spec:input_only + git_source: + ignore_remote: spec:input_only + id: + ignore_remote: spec:output_only + oauth2_app_client_id: + ignore_remote: spec:output_only + oauth2_app_integration_id: + ignore_remote: spec:output_only + pending_deployment: + ignore_remote: spec:output_only + resources[*].uc_securable.securable_kind: + ignore_remote: spec:output_only + service_principal_client_id: + ignore_remote: spec:output_only + service_principal_id: + ignore_remote: spec:output_only + service_principal_name: + ignore_remote: spec:output_only + source_code_path: + ignore_remote: spec:input_only + thumbnail_url: + ignore_remote: spec:output_only + update_time: + ignore_remote: spec:output_only + updater: + ignore_remote: spec:output_only + url: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/apps.yml b/bundle/direct/dresources/configs/apps.yml index a6725316bec..935a992d3c0 100644 --- a/bundle/direct/dresources/configs/apps.yml +++ b/bundle/direct/dresources/configs/apps.yml @@ -1,20 +1,22 @@ -provided_id_fields: - - field: name - reason: id_field -backend_defaults: +fields: + name: + action: id # Backend sets it "MEDIUM" when not specified in the config - - field: compute_size + compute_size: + backend_default: true # 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 + forward_user_access_token: + backend_default: true # 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 + lifecycle: + backend_default: true + lifecycle.started: + backend_default: true + space: + ignore_remote: managed diff --git a/bundle/direct/dresources/configs/catalogs.yml b/bundle/direct/dresources/configs/catalogs.yml index 143a74c447d..1fd8b7eb407 100644 --- a/bundle/direct/dresources/configs/catalogs.yml +++ b/bundle/direct/dresources/configs/catalogs.yml @@ -1,21 +1,18 @@ -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: +fields: # 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: + storage_root: + action: immutable + compare: trim_slash + connection_name: + action: immutable + provider_name: + action: immutable + share_name: + action: immutable + name: + action: id_renameable # UC auto-populates unity.catalog.managed..defaults.* keys after create. - - field: properties['unity.catalog.managed.*.defaults.*'] + properties['unity.catalog.managed.*.defaults.*']: + backend_default: true diff --git a/bundle/direct/dresources/configs/cluster_policies.yml b/bundle/direct/dresources/configs/cluster_policies.yml index 6f2ee1e1b9b..26bcfb4d551 100644 --- a/bundle/direct/dresources/configs/cluster_policies.yml +++ b/bundle/direct/dresources/configs/cluster_policies.yml @@ -1,6 +1,7 @@ -backend_defaults: +fields: # 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 + definition: + backend_default: true diff --git a/bundle/direct/dresources/configs/clusters.yml b/bundle/direct/dresources/configs/clusters.yml index b19d9c0483c..309a9836b18 100644 --- a/bundle/direct/dresources/configs/clusters.yml +++ b/bundle/direct/dresources/configs/clusters.yml @@ -5,53 +5,48 @@ # 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: +remote_additions_when: policy_id +fields: # 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: + aws_attributes: + ignore_remote: managed + azure_attributes: + ignore_remote: managed + gcp_attributes: + ignore_remote: managed # 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 + lifecycle: + backend_default: true + lifecycle.started: + backend_default: true # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L331 # s.SchemaPath("enable_elastic_disk").SetComputed() - - field: enable_elastic_disk - + enable_elastic_disk: + backend_default: true # 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 - + enable_local_disk_encryption: + backend_default: true # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L333 # s.SchemaPath("node_type_id").SetComputed() - - field: node_type_id - + node_type_id: + backend_default: true # 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 - + driver_node_type_id: + backend_default: true # 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 - + driver_instance_pool_id: + backend_default: true # 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 + single_user_name: + backend_default: true diff --git a/bundle/direct/dresources/configs/dashboards.generated.yml b/bundle/direct/dresources/configs/dashboards.generated.yml index 936b6e7f1c6..0553cd25c9c 100644 --- a/bundle/direct/dresources/configs/dashboards.generated.yml +++ b/bundle/direct/dresources/configs/dashboards.generated.yml @@ -1,17 +1,16 @@ # Generated, do not edit. -recreate_on_changes: - - field: parent_path +fields: + create_time: + ignore_remote: spec:output_only + dashboard_id: + ignore_remote: spec:output_only + lifecycle_state: + ignore_remote: spec:output_only + parent_path: + action: immutable 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 + path: + ignore_remote: spec:output_only + update_time: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/dashboards.yml b/bundle/direct/dresources/configs/dashboards.yml index f59f0e38208..9e222666284 100644 --- a/bundle/direct/dresources/configs/dashboards.yml +++ b/bundle/direct/dresources/configs/dashboards.yml @@ -1,17 +1,14 @@ -ignore_remote_changes: +fields: # "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 - + # "serialized_dashboard" holds inlined dashboard JSON that is never read back + # from state, so we persist only its content hash. + serialized_dashboard: + ignore_remote: etag_based + hashed: true # "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 - -# "serialized_dashboard" holds inlined dashboard JSON that is never read back -# from state, so we persist only its content hash. -hashed_fields: - - serialized_dashboard + dataset_catalog: + ignore_remote: input_only + dataset_schema: + ignore_remote: input_only diff --git a/bundle/direct/dresources/configs/database_catalogs.generated.yml b/bundle/direct/dresources/configs/database_catalogs.generated.yml index 5eddcfd2a48..4cc9947d970 100644 --- a/bundle/direct/dresources/configs/database_catalogs.generated.yml +++ b/bundle/direct/dresources/configs/database_catalogs.generated.yml @@ -1,8 +1,7 @@ # Generated, do not edit. -ignore_remote_changes: - - field: create_database_if_not_exists - reason: spec:input_only - - - field: uid - reason: spec:output_only +fields: + create_database_if_not_exists: + ignore_remote: spec:input_only + uid: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/database_catalogs.yml b/bundle/direct/dresources/configs/database_catalogs.yml index 26eda6c377f..61e0154feea 100644 --- a/bundle/direct/dresources/configs/database_catalogs.yml +++ b/bundle/direct/dresources/configs/database_catalogs.yml @@ -1,11 +1,10 @@ -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 +action: immutable +fields: + name: + action: id diff --git a/bundle/direct/dresources/configs/database_instances.generated.yml b/bundle/direct/dresources/configs/database_instances.generated.yml index 66fdebab914..37d4f89c141 100644 --- a/bundle/direct/dresources/configs/database_instances.generated.yml +++ b/bundle/direct/dresources/configs/database_instances.generated.yml @@ -1,78 +1,76 @@ # Generated, do not edit. -recreate_on_changes: - - field: parent_instance_ref +fields: + child_instance_refs: + ignore_remote: spec:output_only + child_instance_refs[*].branch_time: + ignore_remote: spec:output_only + child_instance_refs[*].effective_lsn: + ignore_remote: spec:output_only + child_instance_refs[*].lsn: + ignore_remote: spec:output_only + child_instance_refs[*].name: + ignore_remote: spec:output_only + child_instance_refs[*].uid: + ignore_remote: spec:output_only + creation_time: + ignore_remote: spec:output_only + creator: + ignore_remote: spec:output_only + custom_tags: + ignore_remote: spec:input_only + custom_tags[*].key: + ignore_remote: spec:input_only + custom_tags[*].value: + ignore_remote: spec:input_only + effective_capacity: + ignore_remote: spec:output_only + effective_custom_tags: + ignore_remote: spec:output_only + effective_custom_tags[*].key: + ignore_remote: spec:output_only + effective_custom_tags[*].value: + ignore_remote: spec:output_only + effective_enable_pg_native_login: + ignore_remote: spec:output_only + effective_enable_readable_secondaries: + ignore_remote: spec:output_only + effective_node_count: + ignore_remote: spec:output_only + effective_retention_window_in_days: + ignore_remote: spec:output_only + effective_stopped: + ignore_remote: spec:output_only + effective_usage_policy_id: + ignore_remote: spec:output_only + enable_pg_native_login: + ignore_remote: spec:input_only + enable_readable_secondaries: + ignore_remote: spec:input_only + node_count: + ignore_remote: spec:input_only + parent_instance_ref: + action: immutable 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 + parent_instance_ref.effective_lsn: + ignore_remote: spec:output_only + parent_instance_ref.lsn: + ignore_remote: spec:input_only + parent_instance_ref.uid: + ignore_remote: spec:output_only + pg_version: + ignore_remote: spec:output_only + read_only_dns: + ignore_remote: spec:output_only + read_write_dns: + ignore_remote: spec:output_only + retention_window_in_days: + ignore_remote: spec:input_only + state: + ignore_remote: spec:output_only + stopped: + ignore_remote: spec:input_only + uid: + ignore_remote: spec:output_only + usage_policy_id: + ignore_remote: spec:input_only diff --git a/bundle/direct/dresources/configs/database_instances.yml b/bundle/direct/dresources/configs/database_instances.yml index b6ab8dc9e8a..4450ad01430 100644 --- a/bundle/direct/dresources/configs/database_instances.yml +++ b/bundle/direct/dresources/configs/database_instances.yml @@ -1,3 +1,3 @@ -provided_id_fields: - - field: name - reason: id_field +fields: + name: + action: id diff --git a/bundle/direct/dresources/configs/experiments.generated.yml b/bundle/direct/dresources/configs/experiments.generated.yml index afd372d58f2..4b8a2747f03 100644 --- a/bundle/direct/dresources/configs/experiments.generated.yml +++ b/bundle/direct/dresources/configs/experiments.generated.yml @@ -1,9 +1,8 @@ # Generated, do not edit. -recreate_on_changes: - - field: trace_location +fields: + trace_location: + action: immutable reason: spec:immutable - -ignore_remote_changes: - - field: trace_location.uc_trace_location.effective_table_prefix - reason: spec:output_only + trace_location.uc_trace_location.effective_table_prefix: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/experiments.yml b/bundle/direct/dresources/configs/experiments.yml index 6a261514379..21a4dada2f7 100644 --- a/bundle/direct/dresources/configs/experiments.yml +++ b/bundle/direct/dresources/configs/experiments.yml @@ -1,11 +1,9 @@ # 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 -ignore_remote_changes: +fields: + artifact_location: + action: immutable # Tags updates are not supported by TF. This mirrors that behaviour. - - field: tags - reason: terraform_compat -ignore_local_changes: - - field: tags + tags: + ignore_remote: terraform_compat + action: ignore_local reason: terraform_compat diff --git a/bundle/direct/dresources/configs/external_locations.generated.yml b/bundle/direct/dresources/configs/external_locations.generated.yml index df2846b0a2f..53e45f30a55 100644 --- a/bundle/direct/dresources/configs/external_locations.generated.yml +++ b/bundle/direct/dresources/configs/external_locations.generated.yml @@ -1,19 +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 +fields: + effective_enable_file_events: + ignore_remote: spec:output_only + effective_file_event_queue: + ignore_remote: spec:output_only + file_event_queue.managed_aqs.managed_resource_id: + ignore_remote: spec:output_only + file_event_queue.managed_pubsub.managed_resource_id: + ignore_remote: spec:output_only + file_event_queue.managed_sqs.managed_resource_id: + ignore_remote: spec:output_only + file_event_queue.provided_aqs.managed_resource_id: + ignore_remote: spec:output_only + file_event_queue.provided_pubsub.managed_resource_id: + ignore_remote: spec:output_only + file_event_queue.provided_sqs.managed_resource_id: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/external_locations.yml b/bundle/direct/dresources/configs/external_locations.yml index 55612ef68a6..37e41e70660 100644 --- a/bundle/direct/dresources/configs/external_locations.yml +++ b/bundle/direct/dresources/configs/external_locations.yml @@ -1,10 +1,9 @@ -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 +fields: + credential_name: + action: immutable + encryption_details: + action: immutable + file_event_queue: + action: immutable + name: + action: id_renameable diff --git a/bundle/direct/dresources/configs/genie_spaces.generated.yml b/bundle/direct/dresources/configs/genie_spaces.generated.yml index d9c3e63803d..5dd3a25326e 100644 --- a/bundle/direct/dresources/configs/genie_spaces.generated.yml +++ b/bundle/direct/dresources/configs/genie_spaces.generated.yml @@ -1,5 +1,5 @@ # Generated, do not edit. -ignore_remote_changes: - - field: etag - reason: spec:output_only +fields: + etag: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/genie_spaces.yml b/bundle/direct/dresources/configs/genie_spaces.yml index 41ab7d5aa1b..5819f0e0252 100644 --- a/bundle/direct/dresources/configs/genie_spaces.yml +++ b/bundle/direct/dresources/configs/genie_spaces.yml @@ -1,10 +1,8 @@ -ignore_remote_changes: +fields: # 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 - -# "serialized_space" holds inlined Genie space JSON that is never read back -# from state, so we persist only its content hash. -hashed_fields: - - serialized_space + # "serialized_space" holds inlined Genie space JSON that is never read back + # from state, so we persist only its content hash. + serialized_space: + ignore_remote: etag_based + hashed: true diff --git a/bundle/direct/dresources/configs/instance_pools.yml b/bundle/direct/dresources/configs/instance_pools.yml index 35fec4fbbb4..234b8a20694 100644 --- a/bundle/direct/dresources/configs/instance_pools.yml +++ b/bundle/direct/dresources/configs/instance_pools.yml @@ -1,24 +1,27 @@ # 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: +fields: # 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: + aws_attributes: + ignore_remote: managed + azure_attributes: + ignore_remote: managed + gcp_attributes: + ignore_remote: managed # 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: + disk_spec: + action: immutable + node_type_flexibility: + action: immutable + preloaded_spark_versions: + action: immutable + preloaded_docker_images: + action: immutable # Defaults to true server-side. - - field: enable_elastic_disk + enable_elastic_disk: + backend_default: true # 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 + idle_instance_autotermination_minutes: + backend_values: [60] + max_capacity: + backend_default: true diff --git a/bundle/direct/dresources/configs/internal_immutable_snapshots.yml b/bundle/direct/dresources/configs/internal_immutable_snapshots.yml index 7fcfd7cc35c..2fef38cbf7a 100644 --- a/bundle/direct/dresources/configs/internal_immutable_snapshots.yml +++ b/bundle/direct/dresources/configs/internal_immutable_snapshots.yml @@ -1,2 +1,2 @@ -recreate_on_changes: - - reason: immutable +action: immutable +fields: {} diff --git a/bundle/direct/dresources/configs/job_runs.yml b/bundle/direct/dresources/configs/job_runs.yml index 3fc5c0e74d5..2dfc8cd22c6 100644 --- a/bundle/direct/dresources/configs/job_runs.yml +++ b/bundle/direct/dresources/configs/job_runs.yml @@ -1,42 +1,41 @@ +# 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. # 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 +action: immutable +fields: + job_id: + ignore_remote: 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 + dbt_commands: + ignore_remote: effective_vs_requested + jar_params: + ignore_remote: effective_vs_requested + job_parameters: + ignore_remote: effective_vs_requested + notebook_params: + ignore_remote: effective_vs_requested + pipeline_params: + ignore_remote: effective_vs_requested + python_named_params: + ignore_remote: effective_vs_requested + python_params: + ignore_remote: effective_vs_requested + spark_submit_params: + ignore_remote: effective_vs_requested + sql_params: + ignore_remote: 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 + idempotency_token: + ignore_remote: not_returned_by_api + only: + ignore_remote: not_returned_by_api + performance_target: + ignore_remote: not_returned_by_api + queue: + ignore_remote: not_returned_by_api diff --git a/bundle/direct/dresources/configs/jobs.yml b/bundle/direct/dresources/configs/jobs.yml index c1a9b5a5790..4bf01be8ec7 100644 --- a/bundle/direct/dresources/configs/jobs.yml +++ b/bundle/direct/dresources/configs/jobs.yml @@ -4,64 +4,56 @@ # 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 +fields: + deployment.version_id: + action: ignore_local reason: auto - -ignore_remote_changes: - - field: deployment.version_id - reason: auto - + ignore_remote: 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 - + tasks[*].new_cluster.aws_attributes: + ignore_remote: managed + tasks[*].new_cluster.azure_attributes: + ignore_remote: managed + tasks[*].new_cluster.gcp_attributes: + ignore_remote: managed + tasks[*].for_each_task.task.new_cluster.aws_attributes: + ignore_remote: managed + tasks[*].for_each_task.task.new_cluster.azure_attributes: + ignore_remote: managed + tasks[*].for_each_task.task.new_cluster.gcp_attributes: + ignore_remote: managed + job_clusters[*].new_cluster.aws_attributes: + ignore_remote: managed + job_clusters[*].new_cluster.azure_attributes: + ignore_remote: managed + job_clusters[*].new_cluster.gcp_attributes: + ignore_remote: 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 - + tasks[*].new_cluster.apply_policy_default_values: + ignore_remote: not_returned_by_api + tasks[*].for_each_task.task.new_cluster.apply_policy_default_values: + ignore_remote: not_returned_by_api + job_clusters[*].new_cluster.apply_policy_default_values: + ignore_remote: 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 - + trigger.table_update.condition: + ignore_remote: 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: + triggers[*].table_update.condition: + ignore_remote: input_only # 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 @@ -69,78 +61,93 @@ ignore_remote_additions: # 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: + tasks[*].new_cluster: + remote_additions_when: policy_id + tasks[*].for_each_task.task.new_cluster: + remote_additions_when: policy_id + job_clusters[*].new_cluster: + remote_additions_when: policy_id # 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 - + tasks[*].new_cluster.enable_elastic_disk: + backend_default: true + tasks[*].for_each_task.task.new_cluster.enable_elastic_disk: + backend_default: true + job_clusters[*].new_cluster.enable_elastic_disk: + backend_default: true # 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 - + tasks[*].new_cluster.enable_local_disk_encryption: + backend_default: true + tasks[*].for_each_task.task.new_cluster.enable_local_disk_encryption: + backend_default: true + job_clusters[*].new_cluster.enable_local_disk_encryption: + backend_default: true # 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 - + tasks[*].new_cluster.node_type_id: + backend_default: true + tasks[*].for_each_task.task.new_cluster.node_type_id: + backend_default: true + job_clusters[*].new_cluster.node_type_id: + backend_default: true # 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 - + tasks[*].new_cluster.driver_node_type_id: + backend_default: true + tasks[*].for_each_task.task.new_cluster.driver_node_type_id: + backend_default: true + job_clusters[*].new_cluster.driver_node_type_id: + backend_default: true # 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 - + tasks[*].new_cluster.driver_instance_pool_id: + backend_default: true + tasks[*].for_each_task.task.new_cluster.driver_instance_pool_id: + backend_default: true + job_clusters[*].new_cluster.driver_instance_pool_id: + backend_default: true # 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"] - + format: + backend_values: ["MULTI_TASK", "SINGLE_TASK"] + "tasks[*].run_if": + backend_values: ["ALL_SUCCESS"] # 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"] - + "tasks[*].for_each_task.task.run_if": + backend_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"] - + run_as: + backend_default: true # 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 - + tasks[*].notebook_task.source: + backend_default: true + tasks[*].for_each_task.task.notebook_task.source: + backend_default: true + tasks[*].spark_python_task.source: + backend_default: true + tasks[*].for_each_task.task.spark_python_task.source: + backend_default: true + tasks[*].sql_task.file.source: + backend_default: true + tasks[*].for_each_task.task.sql_task.file.source: + backend_default: true + tasks[*].dbt_task.source: + backend_default: true + tasks[*].for_each_task.task.dbt_task.source: + backend_default: true # 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 + tasks[*].new_cluster.data_security_mode: + backend_default: true + tasks[*].for_each_task.task.new_cluster.data_security_mode: + backend_default: true + job_clusters[*].new_cluster.data_security_mode: + backend_default: true diff --git a/bundle/direct/dresources/configs/mcp_services.generated.yml b/bundle/direct/dresources/configs/mcp_services.generated.yml index f9371afff03..319a35aa190 100644 --- a/bundle/direct/dresources/configs/mcp_services.generated.yml +++ b/bundle/direct/dresources/configs/mcp_services.generated.yml @@ -1,7 +1,7 @@ # Generated, do not edit. -ignore_remote_changes: - - field: config.source_connection.is_deleted - reason: spec:output_only - - field: config.source_connection.options - reason: spec:output_only +fields: + config.source_connection.is_deleted: + ignore_remote: spec:output_only + config.source_connection.options: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/mcp_services.yml b/bundle/direct/dresources/configs/mcp_services.yml index 419cf45197c..ee9406b9630 100644 --- a/bundle/direct/dresources/configs/mcp_services.yml +++ b/bundle/direct/dresources/configs/mcp_services.yml @@ -1,10 +1,10 @@ -provided_id_fields: +fields: # parent + mcp_service_id compose the server-derived resource name # (mcp-services/{catalog}.{schema}.{mcp_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: mcp_service_id - reason: id_field + parent: + action: id + mcp_service_id: + action: id diff --git a/bundle/direct/dresources/configs/model_provider_services.generated.yml b/bundle/direct/dresources/configs/model_provider_services.generated.yml index d87a09b25f8..293f0d82591 100644 --- a/bundle/direct/dresources/configs/model_provider_services.generated.yml +++ b/bundle/direct/dresources/configs/model_provider_services.generated.yml @@ -1,32 +1,30 @@ # Generated, do not edit. -recreate_on_changes: - - field: config.provider_type +fields: + config.amazon_bedrock.direct.aws_access_key.secret_access_key.plaintext: + ignore_remote: spec:input_only + config.anthropic.direct.api_key.plaintext: + ignore_remote: spec:input_only + config.azure_openai.direct.api_key.plaintext: + ignore_remote: spec:input_only + config.azure_openai.direct.entra_service_principal.client_secret.plaintext: + ignore_remote: spec:input_only + config.custom.direct.api_key.plaintext: + ignore_remote: spec:input_only + config.custom.direct.header_auth.api_key_value.plaintext: + ignore_remote: spec:input_only + config.gemini_enterprise.direct.api_key.plaintext: + ignore_remote: spec:input_only + config.inference_table.is_deleted: + ignore_remote: spec:output_only + config.inference_table.table: + ignore_remote: spec:output_only + config.microsoft_foundry.direct.api_key.plaintext: + ignore_remote: spec:input_only + config.microsoft_foundry.direct.entra_service_principal.client_secret.plaintext: + ignore_remote: spec:input_only + config.openai.direct.api_key.plaintext: + ignore_remote: spec:input_only + config.provider_type: + action: immutable reason: spec:immutable - -ignore_remote_changes: - - field: config.amazon_bedrock.direct.aws_access_key.secret_access_key.plaintext - reason: spec:input_only - - field: config.anthropic.direct.api_key.plaintext - reason: spec:input_only - - field: config.azure_openai.direct.api_key.plaintext - reason: spec:input_only - - field: config.azure_openai.direct.entra_service_principal.client_secret.plaintext - reason: spec:input_only - - field: config.custom.direct.api_key.plaintext - reason: spec:input_only - - field: config.custom.direct.header_auth.api_key_value.plaintext - reason: spec:input_only - - field: config.gemini_enterprise.direct.api_key.plaintext - reason: spec:input_only - - field: config.microsoft_foundry.direct.api_key.plaintext - reason: spec:input_only - - field: config.microsoft_foundry.direct.entra_service_principal.client_secret.plaintext - reason: spec:input_only - - field: config.openai.direct.api_key.plaintext - reason: spec:input_only - - - field: config.inference_table.is_deleted - reason: spec:output_only - - field: config.inference_table.table - reason: spec:output_only diff --git a/bundle/direct/dresources/configs/model_provider_services.yml b/bundle/direct/dresources/configs/model_provider_services.yml index f83495c340f..26fab21fdb5 100644 --- a/bundle/direct/dresources/configs/model_provider_services.yml +++ b/bundle/direct/dresources/configs/model_provider_services.yml @@ -1,9 +1,9 @@ -provided_id_fields: +fields: # parent + model_provider_service_id compose the server-derived resource # name (model-provider-services/{catalog}.{schema}.{model_provider_service}), # which is the ID the resource is fetched by. Both immutable; a local # change recreates. DoRead reconstructs them from the returned name. - - field: parent - reason: id_field - - field: model_provider_service_id - reason: id_field + parent: + action: id + model_provider_service_id: + action: id diff --git a/bundle/direct/dresources/configs/model_services.generated.yml b/bundle/direct/dresources/configs/model_services.generated.yml index 1d2a16b848f..9895ec41366 100644 --- a/bundle/direct/dresources/configs/model_services.generated.yml +++ b/bundle/direct/dresources/configs/model_services.generated.yml @@ -1,15 +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 +fields: + config.inference_table.is_deleted: + ignore_remote: spec:output_only + config.inference_table.table: + ignore_remote: spec:output_only + config.routing.destinations[*].is_deleted: + ignore_remote: spec:output_only + config.routing.destinations[*].provisioned_throughput_config.model: + ignore_remote: spec:output_only + config.routing.fallback.destinations[*].is_deleted: + ignore_remote: spec:output_only + config.routing.fallback.destinations[*].provisioned_throughput_config.model: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/model_services.yml b/bundle/direct/dresources/configs/model_services.yml index d1b7a91e6e5..f8d72e18405 100644 --- a/bundle/direct/dresources/configs/model_services.yml +++ b/bundle/direct/dresources/configs/model_services.yml @@ -1,10 +1,10 @@ -provided_id_fields: +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 + parent: + action: id + model_service_id: + action: id diff --git a/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml b/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml index 71df6d6f832..cbed99d1e51 100644 --- a/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml +++ b/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml @@ -1,34 +1,33 @@ # 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 +fields: + config.served_entities[*].burst_scaling_enabled: + ignore_remote: spec:input_only + config.served_entities[*].external_model.ai21labs_config.ai21labs_api_key_plaintext: + ignore_remote: spec:input_only + config.served_entities[*].external_model.amazon_bedrock_config.aws_access_key_id_plaintext: + ignore_remote: spec:input_only + config.served_entities[*].external_model.amazon_bedrock_config.aws_secret_access_key_plaintext: + ignore_remote: spec:input_only + config.served_entities[*].external_model.anthropic_config.anthropic_api_key_plaintext: + ignore_remote: spec:input_only + config.served_entities[*].external_model.cohere_config.cohere_api_key_plaintext: + ignore_remote: spec:input_only + config.served_entities[*].external_model.custom_provider_config.api_key_auth.value_plaintext: + ignore_remote: spec:input_only + config.served_entities[*].external_model.custom_provider_config.bearer_token_auth.token_plaintext: + ignore_remote: spec:input_only + config.served_entities[*].external_model.databricks_model_serving_config.databricks_api_token_plaintext: + ignore_remote: spec:input_only + config.served_entities[*].external_model.google_cloud_vertex_ai_config.private_key_plaintext: + ignore_remote: spec:input_only + config.served_entities[*].external_model.openai_config.microsoft_entra_client_secret_plaintext: + ignore_remote: spec:input_only + config.served_entities[*].external_model.openai_config.openai_api_key_plaintext: + ignore_remote: spec:input_only + config.served_entities[*].external_model.palm_config.palm_api_key_plaintext: + ignore_remote: spec:input_only + config.served_models[*].burst_scaling_enabled: + ignore_remote: spec:input_only + telemetry_config.inference_table_config.name: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/model_serving_endpoints.yml b/bundle/direct/dresources/configs/model_serving_endpoints.yml index c1f88a79a7c..c39921b8b73 100644 --- a/bundle/direct/dresources/configs/model_serving_endpoints.yml +++ b/bundle/direct/dresources/configs/model_serving_endpoints.yml @@ -1,54 +1,52 @@ # 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: +fields: + name: + action: id # 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: + description: + action: immutable + config.auto_capture_config.catalog_name: + action: immutable + config.auto_capture_config.schema_name: + action: immutable + config.auto_capture_config.table_name_prefix: + action: immutable + route_optimized: + action: immutable # 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 + budget_policy_id: + ignore_remote: no_update_api + action: ignore_local 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 + telemetry_config.table_names: + ignore_remote: input_only + rate_limits: + action: ignore_local reason: not_implemented -backend_defaults: # Remote-only telemetry is reported as a change at this parent path. - - field: telemetry_config - + telemetry_config: + backend_default: true # 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 - + config.served_entities[*].name: + backend_default: true # 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 - + config.served_entities[*].workload_type: + backend_default: true # 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 - + config.auto_capture_config.enabled: + backend_default: true # 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 + config.traffic_config: + backend_default: true diff --git a/bundle/direct/dresources/configs/models.yml b/bundle/direct/dresources/configs/models.yml index 15032fac685..b208d4e7de1 100644 --- a/bundle/direct/dresources/configs/models.yml +++ b/bundle/direct/dresources/configs/models.yml @@ -1,18 +1,17 @@ -provided_id_fields: +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 + name: + action: id 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 + # 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. + tags: + ignore_remote: terraform_compat + action: ignore_local reason: terraform_compat diff --git a/bundle/direct/dresources/configs/pipelines.generated.yml b/bundle/direct/dresources/configs/pipelines.generated.yml index 60639caed59..2c4a5f6d98b 100644 --- a/bundle/direct/dresources/configs/pipelines.generated.yml +++ b/bundle/direct/dresources/configs/pipelines.generated.yml @@ -1,5 +1,5 @@ # Generated, do not edit. -ignore_remote_changes: - - field: ingestion_definition.source_type - reason: spec:output_only +fields: + ingestion_definition.source_type: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/pipelines.yml b/bundle/direct/dresources/configs/pipelines.yml index 59b92f268d6..c65db7858c7 100644 --- a/bundle/direct/dresources/configs/pipelines.yml +++ b/bundle/direct/dresources/configs/pipelines.yml @@ -1,32 +1,35 @@ -recreate_on_changes: - - field: storage - reason: immutable - - field: ingestion_definition.connection_name - reason: immutable - - field: ingestion_definition.ingestion_gateway_id - reason: immutable +fields: + storage: + action: immutable + ingestion_definition.connection_name: + action: immutable + ingestion_definition.ingestion_gateway_id: + action: 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 + gateway_definition.connection_id: + action: immutable + gateway_definition.connection_name: + action: immutable + gateway_definition.gateway_storage_catalog: + action: immutable + gateway_definition.gateway_storage_schema: + action: 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 + ingestion_definition.ingest_from_uc_foreign_catalog: + action: 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. + deployment.version_id: + ignore_remote: auto + action: ignore_local 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 + # "id" is output-only, providing it in config would be a mistake + id: + ignore_remote: "!drop" + action: ignore_local 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 @@ -34,39 +37,31 @@ ignore_remote_changes: # 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 + run_as: + ignore_remote: 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: + allow_duplicate_names: + ignore_remote: input_only # Pipelines adds ANSI mode to the configuration map when it is omitted. - - field: configuration['spark.sql.ansi.enabled'] - values: ["true"] - + configuration['spark.sql.ansi.enabled']: + backend_values: ["true"] + clusters[*].node_type_id: + backend_default: true # 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 - + clusters[*].driver_node_type_id: + backend_default: true # 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 - + clusters[*].enable_local_disk_encryption: + backend_default: true # 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 + event_log.catalog: + backend_default: true + event_log.schema: + backend_default: true diff --git a/bundle/direct/dresources/configs/postgres_branches.generated.yml b/bundle/direct/dresources/configs/postgres_branches.generated.yml index 07e938094ce..04244acd604 100644 --- a/bundle/direct/dresources/configs/postgres_branches.generated.yml +++ b/bundle/direct/dresources/configs/postgres_branches.generated.yml @@ -1,29 +1,27 @@ # Generated, do not edit. -recreate_on_changes: - - field: source_branch +fields: + expire_time: + ignore_remote: spec:input_only + is_protected: + ignore_remote: spec:input_only + no_expiry: + ignore_remote: spec:input_only + source_branch: + action: immutable reason: spec:immutable - - field: source_branch_lsn + ignore_remote: spec:input_only + source_branch_lsn: + action: immutable reason: spec:immutable - - field: source_branch_time + ignore_remote: spec:input_only + source_branch_time: + action: immutable reason: spec:immutable - - field: source_snapshot + ignore_remote: spec:input_only + source_snapshot: + action: immutable 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 + ignore_remote: spec:input_only + ttl: + ignore_remote: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_branches.yml b/bundle/direct/dresources/configs/postgres_branches.yml index ff15d1428d5..90c165ee8b5 100644 --- a/bundle/direct/dresources/configs/postgres_branches.yml +++ b/bundle/direct/dresources/configs/postgres_branches.yml @@ -1,14 +1,14 @@ -provided_id_fields: +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: + parent: + action: id + branch_id: + action: id # replace_existing only takes effect on create; toggling it later is a no-op. - - field: replace_existing + replace_existing: + action: ignore_local reason: input_only -stable_output_fields: # name ("projects/{project_id}/branches/{branch_id}") is backend-assigned at # creation and immutable; see the stable_output_fields doc in config.go and ES-2202624. - - field: name + name: + stable_output: true diff --git a/bundle/direct/dresources/configs/postgres_catalogs.generated.yml b/bundle/direct/dresources/configs/postgres_catalogs.generated.yml index 63f0569c669..5ad0248aaeb 100644 --- a/bundle/direct/dresources/configs/postgres_catalogs.generated.yml +++ b/bundle/direct/dresources/configs/postgres_catalogs.generated.yml @@ -1,13 +1,11 @@ # Generated, do not edit. -recreate_on_changes: - - field: postgres_database +fields: + branch: + ignore_remote: spec:input_only + create_database_if_missing: + ignore_remote: spec:input_only + postgres_database: + action: immutable 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 + ignore_remote: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_catalogs.yml b/bundle/direct/dresources/configs/postgres_catalogs.yml index c4f6945071f..c182891e2ef 100644 --- a/bundle/direct/dresources/configs/postgres_catalogs.yml +++ b/bundle/direct/dresources/configs/postgres_catalogs.yml @@ -1,10 +1,9 @@ -provided_id_fields: +# 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. +action: immutable +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 + catalog_id: + action: id diff --git a/bundle/direct/dresources/configs/postgres_databases.generated.yml b/bundle/direct/dresources/configs/postgres_databases.generated.yml index 9a9eb664188..6735924f29f 100644 --- a/bundle/direct/dresources/configs/postgres_databases.generated.yml +++ b/bundle/direct/dresources/configs/postgres_databases.generated.yml @@ -1,7 +1,7 @@ # Generated, do not edit. -ignore_remote_changes: - - field: postgres_database - reason: spec:input_only - - field: role - reason: spec:input_only +fields: + postgres_database: + ignore_remote: spec:input_only + role: + ignore_remote: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_databases.yml b/bundle/direct/dresources/configs/postgres_databases.yml index 3e6095b97ee..ca220fc7079 100644 --- a/bundle/direct/dresources/configs/postgres_databases.yml +++ b/bundle/direct/dresources/configs/postgres_databases.yml @@ -1,14 +1,14 @@ -provided_id_fields: +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: + parent: + action: id + database_id: + action: id # replace_existing only takes effect on create; toggling it later is a no-op. - - field: replace_existing + replace_existing: + action: ignore_local reason: input_only -stable_output_fields: # name ("projects/{p}/branches/{b}/databases/{database_id}") is backend-assigned # at creation and immutable; see the stable_output_fields doc in config.go and ES-2202624. - - field: name + name: + stable_output: true diff --git a/bundle/direct/dresources/configs/postgres_endpoints.generated.yml b/bundle/direct/dresources/configs/postgres_endpoints.generated.yml index 2560c7f5ef7..69bff94afe8 100644 --- a/bundle/direct/dresources/configs/postgres_endpoints.generated.yml +++ b/bundle/direct/dresources/configs/postgres_endpoints.generated.yml @@ -1,23 +1,21 @@ # Generated, do not edit. -recreate_on_changes: - - field: endpoint_type +fields: + autoscaling_limit_max_cu: + ignore_remote: spec:input_only + autoscaling_limit_min_cu: + ignore_remote: spec:input_only + disabled: + ignore_remote: spec:input_only + endpoint_type: + action: immutable 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 + ignore_remote: spec:input_only + group: + ignore_remote: spec:input_only + no_suspension: + ignore_remote: spec:input_only + settings: + ignore_remote: spec:input_only + suspend_timeout_duration: + ignore_remote: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_endpoints.yml b/bundle/direct/dresources/configs/postgres_endpoints.yml index f1a08c18e7d..42ea9a50b3c 100644 --- a/bundle/direct/dresources/configs/postgres_endpoints.yml +++ b/bundle/direct/dresources/configs/postgres_endpoints.yml @@ -1,14 +1,14 @@ -provided_id_fields: +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: + parent: + action: id + endpoint_id: + action: id # replace_existing only takes effect on create; toggling it later is a no-op. - - field: replace_existing + replace_existing: + action: ignore_local reason: input_only -stable_output_fields: # name ("projects/{p}/branches/{b}/endpoints/{endpoint_id}") is backend-assigned # at creation and immutable; see the stable_output_fields doc in config.go and ES-2202624. - - field: name + name: + stable_output: true diff --git a/bundle/direct/dresources/configs/postgres_projects.generated.yml b/bundle/direct/dresources/configs/postgres_projects.generated.yml index cf29cbf156a..2538762e74f 100644 --- a/bundle/direct/dresources/configs/postgres_projects.generated.yml +++ b/bundle/direct/dresources/configs/postgres_projects.generated.yml @@ -1,27 +1,25 @@ # Generated, do not edit. -recreate_on_changes: - - field: pg_version +fields: + budget_policy_id: + ignore_remote: spec:input_only + custom_tags: + ignore_remote: spec:input_only + custom_tags[*].key: + ignore_remote: spec:input_only + custom_tags[*].value: + ignore_remote: spec:input_only + default_branch: + ignore_remote: spec:input_only + default_endpoint_settings: + ignore_remote: spec:input_only + display_name: + ignore_remote: spec:input_only + enable_pg_native_login: + ignore_remote: spec:input_only + history_retention_duration: + ignore_remote: spec:input_only + pg_version: + action: immutable 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 + ignore_remote: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_projects.yml b/bundle/direct/dresources/configs/postgres_projects.yml index 4c5079e7079..7ad7a50a6cb 100644 --- a/bundle/direct/dresources/configs/postgres_projects.yml +++ b/bundle/direct/dresources/configs/postgres_projects.yml @@ -1,8 +1,8 @@ -provided_id_fields: +fields: # project_id is immutable (part of hierarchical name, not in API spec) - - field: project_id - reason: id_field -stable_output_fields: + project_id: + action: id # name ("projects/{project_id}") is backend-assigned at creation and immutable; # see the stable_output_fields doc in config.go and ES-2202624. - - field: name + name: + stable_output: true diff --git a/bundle/direct/dresources/configs/postgres_roles.generated.yml b/bundle/direct/dresources/configs/postgres_roles.generated.yml index d5b37104344..da3d2acd5b9 100644 --- a/bundle/direct/dresources/configs/postgres_roles.generated.yml +++ b/bundle/direct/dresources/configs/postgres_roles.generated.yml @@ -1,13 +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 +fields: + attributes: + ignore_remote: spec:input_only + auth_method: + ignore_remote: spec:input_only + identity_type: + ignore_remote: spec:input_only + membership_roles: + ignore_remote: spec:input_only + postgres_role: + ignore_remote: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_roles.yml b/bundle/direct/dresources/configs/postgres_roles.yml index f7346b98f4e..237e4581bde 100644 --- a/bundle/direct/dresources/configs/postgres_roles.yml +++ b/bundle/direct/dresources/configs/postgres_roles.yml @@ -1,26 +1,26 @@ -ignore_local_changes: +fields: # replace_existing only takes effect on create; toggling it later is a no-op. - - field: replace_existing + replace_existing: + action: ignore_local reason: input_only -stable_output_fields: # name ("projects/{project_id}/branches/{branch_id}/roles/{role_id}") is # backend-assigned at creation and immutable; see the stable_output_fields doc # in config.go and ES-2202624. - - field: name -recreate_on_changes: + name: + stable_output: true # parent and role_id are immutable (together they form the hierarchical name). - - field: parent - reason: immutable - - field: role_id - reason: immutable + parent: + action: immutable + role_id: + action: 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_role: + action: immutable + auth_method: + action: immutable + identity_type: + action: immutable diff --git a/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml b/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml index d81b0d30548..ffe9fe0564b 100644 --- a/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml +++ b/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml @@ -1,10 +1,10 @@ -provided_id_fields: +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 -stable_output_fields: + branch: + action: id # name ("{branch}/snapshot-schedule") is backend-assigned at creation and # immutable; see the stable_output_fields doc in config.go and ES-2202624. - - field: name + name: + stable_output: true diff --git a/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml b/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml index 082f93bd66e..c9a40227d43 100644 --- a/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml +++ b/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml @@ -1,41 +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 +fields: + accelerated_sync: + ignore_remote: spec:input_only + branch: + ignore_remote: spec:input_only + create_database_objects_if_missing: + ignore_remote: spec:input_only + existing_pipeline_id: + ignore_remote: spec:input_only + extra_columns: + ignore_remote: spec:input_only + extra_columns[*].column_name: + ignore_remote: spec:input_only + extra_columns[*].column_type: + ignore_remote: spec:input_only + extra_columns[*].compute: + ignore_remote: spec:input_only + extra_columns[*].maintenance: + ignore_remote: spec:input_only + new_pipeline_spec: + ignore_remote: spec:input_only + postgres_database: + ignore_remote: spec:input_only + primary_key_columns: + ignore_remote: spec:input_only + scheduling_policy: + ignore_remote: spec:input_only + source_table_full_name: + ignore_remote: spec:input_only + timeseries_key: + ignore_remote: spec:input_only + type_overrides: + ignore_remote: spec:input_only + type_overrides[*].column_name: + ignore_remote: spec:input_only + type_overrides[*].pg_type: + ignore_remote: spec:input_only + type_overrides[*].size: + ignore_remote: spec:input_only diff --git a/bundle/direct/dresources/configs/postgres_synced_tables.yml b/bundle/direct/dresources/configs/postgres_synced_tables.yml index 2a76ea3a253..4135f8a9eb4 100644 --- a/bundle/direct/dresources/configs/postgres_synced_tables.yml +++ b/bundle/direct/dresources/configs/postgres_synced_tables.yml @@ -5,8 +5,7 @@ # 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 +action: immutable +fields: + synced_table_id: + action: id diff --git a/bundle/direct/dresources/configs/quality_monitors.yml b/bundle/direct/dresources/configs/quality_monitors.yml index 98f5b059016..f60105893cc 100644 --- a/bundle/direct/dresources/configs/quality_monitors.yml +++ b/bundle/direct/dresources/configs/quality_monitors.yml @@ -1,6 +1,5 @@ -provided_id_fields: - - field: table_name - reason: id_field -recreate_on_changes: - - field: assets_dir - reason: immutable +fields: + table_name: + action: id + assets_dir: + action: immutable diff --git a/bundle/direct/dresources/configs/registered_models.generated.yml b/bundle/direct/dresources/configs/registered_models.generated.yml index 9e376d45246..86a25d73ad8 100644 --- a/bundle/direct/dresources/configs/registered_models.generated.yml +++ b/bundle/direct/dresources/configs/registered_models.generated.yml @@ -1,5 +1,5 @@ # Generated, do not edit. -ignore_remote_changes: - - field: browse_only - reason: spec:output_only +fields: + browse_only: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/registered_models.yml b/bundle/direct/dresources/configs/registered_models.yml index 5d6bd9fd8de..582a8be8c64 100644 --- a/bundle/direct/dresources/configs/registered_models.yml +++ b/bundle/direct/dresources/configs/registered_models.yml @@ -1,34 +1,35 @@ -ignore_remote_changes: +fields: # 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 + created_at: + ignore_remote: output_only + created_by: + ignore_remote: output_only + updated_at: + ignore_remote: output_only + updated_by: + ignore_remote: 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: + aliases: + ignore_remote: input_only # The name can technically be updated without recreate. We recreate for now though # to match TF implementation. - - field: name + name: + action: id 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: + catalog_name: + action: id + schema_name: + action: id + storage_location: + action: immutable # owner, full_name, metastore_id are Computed in TF (backend-set output fields). - - field: owner - - field: full_name - - field: metastore_id + owner: + backend_default: true + full_name: + backend_default: true + metastore_id: + backend_default: true diff --git a/bundle/direct/dresources/configs/schemas.yml b/bundle/direct/dresources/configs/schemas.yml index 8ef27e71d15..d07da2310bc 100644 --- a/bundle/direct/dresources/configs/schemas.yml +++ b/bundle/direct/dresources/configs/schemas.yml @@ -1,17 +1,14 @@ -provided_id_fields: +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: + name: + action: id + catalog_name: + action: id + storage_root: + action: immutable + compare: trim_slash # 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.*'] + properties['unity.catalog.managed.*.defaults.*']: + backend_default: true diff --git a/bundle/direct/dresources/configs/secret_scopes.permissions.yml b/bundle/direct/dresources/configs/secret_scopes.permissions.yml index 61c734eb8c1..cca2ea61f74 100644 --- a/bundle/direct/dresources/configs/secret_scopes.permissions.yml +++ b/bundle/direct/dresources/configs/secret_scopes.permissions.yml @@ -1,6 +1,6 @@ # Permissions for secret scopes use ResourceSecretScopeAcls. -updatable_id_fields: +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 + scope_name: + action: id_renameable diff --git a/bundle/direct/dresources/configs/secret_scopes.yml b/bundle/direct/dresources/configs/secret_scopes.yml index 12f6429a3d3..3a37dc4ee18 100644 --- a/bundle/direct/dresources/configs/secret_scopes.yml +++ b/bundle/direct/dresources/configs/secret_scopes.yml @@ -1,10 +1,9 @@ -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 +fields: + scope: + action: id + scope_backend_type: + action: immutable + backend_azure_keyvault: + action: immutable + initial_manage_principal: + action: immutable diff --git a/bundle/direct/dresources/configs/secrets.generated.yml b/bundle/direct/dresources/configs/secrets.generated.yml index 34877e8fb10..c73fa6c81c4 100644 --- a/bundle/direct/dresources/configs/secrets.generated.yml +++ b/bundle/direct/dresources/configs/secrets.generated.yml @@ -1,32 +1,32 @@ # Generated, do not edit. -recreate_on_changes: - - field: catalog_name +fields: + catalog_name: + action: immutable reason: spec:immutable - - field: name + create_time: + ignore_remote: spec:output_only + created_by: + ignore_remote: spec:output_only + effective_owner: + ignore_remote: spec:output_only + effective_value: + ignore_remote: spec:output_only + full_name: + ignore_remote: spec:output_only + metastore_id: + ignore_remote: spec:output_only + name: + action: immutable reason: spec:immutable - - field: schema_name + owner: + ignore_remote: spec:input_only + schema_name: + action: immutable 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 + update_time: + ignore_remote: spec:output_only + updated_by: + ignore_remote: spec:output_only + value: + ignore_remote: spec:input_only diff --git a/bundle/direct/dresources/configs/secrets.yml b/bundle/direct/dresources/configs/secrets.yml index 22ca3d7f49f..d1f8887e122 100644 --- a/bundle/direct/dresources/configs/secrets.yml +++ b/bundle/direct/dresources/configs/secrets.yml @@ -1,3 +1,5 @@ -sensitive_fields: - - field: value - - field: effective_value +fields: + value: + sensitive: true + effective_value: + sensitive: true diff --git a/bundle/direct/dresources/configs/sql_warehouses.yml b/bundle/direct/dresources/configs/sql_warehouses.yml index 582126d2d89..61f61bea438 100644 --- a/bundle/direct/dresources/configs/sql_warehouses.yml +++ b/bundle/direct/dresources/configs/sql_warehouses.yml @@ -1,37 +1,33 @@ -ignore_remote_changes: +fields: # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L62 # common.CustomizeSchemaPath(m, "channel").SetSuppressDiff() - - field: channel - reason: managed - + channel: + ignore_remote: 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 - + tags: + ignore_remote: 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 - + warehouse_type: + ignore_remote: 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 - + min_num_clusters: + ignore_remote: managed # creator_name is readonly, can't be updated via API - - field: creator_name - reason: output_only - -backend_defaults: + creator_name: + ignore_remote: output_only # 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 - + lifecycle: + backend_default: true + lifecycle.started: + backend_default: true # 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 + enable_serverless_compute: + backend_default: true diff --git a/bundle/direct/dresources/configs/synced_database_tables.generated.yml b/bundle/direct/dresources/configs/synced_database_tables.generated.yml index 2958becd442..95743e431c7 100644 --- a/bundle/direct/dresources/configs/synced_database_tables.generated.yml +++ b/bundle/direct/dresources/configs/synced_database_tables.generated.yml @@ -1,24 +1,23 @@ # 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 +fields: + data_synchronization_status: + ignore_remote: spec:output_only + database_instance_name: + ignore_remote: spec:input_only + effective_database_instance_name: + ignore_remote: spec:output_only + effective_logical_database_name: + ignore_remote: spec:output_only + logical_database_name: + ignore_remote: spec:input_only + spec.accelerated_sync: + ignore_remote: spec:input_only + spec.create_database_objects_if_missing: + ignore_remote: spec:input_only + spec.existing_pipeline_id: + ignore_remote: spec:input_only + spec.new_pipeline_spec: + ignore_remote: spec:input_only + unity_catalog_provisioning_state: + ignore_remote: spec:output_only diff --git a/bundle/direct/dresources/configs/synced_database_tables.yml b/bundle/direct/dresources/configs/synced_database_tables.yml index deb5126e6e1..863ddd38cf3 100644 --- a/bundle/direct/dresources/configs/synced_database_tables.yml +++ b/bundle/direct/dresources/configs/synced_database_tables.yml @@ -1,11 +1,10 @@ -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 +action: immutable +fields: + name: + action: id diff --git a/bundle/direct/dresources/configs/vector_search_endpoints.yml b/bundle/direct/dresources/configs/vector_search_endpoints.yml index 34fe753bd2a..b1748c15fef 100644 --- a/bundle/direct/dresources/configs/vector_search_endpoints.yml +++ b/bundle/direct/dresources/configs/vector_search_endpoints.yml @@ -1,12 +1,10 @@ -provided_id_fields: +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: + name: + action: id + endpoint_type: + action: immutable # 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 + budget_policy_id: + ignore_remote: effective_vs_requested diff --git a/bundle/direct/dresources/configs/vector_search_indexes.yml b/bundle/direct/dresources/configs/vector_search_indexes.yml index 9e5e044030c..15529eb87c3 100644 --- a/bundle/direct/dresources/configs/vector_search_indexes.yml +++ b/bundle/direct/dresources/configs/vector_search_indexes.yml @@ -1,22 +1,20 @@ -provided_id_fields: - - field: name - reason: id_field -recreate_on_changes: +fields: + name: + action: id # 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: + endpoint_name: + action: immutable + index_type: + action: immutable + index_subtype: + action: immutable + primary_key: + action: immutable + delta_sync_index_spec: + action: immutable + direct_access_index_spec: + action: immutable # 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 @@ -24,5 +22,5 @@ ignore_remote_changes: # 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 + direct_access_index_spec.schema_json: + ignore_remote: normalized_by_backend diff --git a/bundle/direct/dresources/configs/volumes.yml b/bundle/direct/dresources/configs/volumes.yml index d4b68e95875..b01cffc5d7c 100644 --- a/bundle/direct/dresources/configs/volumes.yml +++ b/bundle/direct/dresources/configs/volumes.yml @@ -1,18 +1,14 @@ -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: +fields: + catalog_name: + action: id + schema_name: + action: id # 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 + storage_location: + action: immutable + compare: trim_slash + volume_type: + action: immutable + name: + action: id_renameable diff --git a/bundle/direct/dresources/configs_v2/catalogs.yml b/bundle/direct/dresources/configs_v2/catalogs.yml deleted file mode 100644 index 01e1c52bbc3..00000000000 --- a/bundle/direct/dresources/configs_v2/catalogs.yml +++ /dev/null @@ -1,13 +0,0 @@ -fields: - name: {action: id_renameable} - connection_name: {action: immutable} - provider_name: {action: immutable} - share_name: {action: immutable} - - # 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 - storage_root: {action: immutable, compare: trim_slash} - - # UC auto-populates unity.catalog.managed..defaults.* keys after create. - properties['unity.catalog.managed.*.defaults.*']: {action: backend_owned} diff --git a/bundle/direct/dresources/configs_v2/dashboards.yml b/bundle/direct/dresources/configs_v2/dashboards.yml deleted file mode 100644 index e9abb11659d..00000000000 --- a/bundle/direct/dresources/configs_v2/dashboards.yml +++ /dev/null @@ -1,10 +0,0 @@ -fields: - # "serialized_dashboard" holds inlined dashboard JSON that differs textually - # between local config and remote (we rely on etag for drift), and is never read - # back from state, so we persist only its content hash. - serialized_dashboard: {action: ignore_remote, reason: etag_based, hashed: true} - - # "dataset_catalog" and "dataset_schema" are write-only fields the server does not - # return, so local config (values) and remote state (empty) always differ. - dataset_catalog: {action: ignore_remote, reason: input_only} - dataset_schema: {action: ignore_remote, reason: input_only} diff --git a/bundle/direct/dresources/configs_v2/experiments.yml b/bundle/direct/dresources/configs_v2/experiments.yml deleted file mode 100644 index 7d95707c3ba..00000000000 --- a/bundle/direct/dresources/configs_v2/experiments.yml +++ /dev/null @@ -1,6 +0,0 @@ -fields: - # TF implementation: https://github.com/databricks/terraform-provider-databricks/blob/6c106e8e7052bb2726148d66309fd460ed444236/mlflow/resource_mlflow_experiment.go#L22 - artifact_location: {action: immutable} - - # Tags updates are not supported by TF. Skip both local edits and remote drift. - tags: {action: ignore, reason: terraform_compat} diff --git a/bundle/direct/dresources/configs_v2/postgres_branches.yml b/bundle/direct/dresources/configs_v2/postgres_branches.yml deleted file mode 100644 index 425b6b6f024..00000000000 --- a/bundle/direct/dresources/configs_v2/postgres_branches.yml +++ /dev/null @@ -1,11 +0,0 @@ -fields: - # parent and branch_id are immutable (part of hierarchical name, not in API spec). - parent: {action: id} - branch_id: {action: id} - - # name ("projects/{project_id}/branches/{branch_id}") is backend-assigned at - # creation and immutable; see the stable_output_fields doc in config.go and ES-2202624. - name: {stable_output: true} - - # replace_existing only takes effect on create; toggling it later is a no-op. - replace_existing: {action: ignore_local, reason: input_only} diff --git a/bundle/direct/dresources/configs_v2/postgres_projects.yml b/bundle/direct/dresources/configs_v2/postgres_projects.yml deleted file mode 100644 index 6039875c077..00000000000 --- a/bundle/direct/dresources/configs_v2/postgres_projects.yml +++ /dev/null @@ -1,7 +0,0 @@ -fields: - # project_id is immutable (part of hierarchical name, not in API spec). - project_id: {action: id} - - # name ("projects/{project_id}") is backend-assigned at creation and immutable; - # see the stable_output_fields doc in config.go and ES-2202624. - name: {stable_output: true} diff --git a/bundle/direct/dresources/configs_v2/postgres_roles.yml b/bundle/direct/dresources/configs_v2/postgres_roles.yml deleted file mode 100644 index ee4fd763546..00000000000 --- a/bundle/direct/dresources/configs_v2/postgres_roles.yml +++ /dev/null @@ -1,21 +0,0 @@ -fields: - # name ("projects/{project_id}/branches/{branch_id}/roles/{role_id}") is - # backend-assigned at creation and immutable; see the stable_output_fields doc - # in config.go and ES-2202624. - name: {stable_output: true} - - # parent and role_id are immutable (together they form the hierarchical name). - parent: {action: immutable} - role_id: {action: 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. - postgres_role: {action: immutable} - auth_method: {action: immutable} - identity_type: {action: immutable} - - # replace_existing only takes effect on create; toggling it later is a no-op. - replace_existing: {action: ignore_local, reason: input_only} diff --git a/bundle/direct/dresources/configs_v2/schemas.yml b/bundle/direct/dresources/configs_v2/schemas.yml deleted file mode 100644 index 5bb78f3fda8..00000000000 --- a/bundle/direct/dresources/configs_v2/schemas.yml +++ /dev/null @@ -1,11 +0,0 @@ -fields: - # UC lowercases identifier names; remote returns "myschema" for config "MySchema". - name: {action: id} - catalog_name: {action: id} - - storage_root: {action: immutable, compare: trim_slash} - - # 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". - properties['unity.catalog.managed.*.defaults.*']: {action: backend_owned} diff --git a/bundle/direct/dresources/configs_v2/secrets.yml b/bundle/direct/dresources/configs_v2/secrets.yml deleted file mode 100644 index 66d5675bdcd..00000000000 --- a/bundle/direct/dresources/configs_v2/secrets.yml +++ /dev/null @@ -1,5 +0,0 @@ -fields: - # Modifier-only: no action, just redaction. The immutable/input-only rules for - # these fields come from secrets.generated.yml. - value: {sensitive: true} - effective_value: {sensitive: true} diff --git a/bundle/direct/dresources/configs_v2/volumes.yml b/bundle/direct/dresources/configs_v2/volumes.yml deleted file mode 100644 index 10db253d747..00000000000 --- a/bundle/direct/dresources/configs_v2/volumes.yml +++ /dev/null @@ -1,9 +0,0 @@ -fields: - catalog_name: {action: id} - schema_name: {action: id} - name: {action: id_renameable} - volume_type: {action: immutable} - - # 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 - storage_location: {action: immutable, compare: trim_slash} diff --git a/bundle/direct/dresources/field_policy.go b/bundle/direct/dresources/field_policy.go index 8b39bd17177..7b33591003c 100644 --- a/bundle/direct/dresources/field_policy.go +++ b/bundle/direct/dresources/field_policy.go @@ -1,92 +1,97 @@ package dresources import ( - "embed" + "encoding/json" "fmt" - "io/fs" "maps" - "path" "slices" - "strings" - "sync" "github.com/databricks/cli/libs/structs/structpath" - "go.yaml.in/yaml/v3" ) -// FieldPolicy is the field-keyed alternative to the flat per-behaviour lists in -// ResourceLifecycleConfig. A field prefix names exactly one Action plus optional -// orthogonal modifiers, so a field cannot be handed two contradictory actions: -// the config is a map keyed by field, and the YAML loader rejects a duplicate key. -// That makes the action-exclusivity checks in config_test.go unnecessary — the -// redundancy is unrepresentable rather than merely tested against. +// FieldPolicyConfig is one resource's lifecycle config in the field-keyed format: a +// map from field prefix to its policy, plus a few resource-level settings. It is the +// authoring format for configs/.yml; Lower turns it into the flat +// ResourceLifecycleConfig the planner consumes. // -// Action decides what happens when the field differs. The three modifiers -// (Compare, Hashed, Sensitive) live on independent axes — comparison, state -// storage, redaction — so they legitimately combine with any Action and can -// never contradict it. +// Keying by field makes a field's whole policy live in one place and makes a +// duplicate field a YAML load error, so the redundancy the flat format needed +// dedicated exclusivity/redundancy tests for is unrepresentable here. +type FieldPolicyConfig struct { + // Action is a resource-root local-change action: the whole resource behaves this + // way on any change (e.g. immutable when the API has no update endpoint). Mirrors + // a flat action rule with the field omitted. Reason overrides its default. + Action action `yaml:"action,omitempty"` + Reason string `yaml:"reason,omitempty"` + + // RemoteAdditionsWhen gates ignore_remote_additions at the resource root (the + // whole object is backend-co-owned when this field is set). Per-object gates go + // on the field entry instead. See RemoteAdditionRule. + RemoteAdditionsWhen *structpath.PathNode `yaml:"remote_additions_when,omitempty"` + + Fields map[string]FieldPolicy `yaml:"fields"` +} + +// FieldPolicy is one field's lifecycle policy: a single Action (the local-change +// decision) plus independent modifiers on other axes (remote drift, comparison, +// backend defaults, storage, redaction, reference resolution). The modifiers never +// conflict with the Action, so a field cannot be handed contradictory behaviours. type FieldPolicy struct { - // Action is the single lifecycle decision for the field. Empty means the - // default (a normal in-place update). Its UnmarshalYAML rejects any value - // outside validActions, so an unknown action fails at parse time. + // Action is the local-change decision. Empty = a normal in-place update. Action action `yaml:"action,omitempty"` - // Reason overrides the default reason string surfaced in the plan. Actions - // with an obvious reason (id, immutable, ...) supply a default; ignore* - // actions require it because the justification is resource-specific. + // Reason overrides the reason surfaced in the plan for Action. id/id_renameable/ + // immutable supply a default; ignore_local requires it (the justification varies). Reason string `yaml:"reason,omitempty"` - // Compare customizes equality before the action is decided. A string (not a - // bool) because comparison modes can grow and could combine — "" | "trim_slash" - // today, "ci" etc. later. + // IgnoreRemote, when non-empty, skips remote-only drift on the field; its value is + // the reason (which varies: managed, input_only, etag_based, spec:input_only, ...). + IgnoreRemote string `yaml:"ignore_remote,omitempty"` + + // Compare customizes equality before the action is decided. "" | "trim_slash". Compare string `yaml:"compare,omitempty"` - // Hashed persists the value to state as a content hash instead of the raw - // value. For large, equality-only fields never read back from state. + // BackendDefault skips a config-absent value the backend populated. BackendValues + // constrains it to specific remote values and implies BackendDefault. + BackendDefault bool `yaml:"backend_default,omitempty"` + BackendValues []any `yaml:"backend_values,omitempty"` + + // Hashed persists the value to state as a content hash instead of the raw value. Hashed bool `yaml:"hashed,omitempty"` // Sensitive marks the field for redaction in logs and state. Sensitive bool `yaml:"sensitive,omitempty"` - // StableOutput marks an output-only field the backend assigns at creation and - // never changes, so a cross-resource reference to it resolves from the remote - // cache during a keeps-ID update instead of being delayed. See - // StableOutputFields in config.go. + // StableOutput marks an output-only field the backend assigns at creation and never + // changes, so a cross-resource reference resolves from the remote cache during a + // keeps-ID update. See StableOutputFields in config.go. StableOutput bool `yaml:"stable_output,omitempty"` + + // RemoteAdditionsWhen gates ignore_remote_additions on this object (its contents are + // backend-co-owned when the named sibling field is set). + RemoteAdditionsWhen *structpath.PathNode `yaml:"remote_additions_when,omitempty"` } -// action is the lifecycle decision for a field. It is a defined type with an -// UnmarshalYAML that validates against validActions, so an unknown action is a -// parse-time error (with file/line) rather than a failure discovered in Lower. +// action is the local-change decision for a field. Its UnmarshalYAML validates against +// validActions, so an unknown action is a parse-time error (with file/line). type action string -// Action values. Each maps to one action rung of the plan ladder; see the table -// in README.md. immutable/id/id_renameable all recreate-or-rename on a local -// change and skip a remote-only diff; ignore* skip; backend_owned tolerates a -// value the backend populated. const ( actionMutable action = "" // default: a normal in-place update actionImmutable action = "immutable" // recreate on local change actionID action = "id" // provided id: recreate on change, skip remote drift actionIDRenameable action = "id_renameable" // updatable id: rename in place on change - actionIgnore action = "ignore" // skip both local edits and remote drift - actionIgnoreLocal action = "ignore_local" // skip local edits only - actionIgnoreRemote action = "ignore_remote" // skip remote drift only - actionBackendOwned action = "backend_owned" // skip a config-absent value the backend set + actionIgnoreLocal action = "ignore_local" // skip local edits ) -// validActions is the single source of truth for which actions parse. The -// lowerAction switch must handle each; TestFieldPolicyEveryActionLowers keeps the -// two in sync. +// validActions is the single source of truth for which actions parse. lowerAction must +// handle each; TestFieldPolicyEveryActionLowers keeps the two in sync. var validActions = map[action]bool{ actionMutable: true, actionImmutable: true, actionID: true, actionIDRenameable: true, - actionIgnore: true, actionIgnoreLocal: true, - actionIgnoreRemote: true, - actionBackendOwned: true, } func (a *action) UnmarshalYAML(unmarshal func(any) error) error { @@ -112,17 +117,20 @@ const ( reasonTrimSlash = "uc_strips_trailing_slash" ) -// FieldPolicyConfig is one resource's field-keyed lifecycle config. -type FieldPolicyConfig struct { - Fields map[string]FieldPolicy `yaml:"fields"` -} - -// Lower translates the field-keyed config into the existing ResourceLifecycleConfig -// so the plan ladder and every consumer keep working unchanged. It is the seam that -// lets the two formats coexist during migration. +// Lower translates the field-keyed config into the flat ResourceLifecycleConfig the +// planner and every other consumer already use, so wiring in this format changes +// nothing downstream. func (c FieldPolicyConfig) Lower() (ResourceLifecycleConfig, error) { var out ResourceLifecycleConfig + // Resource-root rules (field omitted); mirror the flat loader's nil Field. + if err := lowerAction(&out, nil, c.Action, c.Reason); err != nil { + return out, err + } + if c.RemoteAdditionsWhen != nil { + out.IgnoreRemoteAdditions = append(out.IgnoreRemoteAdditions, RemoteAdditionRule{Field: nil, WhenSet: c.RemoteAdditionsWhen}) + } + // Deterministic order keeps the lowered lists stable for goldens and diffs. for _, field := range slices.Sorted(maps.Keys(c.Fields)) { p := c.Fields[field] @@ -130,11 +138,7 @@ func (c FieldPolicyConfig) Lower() (ResourceLifecycleConfig, error) { if err != nil { return out, fmt.Errorf("field %q: %w", field, err) } - - if err := lowerAction(&out, pattern, p); err != nil { - return out, fmt.Errorf("field %q: %w", field, err) - } - if err := lowerModifiers(&out, pattern, field, p); err != nil { + if err := lowerField(&out, pattern, field, p); err != nil { return out, fmt.Errorf("field %q: %w", field, err) } } @@ -142,37 +146,38 @@ func (c FieldPolicyConfig) Lower() (ResourceLifecycleConfig, error) { return out, nil } -func lowerAction(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, p FieldPolicy) error { - rule := FieldRule{Field: pattern, Reason: p.Reason} - switch p.Action { +// lowerAction appends the flat rule for one local-change action on pattern (nil = root). +func lowerAction(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, act action, reason string) error { + switch act { case actionMutable: - if p.Reason != "" { - return fmt.Errorf("reason %q set without an action", p.Reason) + if reason != "" { + return fmt.Errorf("reason %q set without an action", reason) } case actionID: - out.ProvidedIDFields = append(out.ProvidedIDFields, withDefaultReason(rule, reasonIDField)) + out.ProvidedIDFields = append(out.ProvidedIDFields, FieldRule{Field: pattern, Reason: reasonOr(reason, reasonIDField)}) case actionIDRenameable: - out.UpdatableIDFields = append(out.UpdatableIDFields, withDefaultReason(rule, reasonIDChanges)) + out.UpdatableIDFields = append(out.UpdatableIDFields, FieldRule{Field: pattern, Reason: reasonOr(reason, reasonIDChanges)}) case actionImmutable: - out.RecreateOnChanges = append(out.RecreateOnChanges, withDefaultReason(rule, reasonImmutable)) - case actionIgnore: - out.IgnoreLocalChanges = append(out.IgnoreLocalChanges, rule) - out.IgnoreRemoteChanges = append(out.IgnoreRemoteChanges, rule) + out.RecreateOnChanges = append(out.RecreateOnChanges, FieldRule{Field: pattern, Reason: reasonOr(reason, reasonImmutable)}) case actionIgnoreLocal: - out.IgnoreLocalChanges = append(out.IgnoreLocalChanges, rule) - case actionIgnoreRemote: - out.IgnoreRemoteChanges = append(out.IgnoreRemoteChanges, rule) - case actionBackendOwned: - out.BackendDefaults = append(out.BackendDefaults, BackendDefaultRule{Field: pattern, Values: nil}) + out.IgnoreLocalChanges = append(out.IgnoreLocalChanges, FieldRule{Field: pattern, Reason: reason}) default: // Unreachable via YAML (UnmarshalYAML validates); guards a value added to // validActions but not handled here. - return fmt.Errorf("unhandled action %q", p.Action) + return fmt.Errorf("unhandled action %q", act) } return nil } -func lowerModifiers(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, field string, p FieldPolicy) error { +func lowerField(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, field string, p FieldPolicy) error { + if err := lowerAction(out, pattern, p.Action, p.Reason); err != nil { + return err + } + + if p.IgnoreRemote != "" { + out.IgnoreRemoteChanges = append(out.IgnoreRemoteChanges, FieldRule{Field: pattern, Reason: p.IgnoreRemote}) + } + switch p.Compare { case "": case compareTrimSlash: @@ -181,68 +186,48 @@ func lowerModifiers(out *ResourceLifecycleConfig, pattern *structpath.PatternNod return fmt.Errorf("unknown compare %q", p.Compare) } + if p.BackendDefault || len(p.BackendValues) > 0 { + values, err := toRawValues(p.BackendValues) + if err != nil { + return err + } + out.BackendDefaults = append(out.BackendDefaults, BackendDefaultRule{Field: pattern, Values: values}) + } + if p.Hashed { out.HashedFields = append(out.HashedFields, field) } - if p.Sensitive { out.SensitiveFields = append(out.SensitiveFields, FieldRule{Field: pattern, Reason: ""}) } - if p.StableOutput { out.StableOutputFields = append(out.StableOutputFields, FieldRule{Field: pattern, Reason: ""}) } + if p.RemoteAdditionsWhen != nil { + out.IgnoreRemoteAdditions = append(out.IgnoreRemoteAdditions, RemoteAdditionRule{Field: pattern, WhenSet: p.RemoteAdditionsWhen}) + } return nil } -func withDefaultReason(rule FieldRule, def string) FieldRule { - if rule.Reason == "" { - rule.Reason = def +func reasonOr(reason, def string) string { + if reason == "" { + return def } - return rule + return reason } -//go:embed configs_v2/*.yml -var fieldPolicyFS embed.FS - -const configV2Dir = "configs_v2" - -// loadFieldPolicyConfigs parses every configs_v2/.yml and lowers it -// to a ResourceLifecycleConfig, keyed by resource type. -var loadFieldPolicyConfigs = sync.OnceValue(func() map[string]ResourceLifecycleConfig { - names, err := fs.Glob(fieldPolicyFS, configV2Dir+"/*"+ymlSuffix) - if err != nil { - panic(err) +// toRawValues marshals native YAML backend_values to JSON, matching BackendDefaultRule.UnmarshalYAML. +func toRawValues(vals []any) ([]json.RawMessage, error) { + if len(vals) == 0 { + return nil, nil } - - result := map[string]ResourceLifecycleConfig{} - for _, name := range names { - resourceType := strings.TrimSuffix(path.Base(name), ymlSuffix) - - data, err := fieldPolicyFS.ReadFile(name) + out := make([]json.RawMessage, 0, len(vals)) + for _, v := range vals { + raw, err := json.Marshal(v) if err != nil { - panic(err) - } - - var fpc FieldPolicyConfig - if err := yaml.Unmarshal(data, &fpc); err != nil { - panic(fmt.Errorf("%s: %w", name, err)) + return nil, err } - - lowered, err := fpc.Lower() - if err != nil { - panic(fmt.Errorf("%s: %w", name, err)) - } - result[resourceType] = lowered - } - return result -}) - -// GetFieldPolicyConfig returns the lowered lifecycle config authored in the -// field-keyed configs_v2/ format, or nil if the resource has no such file. -func GetFieldPolicyConfig(resourceType string) *ResourceLifecycleConfig { - if rc, ok := loadFieldPolicyConfigs()[resourceType]; ok { - return &rc + out = append(out, raw) } - return nil + return out, nil } diff --git a/bundle/direct/dresources/field_policy_test.go b/bundle/direct/dresources/field_policy_test.go index 5f7b4ef87cc..c16c504d347 100644 --- a/bundle/direct/dresources/field_policy_test.go +++ b/bundle/direct/dresources/field_policy_test.go @@ -1,7 +1,6 @@ package dresources import ( - "slices" "testing" "github.com/stretchr/testify/assert" @@ -9,62 +8,6 @@ import ( "go.yaml.in/yaml/v3" ) -// normalizeConfig projects a ResourceLifecycleConfig into an order-independent -// "category -> sorted rules" shape so two configs can be compared for behavioural -// equivalence regardless of the order fields were authored in. -func normalizeConfig(c ResourceLifecycleConfig) map[string][]string { - fieldRules := func(rules []FieldRule) []string { - out := make([]string, 0, len(rules)) - for _, r := range rules { - out = append(out, r.Field.String()+"|"+r.Reason) - } - slices.Sort(out) - return out - } - - backend := make([]string, 0, len(c.BackendDefaults)) - for _, r := range c.BackendDefaults { - backend = append(backend, r.Field.String()) - } - slices.Sort(backend) - - hashed := slices.Clone(c.HashedFields) - slices.Sort(hashed) - - return map[string][]string{ - "ignore_remote_changes": fieldRules(c.IgnoreRemoteChanges), - "ignore_local_changes": fieldRules(c.IgnoreLocalChanges), - "recreate_on_changes": fieldRules(c.RecreateOnChanges), - "provided_id_fields": fieldRules(c.ProvidedIDFields), - "updatable_id_fields": fieldRules(c.UpdatableIDFields), - "normalize_slash": fieldRules(c.NormalizeSlash), - "sensitive_fields": fieldRules(c.SensitiveFields), - "stable_output_fields": fieldRules(c.StableOutputFields), - "backend_defaults": backend, - "hashed_fields": hashed, - } -} - -// TestFieldPolicyLowersToLiveConfig proves the field-keyed configs_v2/.yml files -// lower to exactly the same lifecycle behaviour as the flat hand-written configs/.yml -// they replace. As long as this passes, the plan ladder and every other consumer are -// unaffected, so the migration is behaviour-preserving. -func TestFieldPolicyLowersToLiveConfig(t *testing.T) { - migrated := []string{ - "volumes", "schemas", "catalogs", "experiments", - "postgres_roles", "postgres_projects", "postgres_branches", - "dashboards", "secrets", - } - for _, resourceType := range migrated { - t.Run(resourceType, func(t *testing.T) { - v2 := GetFieldPolicyConfig(resourceType) - require.NotNil(t, v2, "no configs_v2/%s.yml", resourceType) - live := GetResourceConfig(resourceType) - assert.Equal(t, normalizeConfig(*live), normalizeConfig(*v2)) - }) - } -} - // TestFieldPolicyRejectsDuplicateField shows the redundancy class the flat format // needed a dedicated exclusivity test for is now unrepresentable: a field can carry // only one policy, and a repeated key is a load error, not a silently-shadowed rule. @@ -92,3 +35,20 @@ func TestFieldPolicyEveryActionLowers(t *testing.T) { require.NoErrorf(t, err, "action %q", a) } } + +// TestFieldPolicyLowerVolumes pins the lowering of a representative resource that +// exercises id, id_renameable, immutable, and the trim_slash comparison modifier. +func TestFieldPolicyLowerVolumes(t *testing.T) { + rules := func(rs []FieldRule) []string { + out := []string{} + for _, r := range rs { + out = append(out, r.Field.String()+"|"+r.Reason) + } + return out + } + c := GetResourceConfig("volumes") + assert.ElementsMatch(t, []string{"catalog_name|id_field", "schema_name|id_field"}, rules(c.ProvidedIDFields)) + assert.ElementsMatch(t, []string{"name|id_changes"}, rules(c.UpdatableIDFields)) + assert.ElementsMatch(t, []string{"storage_location|immutable", "volume_type|immutable"}, rules(c.RecreateOnChanges)) + assert.ElementsMatch(t, []string{"storage_location|uc_strips_trailing_slash"}, rules(c.NormalizeSlash)) +} diff --git a/bundle/direct/tools/generate_resources.py b/bundle/direct/tools/generate_resources.py index 01302fedc79..5c258dc67ea 100644 --- a/bundle/direct/tools/generate_resources.py +++ b/bundle/direct/tools/generate_resources.py @@ -141,24 +141,9 @@ def filter_prefixes(fields): return result -def write_field_group(lines, header, fields): - """Write a group of fields with field and reason, grouped by behavior.""" - if lines: - lines.append("") - lines.append(f"{header}:") - # Group by behavior - by_behavior = {} - for field, behavior in fields: - by_behavior.setdefault(behavior, []).append(field) - first = True - for behavior in sorted(by_behavior): - if not first: - lines.append("") - first = False - reason = f"spec:{behavior.lower()}" - for field in by_behavior[behavior]: - lines.append(f" - field: {field}") - lines.append(f" reason: {reason}") +def quote_key(field): + """Quote a field key when it starts with a character YAML would misread (e.g. '*').""" + return f'"{field}"' if field[:1] in "*[]{}&!|>%@`\"'#" else field GENERATED_SUFFIX = ".generated.yml" @@ -167,7 +152,12 @@ def write_field_group(lines, header, fields): def generate(behaviors): - """Render one resource's field behaviors, or "" if it has none.""" + """Render one resource's field behaviors in the field-keyed format, or "" if none. + + A field maps to a single action (immutable) plus an ignore_remote modifier whose value + is the reason; the two are filtered for prefixes independently, matching the flat + recreate_on_changes / ignore_remote_changes lists they replace. + """ ignore_remote, recreate = [], [] for field, fb in sorted(behaviors.items()): if "OUTPUT_ONLY" in fb: @@ -183,13 +173,19 @@ def generate(behaviors): if not ignore_remote and not recreate: return "" - lines = [] - if recreate: - write_field_group(lines, "recreate_on_changes", recreate) - if ignore_remote: - write_field_group(lines, "ignore_remote_changes", ignore_remote) + entries = {} # field -> ordered list of (key, value) + for field, _ in recreate: + entries.setdefault(field, []).extend([("action", "immutable"), ("reason", "spec:immutable")]) + for field, behavior in ignore_remote: + entries.setdefault(field, []).append(("ignore_remote", f"spec:{behavior.lower()}")) + + lines = [HEADER, "", "fields:"] + for field in sorted(entries): + lines.append(f" {quote_key(field)}:") + for key, value in entries[field]: + lines.append(f" {key}: {value}") - return HEADER + "\n\n" + "\n".join(lines) + "\n" + return "\n".join(lines) + "\n" def write_files(outdir, resource_behaviors): From b1cce6d19c7d2279578874b17e6e158d4c6f8448 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 30 Sep 2026 10:54:55 +0200 Subject: [PATCH 5/8] dresources: use nil slice in field_policy test (gocritic) Co-authored-by: Isaac --- bundle/direct/dresources/field_policy_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle/direct/dresources/field_policy_test.go b/bundle/direct/dresources/field_policy_test.go index c16c504d347..78fbe57d637 100644 --- a/bundle/direct/dresources/field_policy_test.go +++ b/bundle/direct/dresources/field_policy_test.go @@ -40,7 +40,7 @@ func TestFieldPolicyEveryActionLowers(t *testing.T) { // exercises id, id_renameable, immutable, and the trim_slash comparison modifier. func TestFieldPolicyLowerVolumes(t *testing.T) { rules := func(rs []FieldRule) []string { - out := []string{} + var out []string for _, r := range rs { out = append(out, r.Field.String()+"|"+r.Reason) } From e407dd86f978b8e4350a415bafdeef76d2145e52 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 30 Sep 2026 11:54:19 +0200 Subject: [PATCH 6/8] dresources: use a spec: field for OpenAPI behaviours in generated configs Generated configs listed each field as action: immutable / ignore_remote: spec:, which duplicates the mechanism the behaviour already implies. Replace with a single spec: field (immutable / input_only / output_only, one or a list) that maps to recreate / ignore-remote with a spec: reason. Hand-written configs are unchanged: their input_only etc. are manual decisions, not spec-derived, so they keep action:/ignore_remote: without the spec: prefix. Co-authored-by: Isaac --- .../dresources/configs/alerts.generated.yml | 16 ++-- .../dresources/configs/apps.generated.yml | 50 ++++++------- .../configs/dashboards.generated.yml | 13 ++-- .../configs/database_catalogs.generated.yml | 4 +- .../configs/database_instances.generated.yml | 73 +++++++++---------- .../configs/experiments.generated.yml | 5 +- .../configs/external_locations.generated.yml | 16 ++-- .../configs/genie_spaces.generated.yml | 2 +- .../configs/mcp_services.generated.yml | 4 +- .../model_provider_services.generated.yml | 27 ++++--- .../configs/model_services.generated.yml | 12 +-- .../model_serving_endpoints.generated.yml | 30 ++++---- .../configs/pipelines.generated.yml | 2 +- .../configs/postgres_branches.generated.yml | 24 ++---- .../configs/postgres_catalogs.generated.yml | 8 +- .../configs/postgres_databases.generated.yml | 4 +- .../configs/postgres_endpoints.generated.yml | 18 ++--- .../configs/postgres_projects.generated.yml | 22 +++--- .../configs/postgres_roles.generated.yml | 10 +-- .../postgres_synced_tables.generated.yml | 38 +++++----- .../configs/registered_models.generated.yml | 2 +- .../dresources/configs/secrets.generated.yml | 29 ++++---- .../synced_database_tables.generated.yml | 20 ++--- bundle/direct/dresources/field_policy.go | 60 +++++++++++++++ bundle/direct/dresources/field_policy_test.go | 23 ++++++ bundle/direct/tools/generate_resources.py | 20 ++--- 26 files changed, 298 insertions(+), 234 deletions(-) diff --git a/bundle/direct/dresources/configs/alerts.generated.yml b/bundle/direct/dresources/configs/alerts.generated.yml index 73daaf25c00..cc9d96e22da 100644 --- a/bundle/direct/dresources/configs/alerts.generated.yml +++ b/bundle/direct/dresources/configs/alerts.generated.yml @@ -2,18 +2,18 @@ fields: create_time: - ignore_remote: spec:output_only + spec: output_only effective_run_as: - ignore_remote: spec:output_only + spec: output_only evaluation.last_evaluated_at: - ignore_remote: spec:output_only + spec: output_only evaluation.state: - ignore_remote: spec:output_only + spec: output_only id: - ignore_remote: spec:output_only + spec: output_only lifecycle_state: - ignore_remote: spec:output_only + spec: output_only owner_user_name: - ignore_remote: spec:output_only + spec: output_only update_time: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/apps.generated.yml b/bundle/direct/dresources/configs/apps.generated.yml index c2438a91919..39ffbca1c6b 100644 --- a/bundle/direct/dresources/configs/apps.generated.yml +++ b/bundle/direct/dresources/configs/apps.generated.yml @@ -2,52 +2,52 @@ fields: active_deployment: - ignore_remote: spec:output_only + spec: output_only app_status: - ignore_remote: spec:output_only + spec: output_only compute_status: - ignore_remote: spec:output_only + spec: output_only create_time: - ignore_remote: spec:output_only + spec: output_only creator: - ignore_remote: spec:output_only + spec: output_only default_git_source: - ignore_remote: spec:output_only + spec: output_only default_source_code_path: - ignore_remote: spec:output_only + spec: output_only effective_budget_policy_id: - ignore_remote: spec:output_only + spec: output_only effective_usage_policy_id: - ignore_remote: spec:output_only + spec: output_only effective_user_api_scopes: - ignore_remote: spec:output_only + spec: output_only git_repository.caller_credential_id: - ignore_remote: spec:input_only + spec: input_only git_source: - ignore_remote: spec:input_only + spec: input_only id: - ignore_remote: spec:output_only + spec: output_only oauth2_app_client_id: - ignore_remote: spec:output_only + spec: output_only oauth2_app_integration_id: - ignore_remote: spec:output_only + spec: output_only pending_deployment: - ignore_remote: spec:output_only + spec: output_only resources[*].uc_securable.securable_kind: - ignore_remote: spec:output_only + spec: output_only service_principal_client_id: - ignore_remote: spec:output_only + spec: output_only service_principal_id: - ignore_remote: spec:output_only + spec: output_only service_principal_name: - ignore_remote: spec:output_only + spec: output_only source_code_path: - ignore_remote: spec:input_only + spec: input_only thumbnail_url: - ignore_remote: spec:output_only + spec: output_only update_time: - ignore_remote: spec:output_only + spec: output_only updater: - ignore_remote: spec:output_only + spec: output_only url: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/dashboards.generated.yml b/bundle/direct/dresources/configs/dashboards.generated.yml index 0553cd25c9c..8437a83b8b4 100644 --- a/bundle/direct/dresources/configs/dashboards.generated.yml +++ b/bundle/direct/dresources/configs/dashboards.generated.yml @@ -2,15 +2,14 @@ fields: create_time: - ignore_remote: spec:output_only + spec: output_only dashboard_id: - ignore_remote: spec:output_only + spec: output_only lifecycle_state: - ignore_remote: spec:output_only + spec: output_only parent_path: - action: immutable - reason: spec:immutable + spec: immutable path: - ignore_remote: spec:output_only + spec: output_only update_time: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/database_catalogs.generated.yml b/bundle/direct/dresources/configs/database_catalogs.generated.yml index 4cc9947d970..b84ea7f63f1 100644 --- a/bundle/direct/dresources/configs/database_catalogs.generated.yml +++ b/bundle/direct/dresources/configs/database_catalogs.generated.yml @@ -2,6 +2,6 @@ fields: create_database_if_not_exists: - ignore_remote: spec:input_only + spec: input_only uid: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/database_instances.generated.yml b/bundle/direct/dresources/configs/database_instances.generated.yml index 37d4f89c141..1a45824b18a 100644 --- a/bundle/direct/dresources/configs/database_instances.generated.yml +++ b/bundle/direct/dresources/configs/database_instances.generated.yml @@ -2,75 +2,74 @@ fields: child_instance_refs: - ignore_remote: spec:output_only + spec: output_only child_instance_refs[*].branch_time: - ignore_remote: spec:output_only + spec: output_only child_instance_refs[*].effective_lsn: - ignore_remote: spec:output_only + spec: output_only child_instance_refs[*].lsn: - ignore_remote: spec:output_only + spec: output_only child_instance_refs[*].name: - ignore_remote: spec:output_only + spec: output_only child_instance_refs[*].uid: - ignore_remote: spec:output_only + spec: output_only creation_time: - ignore_remote: spec:output_only + spec: output_only creator: - ignore_remote: spec:output_only + spec: output_only custom_tags: - ignore_remote: spec:input_only + spec: input_only custom_tags[*].key: - ignore_remote: spec:input_only + spec: input_only custom_tags[*].value: - ignore_remote: spec:input_only + spec: input_only effective_capacity: - ignore_remote: spec:output_only + spec: output_only effective_custom_tags: - ignore_remote: spec:output_only + spec: output_only effective_custom_tags[*].key: - ignore_remote: spec:output_only + spec: output_only effective_custom_tags[*].value: - ignore_remote: spec:output_only + spec: output_only effective_enable_pg_native_login: - ignore_remote: spec:output_only + spec: output_only effective_enable_readable_secondaries: - ignore_remote: spec:output_only + spec: output_only effective_node_count: - ignore_remote: spec:output_only + spec: output_only effective_retention_window_in_days: - ignore_remote: spec:output_only + spec: output_only effective_stopped: - ignore_remote: spec:output_only + spec: output_only effective_usage_policy_id: - ignore_remote: spec:output_only + spec: output_only enable_pg_native_login: - ignore_remote: spec:input_only + spec: input_only enable_readable_secondaries: - ignore_remote: spec:input_only + spec: input_only node_count: - ignore_remote: spec:input_only + spec: input_only parent_instance_ref: - action: immutable - reason: spec:immutable + spec: immutable parent_instance_ref.effective_lsn: - ignore_remote: spec:output_only + spec: output_only parent_instance_ref.lsn: - ignore_remote: spec:input_only + spec: input_only parent_instance_ref.uid: - ignore_remote: spec:output_only + spec: output_only pg_version: - ignore_remote: spec:output_only + spec: output_only read_only_dns: - ignore_remote: spec:output_only + spec: output_only read_write_dns: - ignore_remote: spec:output_only + spec: output_only retention_window_in_days: - ignore_remote: spec:input_only + spec: input_only state: - ignore_remote: spec:output_only + spec: output_only stopped: - ignore_remote: spec:input_only + spec: input_only uid: - ignore_remote: spec:output_only + spec: output_only usage_policy_id: - ignore_remote: spec:input_only + spec: input_only diff --git a/bundle/direct/dresources/configs/experiments.generated.yml b/bundle/direct/dresources/configs/experiments.generated.yml index 4b8a2747f03..2a9bf99f1de 100644 --- a/bundle/direct/dresources/configs/experiments.generated.yml +++ b/bundle/direct/dresources/configs/experiments.generated.yml @@ -2,7 +2,6 @@ fields: trace_location: - action: immutable - reason: spec:immutable + spec: immutable trace_location.uc_trace_location.effective_table_prefix: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/external_locations.generated.yml b/bundle/direct/dresources/configs/external_locations.generated.yml index 53e45f30a55..99e8eafdf87 100644 --- a/bundle/direct/dresources/configs/external_locations.generated.yml +++ b/bundle/direct/dresources/configs/external_locations.generated.yml @@ -2,18 +2,18 @@ fields: effective_enable_file_events: - ignore_remote: spec:output_only + spec: output_only effective_file_event_queue: - ignore_remote: spec:output_only + spec: output_only file_event_queue.managed_aqs.managed_resource_id: - ignore_remote: spec:output_only + spec: output_only file_event_queue.managed_pubsub.managed_resource_id: - ignore_remote: spec:output_only + spec: output_only file_event_queue.managed_sqs.managed_resource_id: - ignore_remote: spec:output_only + spec: output_only file_event_queue.provided_aqs.managed_resource_id: - ignore_remote: spec:output_only + spec: output_only file_event_queue.provided_pubsub.managed_resource_id: - ignore_remote: spec:output_only + spec: output_only file_event_queue.provided_sqs.managed_resource_id: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/genie_spaces.generated.yml b/bundle/direct/dresources/configs/genie_spaces.generated.yml index 5dd3a25326e..606843be78c 100644 --- a/bundle/direct/dresources/configs/genie_spaces.generated.yml +++ b/bundle/direct/dresources/configs/genie_spaces.generated.yml @@ -2,4 +2,4 @@ fields: etag: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/mcp_services.generated.yml b/bundle/direct/dresources/configs/mcp_services.generated.yml index 319a35aa190..178b3c04da3 100644 --- a/bundle/direct/dresources/configs/mcp_services.generated.yml +++ b/bundle/direct/dresources/configs/mcp_services.generated.yml @@ -2,6 +2,6 @@ fields: config.source_connection.is_deleted: - ignore_remote: spec:output_only + spec: output_only config.source_connection.options: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/model_provider_services.generated.yml b/bundle/direct/dresources/configs/model_provider_services.generated.yml index 293f0d82591..a8a833a7bb3 100644 --- a/bundle/direct/dresources/configs/model_provider_services.generated.yml +++ b/bundle/direct/dresources/configs/model_provider_services.generated.yml @@ -2,29 +2,28 @@ fields: config.amazon_bedrock.direct.aws_access_key.secret_access_key.plaintext: - ignore_remote: spec:input_only + spec: input_only config.anthropic.direct.api_key.plaintext: - ignore_remote: spec:input_only + spec: input_only config.azure_openai.direct.api_key.plaintext: - ignore_remote: spec:input_only + spec: input_only config.azure_openai.direct.entra_service_principal.client_secret.plaintext: - ignore_remote: spec:input_only + spec: input_only config.custom.direct.api_key.plaintext: - ignore_remote: spec:input_only + spec: input_only config.custom.direct.header_auth.api_key_value.plaintext: - ignore_remote: spec:input_only + spec: input_only config.gemini_enterprise.direct.api_key.plaintext: - ignore_remote: spec:input_only + spec: input_only config.inference_table.is_deleted: - ignore_remote: spec:output_only + spec: output_only config.inference_table.table: - ignore_remote: spec:output_only + spec: output_only config.microsoft_foundry.direct.api_key.plaintext: - ignore_remote: spec:input_only + spec: input_only config.microsoft_foundry.direct.entra_service_principal.client_secret.plaintext: - ignore_remote: spec:input_only + spec: input_only config.openai.direct.api_key.plaintext: - ignore_remote: spec:input_only + spec: input_only config.provider_type: - action: immutable - reason: spec:immutable + spec: immutable diff --git a/bundle/direct/dresources/configs/model_services.generated.yml b/bundle/direct/dresources/configs/model_services.generated.yml index 9895ec41366..195e160424e 100644 --- a/bundle/direct/dresources/configs/model_services.generated.yml +++ b/bundle/direct/dresources/configs/model_services.generated.yml @@ -2,14 +2,14 @@ fields: config.inference_table.is_deleted: - ignore_remote: spec:output_only + spec: output_only config.inference_table.table: - ignore_remote: spec:output_only + spec: output_only config.routing.destinations[*].is_deleted: - ignore_remote: spec:output_only + spec: output_only config.routing.destinations[*].provisioned_throughput_config.model: - ignore_remote: spec:output_only + spec: output_only config.routing.fallback.destinations[*].is_deleted: - ignore_remote: spec:output_only + spec: output_only config.routing.fallback.destinations[*].provisioned_throughput_config.model: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml b/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml index cbed99d1e51..e17195d3562 100644 --- a/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml +++ b/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml @@ -2,32 +2,32 @@ fields: config.served_entities[*].burst_scaling_enabled: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.ai21labs_config.ai21labs_api_key_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.amazon_bedrock_config.aws_access_key_id_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.amazon_bedrock_config.aws_secret_access_key_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.anthropic_config.anthropic_api_key_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.cohere_config.cohere_api_key_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.custom_provider_config.api_key_auth.value_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.custom_provider_config.bearer_token_auth.token_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.databricks_model_serving_config.databricks_api_token_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.google_cloud_vertex_ai_config.private_key_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.openai_config.microsoft_entra_client_secret_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.openai_config.openai_api_key_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_entities[*].external_model.palm_config.palm_api_key_plaintext: - ignore_remote: spec:input_only + spec: input_only config.served_models[*].burst_scaling_enabled: - ignore_remote: spec:input_only + spec: input_only telemetry_config.inference_table_config.name: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/pipelines.generated.yml b/bundle/direct/dresources/configs/pipelines.generated.yml index 2c4a5f6d98b..196f4e54e54 100644 --- a/bundle/direct/dresources/configs/pipelines.generated.yml +++ b/bundle/direct/dresources/configs/pipelines.generated.yml @@ -2,4 +2,4 @@ fields: ingestion_definition.source_type: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/postgres_branches.generated.yml b/bundle/direct/dresources/configs/postgres_branches.generated.yml index 04244acd604..00b19c5afbf 100644 --- a/bundle/direct/dresources/configs/postgres_branches.generated.yml +++ b/bundle/direct/dresources/configs/postgres_branches.generated.yml @@ -2,26 +2,18 @@ fields: expire_time: - ignore_remote: spec:input_only + spec: input_only is_protected: - ignore_remote: spec:input_only + spec: input_only no_expiry: - ignore_remote: spec:input_only + spec: input_only source_branch: - action: immutable - reason: spec:immutable - ignore_remote: spec:input_only + spec: [immutable, input_only] source_branch_lsn: - action: immutable - reason: spec:immutable - ignore_remote: spec:input_only + spec: [immutable, input_only] source_branch_time: - action: immutable - reason: spec:immutable - ignore_remote: spec:input_only + spec: [immutable, input_only] source_snapshot: - action: immutable - reason: spec:immutable - ignore_remote: spec:input_only + spec: [immutable, input_only] ttl: - ignore_remote: spec:input_only + spec: input_only diff --git a/bundle/direct/dresources/configs/postgres_catalogs.generated.yml b/bundle/direct/dresources/configs/postgres_catalogs.generated.yml index 5ad0248aaeb..2be112d055b 100644 --- a/bundle/direct/dresources/configs/postgres_catalogs.generated.yml +++ b/bundle/direct/dresources/configs/postgres_catalogs.generated.yml @@ -2,10 +2,8 @@ fields: branch: - ignore_remote: spec:input_only + spec: input_only create_database_if_missing: - ignore_remote: spec:input_only + spec: input_only postgres_database: - action: immutable - reason: spec:immutable - ignore_remote: spec:input_only + spec: [immutable, input_only] diff --git a/bundle/direct/dresources/configs/postgres_databases.generated.yml b/bundle/direct/dresources/configs/postgres_databases.generated.yml index 6735924f29f..c6362094e4e 100644 --- a/bundle/direct/dresources/configs/postgres_databases.generated.yml +++ b/bundle/direct/dresources/configs/postgres_databases.generated.yml @@ -2,6 +2,6 @@ fields: postgres_database: - ignore_remote: spec:input_only + spec: input_only role: - ignore_remote: spec:input_only + spec: input_only diff --git a/bundle/direct/dresources/configs/postgres_endpoints.generated.yml b/bundle/direct/dresources/configs/postgres_endpoints.generated.yml index 69bff94afe8..fb92a51a924 100644 --- a/bundle/direct/dresources/configs/postgres_endpoints.generated.yml +++ b/bundle/direct/dresources/configs/postgres_endpoints.generated.yml @@ -2,20 +2,18 @@ fields: autoscaling_limit_max_cu: - ignore_remote: spec:input_only + spec: input_only autoscaling_limit_min_cu: - ignore_remote: spec:input_only + spec: input_only disabled: - ignore_remote: spec:input_only + spec: input_only endpoint_type: - action: immutable - reason: spec:immutable - ignore_remote: spec:input_only + spec: [immutable, input_only] group: - ignore_remote: spec:input_only + spec: input_only no_suspension: - ignore_remote: spec:input_only + spec: input_only settings: - ignore_remote: spec:input_only + spec: input_only suspend_timeout_duration: - ignore_remote: spec:input_only + spec: input_only diff --git a/bundle/direct/dresources/configs/postgres_projects.generated.yml b/bundle/direct/dresources/configs/postgres_projects.generated.yml index 2538762e74f..bd47cc76d41 100644 --- a/bundle/direct/dresources/configs/postgres_projects.generated.yml +++ b/bundle/direct/dresources/configs/postgres_projects.generated.yml @@ -2,24 +2,22 @@ fields: budget_policy_id: - ignore_remote: spec:input_only + spec: input_only custom_tags: - ignore_remote: spec:input_only + spec: input_only custom_tags[*].key: - ignore_remote: spec:input_only + spec: input_only custom_tags[*].value: - ignore_remote: spec:input_only + spec: input_only default_branch: - ignore_remote: spec:input_only + spec: input_only default_endpoint_settings: - ignore_remote: spec:input_only + spec: input_only display_name: - ignore_remote: spec:input_only + spec: input_only enable_pg_native_login: - ignore_remote: spec:input_only + spec: input_only history_retention_duration: - ignore_remote: spec:input_only + spec: input_only pg_version: - action: immutable - reason: spec:immutable - ignore_remote: spec:input_only + spec: [immutable, input_only] diff --git a/bundle/direct/dresources/configs/postgres_roles.generated.yml b/bundle/direct/dresources/configs/postgres_roles.generated.yml index da3d2acd5b9..9f89810320e 100644 --- a/bundle/direct/dresources/configs/postgres_roles.generated.yml +++ b/bundle/direct/dresources/configs/postgres_roles.generated.yml @@ -2,12 +2,12 @@ fields: attributes: - ignore_remote: spec:input_only + spec: input_only auth_method: - ignore_remote: spec:input_only + spec: input_only identity_type: - ignore_remote: spec:input_only + spec: input_only membership_roles: - ignore_remote: spec:input_only + spec: input_only postgres_role: - ignore_remote: spec:input_only + spec: input_only diff --git a/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml b/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml index c9a40227d43..138f47acb4c 100644 --- a/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml +++ b/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml @@ -2,40 +2,40 @@ fields: accelerated_sync: - ignore_remote: spec:input_only + spec: input_only branch: - ignore_remote: spec:input_only + spec: input_only create_database_objects_if_missing: - ignore_remote: spec:input_only + spec: input_only existing_pipeline_id: - ignore_remote: spec:input_only + spec: input_only extra_columns: - ignore_remote: spec:input_only + spec: input_only extra_columns[*].column_name: - ignore_remote: spec:input_only + spec: input_only extra_columns[*].column_type: - ignore_remote: spec:input_only + spec: input_only extra_columns[*].compute: - ignore_remote: spec:input_only + spec: input_only extra_columns[*].maintenance: - ignore_remote: spec:input_only + spec: input_only new_pipeline_spec: - ignore_remote: spec:input_only + spec: input_only postgres_database: - ignore_remote: spec:input_only + spec: input_only primary_key_columns: - ignore_remote: spec:input_only + spec: input_only scheduling_policy: - ignore_remote: spec:input_only + spec: input_only source_table_full_name: - ignore_remote: spec:input_only + spec: input_only timeseries_key: - ignore_remote: spec:input_only + spec: input_only type_overrides: - ignore_remote: spec:input_only + spec: input_only type_overrides[*].column_name: - ignore_remote: spec:input_only + spec: input_only type_overrides[*].pg_type: - ignore_remote: spec:input_only + spec: input_only type_overrides[*].size: - ignore_remote: spec:input_only + spec: input_only diff --git a/bundle/direct/dresources/configs/registered_models.generated.yml b/bundle/direct/dresources/configs/registered_models.generated.yml index 86a25d73ad8..95b314b4056 100644 --- a/bundle/direct/dresources/configs/registered_models.generated.yml +++ b/bundle/direct/dresources/configs/registered_models.generated.yml @@ -2,4 +2,4 @@ fields: browse_only: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/configs/secrets.generated.yml b/bundle/direct/dresources/configs/secrets.generated.yml index c73fa6c81c4..ab3b54bc77e 100644 --- a/bundle/direct/dresources/configs/secrets.generated.yml +++ b/bundle/direct/dresources/configs/secrets.generated.yml @@ -2,31 +2,28 @@ fields: catalog_name: - action: immutable - reason: spec:immutable + spec: immutable create_time: - ignore_remote: spec:output_only + spec: output_only created_by: - ignore_remote: spec:output_only + spec: output_only effective_owner: - ignore_remote: spec:output_only + spec: output_only effective_value: - ignore_remote: spec:output_only + spec: output_only full_name: - ignore_remote: spec:output_only + spec: output_only metastore_id: - ignore_remote: spec:output_only + spec: output_only name: - action: immutable - reason: spec:immutable + spec: immutable owner: - ignore_remote: spec:input_only + spec: input_only schema_name: - action: immutable - reason: spec:immutable + spec: immutable update_time: - ignore_remote: spec:output_only + spec: output_only updated_by: - ignore_remote: spec:output_only + spec: output_only value: - ignore_remote: spec:input_only + spec: input_only diff --git a/bundle/direct/dresources/configs/synced_database_tables.generated.yml b/bundle/direct/dresources/configs/synced_database_tables.generated.yml index 95743e431c7..00e1894669a 100644 --- a/bundle/direct/dresources/configs/synced_database_tables.generated.yml +++ b/bundle/direct/dresources/configs/synced_database_tables.generated.yml @@ -2,22 +2,22 @@ fields: data_synchronization_status: - ignore_remote: spec:output_only + spec: output_only database_instance_name: - ignore_remote: spec:input_only + spec: input_only effective_database_instance_name: - ignore_remote: spec:output_only + spec: output_only effective_logical_database_name: - ignore_remote: spec:output_only + spec: output_only logical_database_name: - ignore_remote: spec:input_only + spec: input_only spec.accelerated_sync: - ignore_remote: spec:input_only + spec: input_only spec.create_database_objects_if_missing: - ignore_remote: spec:input_only + spec: input_only spec.existing_pipeline_id: - ignore_remote: spec:input_only + spec: input_only spec.new_pipeline_spec: - ignore_remote: spec:input_only + spec: input_only unity_catalog_provisioning_state: - ignore_remote: spec:output_only + spec: output_only diff --git a/bundle/direct/dresources/field_policy.go b/bundle/direct/dresources/field_policy.go index 7b33591003c..c0f76b310de 100644 --- a/bundle/direct/dresources/field_policy.go +++ b/bundle/direct/dresources/field_policy.go @@ -37,6 +37,13 @@ type FieldPolicyConfig struct { // backend defaults, storage, redaction, reference resolution). The modifiers never // conflict with the Action, so a field cannot be handed contradictory behaviours. type FieldPolicy struct { + // Spec lists the OpenAPI-declared behaviours for a field (one or a list of + // immutable / input_only / output_only). Each implies its own mechanism and a + // "spec:" reason: immutable recreates, input_only/output_only skip + // remote drift. This is what the generated configs emit; hand-written configs use + // Action / IgnoreRemote instead (their reasons are manual, not spec-derived). + Spec specBehaviours `yaml:"spec,omitempty"` + // Action is the local-change decision. Empty = a normal in-place update. Action action `yaml:"action,omitempty"` @@ -106,6 +113,39 @@ func (a *action) UnmarshalYAML(unmarshal func(any) error) error { return nil } +// OpenAPI behaviours a spec entry may carry, each mapping to a mechanism and a +// "spec:" reason in lowerSpec. +const ( + specImmutable = "immutable" + specInputOnly = "input_only" + specOutputOnly = "output_only" +) + +var validSpecBehaviours = map[string]bool{specImmutable: true, specInputOnly: true, specOutputOnly: true} + +// specBehaviours accepts either a single behaviour ("spec: output_only") or a list +// ("spec: [immutable, input_only]"), and rejects unknown behaviours at parse time. +type specBehaviours []string + +func (s *specBehaviours) UnmarshalYAML(unmarshal func(any) error) error { + var one string + if err := unmarshal(&one); err == nil { + *s = specBehaviours{one} + } else { + var many []string + if err := unmarshal(&many); err != nil { + return err + } + *s = many + } + for _, b := range *s { + if !validSpecBehaviours[b] { + return fmt.Errorf("unknown spec behaviour %q", b) + } + } + return nil +} + const ( compareTrimSlash = "trim_slash" @@ -169,10 +209,30 @@ func lowerAction(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, return nil } +// lowerSpec appends the flat rules for a field's OpenAPI-declared behaviours, each with +// a "spec:" reason: immutable recreates, input_only/output_only skip remote drift. +func lowerSpec(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, spec specBehaviours) error { + for _, b := range spec { + switch b { + case specImmutable: + out.RecreateOnChanges = append(out.RecreateOnChanges, FieldRule{Field: pattern, Reason: "spec:" + specImmutable}) + case specInputOnly, specOutputOnly: + out.IgnoreRemoteChanges = append(out.IgnoreRemoteChanges, FieldRule{Field: pattern, Reason: "spec:" + b}) + default: + // Unreachable via YAML (UnmarshalYAML validates against validSpecBehaviours). + return fmt.Errorf("unhandled spec behaviour %q", b) + } + } + return nil +} + func lowerField(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, field string, p FieldPolicy) error { if err := lowerAction(out, pattern, p.Action, p.Reason); err != nil { return err } + if err := lowerSpec(out, pattern, p.Spec); err != nil { + return err + } if p.IgnoreRemote != "" { out.IgnoreRemoteChanges = append(out.IgnoreRemoteChanges, FieldRule{Field: pattern, Reason: p.IgnoreRemote}) diff --git a/bundle/direct/dresources/field_policy_test.go b/bundle/direct/dresources/field_policy_test.go index 78fbe57d637..7323ef0e922 100644 --- a/bundle/direct/dresources/field_policy_test.go +++ b/bundle/direct/dresources/field_policy_test.go @@ -36,6 +36,29 @@ func TestFieldPolicyEveryActionLowers(t *testing.T) { } } +// TestFieldPolicyRejectsUnknownSpec proves the spec behaviour set is enforced by the parser. +func TestFieldPolicyRejectsUnknownSpec(t *testing.T) { + var fpc FieldPolicyConfig + err := yaml.Unmarshal([]byte("fields:\n name: { spec: writeonce }\n"), &fpc) + require.Error(t, err) + assert.Contains(t, err.Error(), `unknown spec behaviour "writeonce"`) +} + +// TestFieldPolicySpecLowers pins each OpenAPI behaviour to its mechanism and spec: reason. +func TestFieldPolicySpecLowers(t *testing.T) { + c, err := FieldPolicyConfig{Fields: map[string]FieldPolicy{ + "a": {Spec: specBehaviours{specImmutable, specInputOnly}}, + "b": {Spec: specBehaviours{specOutputOnly}}, + }}.Lower() + require.NoError(t, err) + assert.Equal(t, "spec:immutable", c.RecreateOnChanges[0].Reason) + remote := []string{} + for _, r := range c.IgnoreRemoteChanges { + remote = append(remote, r.Field.String()+"|"+r.Reason) + } + assert.ElementsMatch(t, []string{"a|spec:input_only", "b|spec:output_only"}, remote) +} + // TestFieldPolicyLowerVolumes pins the lowering of a representative resource that // exercises id, id_renameable, immutable, and the trim_slash comparison modifier. func TestFieldPolicyLowerVolumes(t *testing.T) { diff --git a/bundle/direct/tools/generate_resources.py b/bundle/direct/tools/generate_resources.py index 5c258dc67ea..c744dbca5b4 100644 --- a/bundle/direct/tools/generate_resources.py +++ b/bundle/direct/tools/generate_resources.py @@ -154,9 +154,10 @@ def quote_key(field): def generate(behaviors): """Render one resource's field behaviors in the field-keyed format, or "" if none. - A field maps to a single action (immutable) plus an ignore_remote modifier whose value - is the reason; the two are filtered for prefixes independently, matching the flat - recreate_on_changes / ignore_remote_changes lists they replace. + Each field lists its OpenAPI behaviours under `spec` (immutable / input_only / + output_only); the loader maps each to its mechanism and a spec: reason. + immutable and the remote-ignored behaviours are filtered for prefixes independently, + matching the flat recreate_on_changes / ignore_remote_changes lists they replace. """ ignore_remote, recreate = [], [] for field, fb in sorted(behaviors.items()): @@ -173,17 +174,18 @@ def generate(behaviors): if not ignore_remote and not recreate: return "" - entries = {} # field -> ordered list of (key, value) + spec = {} # field -> ordered list of behaviours for field, _ in recreate: - entries.setdefault(field, []).extend([("action", "immutable"), ("reason", "spec:immutable")]) + spec.setdefault(field, []).append("immutable") for field, behavior in ignore_remote: - entries.setdefault(field, []).append(("ignore_remote", f"spec:{behavior.lower()}")) + spec.setdefault(field, []).append(behavior.lower()) lines = [HEADER, "", "fields:"] - for field in sorted(entries): + for field in sorted(spec): + behaviours = spec[field] + value = behaviours[0] if len(behaviours) == 1 else "[" + ", ".join(behaviours) + "]" lines.append(f" {quote_key(field)}:") - for key, value in entries[field]: - lines.append(f" {key}: {value}") + lines.append(f" spec: {value}") return "\n".join(lines) + "\n" From 35c2109915f899988174f1a3273eaec76b93a8e5 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 30 Sep 2026 11:59:18 +0200 Subject: [PATCH 7/8] dresources: use nil slice in spec lowering test (gocritic) Co-authored-by: Isaac --- bundle/direct/dresources/field_policy_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle/direct/dresources/field_policy_test.go b/bundle/direct/dresources/field_policy_test.go index 7323ef0e922..ede8b8bf8b2 100644 --- a/bundle/direct/dresources/field_policy_test.go +++ b/bundle/direct/dresources/field_policy_test.go @@ -52,7 +52,7 @@ func TestFieldPolicySpecLowers(t *testing.T) { }}.Lower() require.NoError(t, err) assert.Equal(t, "spec:immutable", c.RecreateOnChanges[0].Reason) - remote := []string{} + var remote []string for _, r := range c.IgnoreRemoteChanges { remote = append(remote, r.Field.String()+"|"+r.Reason) } From 4bdb473313fc833fe8205a4c335c43ee23dd0e5a Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 30 Sep 2026 13:44:24 +0200 Subject: [PATCH 8/8] dresources: token-list field format with immutable_output/mutable_output Author each field as a list of behaviour tokens (map form only when a behaviour carries a value). Output-only fields split into mutable_output (skip drift) and immutable_output (backend-assigned, never changes -> stable_output, so a ${resources.X.name} reference resolves from the remote cache during an in-place update). An output field can never recreate, so IMMUTABLE+OUTPUT_ONLY folds into the single immutable_output token instead of recreate+ignore_remote. Generated configs derive their reasons (spec: prefix) from the file, so a field is just its token list. Lowers to the identical ResourceLifecycleConfig as before. Co-authored-by: Isaac --- bundle/direct/dresources/config.go | 5 +- .../dresources/configs/alerts.generated.yml | 24 +- .../dresources/configs/apps.generated.yml | 75 ++--- bundle/direct/dresources/configs/apps.yml | 24 +- bundle/direct/dresources/configs/catalogs.yml | 20 +- .../dresources/configs/cluster_policies.yml | 3 +- bundle/direct/dresources/configs/clusters.yml | 36 +-- .../configs/dashboards.generated.yml | 18 +- .../direct/dresources/configs/dashboards.yml | 6 +- .../configs/database_catalogs.generated.yml | 6 +- .../dresources/configs/database_catalogs.yml | 3 +- .../configs/database_instances.generated.yml | 108 +++---- .../dresources/configs/database_instances.yml | 3 +- .../configs/experiments.generated.yml | 6 +- .../direct/dresources/configs/experiments.yml | 4 +- .../configs/external_locations.generated.yml | 24 +- .../dresources/configs/external_locations.yml | 12 +- .../configs/genie_spaces.generated.yml | 3 +- .../dresources/configs/instance_pools.yml | 20 +- bundle/direct/dresources/configs/jobs.yml | 118 ++------ .../configs/mcp_services.generated.yml | 6 +- .../dresources/configs/mcp_services.yml | 6 +- .../model_provider_services.generated.yml | 39 +-- .../configs/model_provider_services.yml | 6 +- .../configs/model_services.generated.yml | 18 +- .../dresources/configs/model_services.yml | 6 +- .../model_serving_endpoints.generated.yml | 45 +-- .../configs/model_serving_endpoints.yml | 51 +--- .../configs/pipelines.generated.yml | 3 +- .../direct/dresources/configs/pipelines.yml | 61 +--- .../configs/postgres_branches.generated.yml | 24 +- .../dresources/configs/postgres_branches.yml | 10 +- .../configs/postgres_catalogs.generated.yml | 9 +- .../dresources/configs/postgres_catalogs.yml | 3 +- .../configs/postgres_databases.generated.yml | 6 +- .../dresources/configs/postgres_databases.yml | 10 +- .../configs/postgres_endpoints.generated.yml | 24 +- .../dresources/configs/postgres_endpoints.yml | 10 +- .../configs/postgres_projects.generated.yml | 30 +- .../dresources/configs/postgres_projects.yml | 8 +- .../configs/postgres_roles.generated.yml | 15 +- .../dresources/configs/postgres_roles.yml | 24 +- .../configs/postgres_snapshot_schedules.yml | 8 +- .../postgres_synced_tables.generated.yml | 57 ++-- .../configs/postgres_synced_tables.yml | 3 +- .../dresources/configs/quality_monitors.yml | 6 +- .../configs/registered_models.generated.yml | 3 +- .../dresources/configs/registered_models.yml | 39 +-- bundle/direct/dresources/configs/schemas.yml | 16 +- .../configs/secret_scopes.permissions.yml | 3 +- .../dresources/configs/secret_scopes.yml | 12 +- .../dresources/configs/secrets.generated.yml | 39 +-- bundle/direct/dresources/configs/secrets.yml | 6 +- .../dresources/configs/sql_warehouses.yml | 19 +- .../synced_database_tables.generated.yml | 30 +- .../configs/synced_database_tables.yml | 3 +- .../configs/vector_search_endpoints.yml | 8 +- .../configs/vector_search_indexes.yml | 30 +- bundle/direct/dresources/configs/volumes.yml | 18 +- bundle/direct/dresources/field_policy.go | 284 ++++++++++-------- bundle/direct/dresources/field_policy_test.go | 90 +++--- bundle/direct/tools/generate_resources.py | 43 +-- 62 files changed, 595 insertions(+), 1054 deletions(-) diff --git a/bundle/direct/dresources/config.go b/bundle/direct/dresources/config.go index 57372db9fe1..856c7c783d2 100644 --- a/bundle/direct/dresources/config.go +++ b/bundle/direct/dresources/config.go @@ -177,8 +177,9 @@ var loadConfigs = sync.OnceValues(func() (*Config, *Config) { for _, name := range names { dst, resourceType := handWritten, strings.TrimSuffix(path.Base(name), ymlSuffix) + isGenerated := false if trimmed, ok := strings.CutSuffix(resourceType, generatedSuffix); ok { - dst, resourceType = generated, trimmed + dst, resourceType, isGenerated = generated, trimmed, true } data, err := configFS.ReadFile(name) @@ -190,7 +191,7 @@ var loadConfigs = sync.OnceValues(func() (*Config, *Config) { if err := yaml.Unmarshal(data, &fpc); err != nil { panic(fmt.Errorf("%s: %w", name, err)) } - rc, err := fpc.Lower() + rc, err := fpc.Lower(isGenerated) if err != nil { panic(fmt.Errorf("%s: %w", name, err)) } diff --git a/bundle/direct/dresources/configs/alerts.generated.yml b/bundle/direct/dresources/configs/alerts.generated.yml index cc9d96e22da..6ae944037f6 100644 --- a/bundle/direct/dresources/configs/alerts.generated.yml +++ b/bundle/direct/dresources/configs/alerts.generated.yml @@ -1,19 +1,11 @@ # Generated, do not edit. fields: - create_time: - spec: output_only - effective_run_as: - spec: output_only - evaluation.last_evaluated_at: - spec: output_only - evaluation.state: - spec: output_only - id: - spec: output_only - lifecycle_state: - spec: output_only - owner_user_name: - spec: output_only - update_time: - spec: output_only + create_time: [mutable_output] + effective_run_as: [mutable_output] + evaluation.last_evaluated_at: [mutable_output] + evaluation.state: [mutable_output] + id: [mutable_output] + lifecycle_state: [mutable_output] + owner_user_name: [mutable_output] + update_time: [mutable_output] diff --git a/bundle/direct/dresources/configs/apps.generated.yml b/bundle/direct/dresources/configs/apps.generated.yml index 39ffbca1c6b..c0545ae4b59 100644 --- a/bundle/direct/dresources/configs/apps.generated.yml +++ b/bundle/direct/dresources/configs/apps.generated.yml @@ -1,53 +1,28 @@ # Generated, do not edit. fields: - active_deployment: - spec: output_only - app_status: - spec: output_only - compute_status: - spec: output_only - create_time: - spec: output_only - creator: - spec: output_only - default_git_source: - spec: output_only - default_source_code_path: - spec: output_only - effective_budget_policy_id: - spec: output_only - effective_usage_policy_id: - spec: output_only - effective_user_api_scopes: - spec: output_only - git_repository.caller_credential_id: - spec: input_only - git_source: - spec: input_only - id: - spec: output_only - oauth2_app_client_id: - spec: output_only - oauth2_app_integration_id: - spec: output_only - pending_deployment: - spec: output_only - resources[*].uc_securable.securable_kind: - spec: output_only - service_principal_client_id: - spec: output_only - service_principal_id: - spec: output_only - service_principal_name: - spec: output_only - source_code_path: - spec: input_only - thumbnail_url: - spec: output_only - update_time: - spec: output_only - updater: - spec: output_only - url: - spec: output_only + active_deployment: [mutable_output] + app_status: [mutable_output] + compute_status: [mutable_output] + create_time: [mutable_output] + creator: [mutable_output] + default_git_source: [mutable_output] + default_source_code_path: [mutable_output] + effective_budget_policy_id: [mutable_output] + effective_usage_policy_id: [mutable_output] + effective_user_api_scopes: [mutable_output] + git_repository.caller_credential_id: [input_only] + git_source: [input_only] + id: [mutable_output] + oauth2_app_client_id: [mutable_output] + oauth2_app_integration_id: [mutable_output] + pending_deployment: [mutable_output] + resources[*].uc_securable.securable_kind: [mutable_output] + service_principal_client_id: [mutable_output] + service_principal_id: [mutable_output] + service_principal_name: [mutable_output] + source_code_path: [input_only] + thumbnail_url: [mutable_output] + update_time: [mutable_output] + updater: [mutable_output] + url: [mutable_output] diff --git a/bundle/direct/dresources/configs/apps.yml b/bundle/direct/dresources/configs/apps.yml index 935a992d3c0..d5014e67577 100644 --- a/bundle/direct/dresources/configs/apps.yml +++ b/bundle/direct/dresources/configs/apps.yml @@ -1,22 +1,8 @@ fields: - name: - action: id - # Backend sets it "MEDIUM" when not specified in the config - compute_size: - backend_default: true - # 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. - forward_user_access_token: - backend_default: true - # 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). - lifecycle: - backend_default: true - lifecycle.started: - backend_default: true + name: [id] + compute_size: [backend_default] + forward_user_access_token: [backend_default] + lifecycle: [backend_default] + lifecycle.started: [backend_default] space: ignore_remote: managed diff --git a/bundle/direct/dresources/configs/catalogs.yml b/bundle/direct/dresources/configs/catalogs.yml index 1fd8b7eb407..78aa49ba996 100644 --- a/bundle/direct/dresources/configs/catalogs.yml +++ b/bundle/direct/dresources/configs/catalogs.yml @@ -2,17 +2,9 @@ fields: # 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 - storage_root: - action: immutable - compare: trim_slash - connection_name: - action: immutable - provider_name: - action: immutable - share_name: - action: immutable - name: - action: id_renameable - # UC auto-populates unity.catalog.managed..defaults.* keys after create. - properties['unity.catalog.managed.*.defaults.*']: - backend_default: true + storage_root: [immutable, trim_slash] + connection_name: [immutable] + provider_name: [immutable] + share_name: [immutable] + name: [renameable_id] + properties['unity.catalog.managed.*.defaults.*']: [backend_default] diff --git a/bundle/direct/dresources/configs/cluster_policies.yml b/bundle/direct/dresources/configs/cluster_policies.yml index 26bcfb4d551..3c4b0ce9618 100644 --- a/bundle/direct/dresources/configs/cluster_policies.yml +++ b/bundle/direct/dresources/configs/cluster_policies.yml @@ -3,5 +3,4 @@ fields: # 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. - definition: - backend_default: true + definition: [backend_default] diff --git a/bundle/direct/dresources/configs/clusters.yml b/bundle/direct/dresources/configs/clusters.yml index 309a9836b18..a2093bba492 100644 --- a/bundle/direct/dresources/configs/clusters.yml +++ b/bundle/direct/dresources/configs/clusters.yml @@ -22,31 +22,11 @@ fields: # 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). - lifecycle: - backend_default: true - lifecycle.started: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L331 - # s.SchemaPath("enable_elastic_disk").SetComputed() - enable_elastic_disk: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L332 - # s.SchemaPath("enable_local_disk_encryption").SetComputed() - enable_local_disk_encryption: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L333 - # s.SchemaPath("node_type_id").SetComputed() - node_type_id: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L334 - # s.SchemaPath("driver_node_type_id").SetComputed() - driver_node_type_id: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L335 - # s.SchemaPath("driver_instance_pool_id").SetComputed() - driver_instance_pool_id: - backend_default: true - # Terraform currently does not do this, but it is a field with backend default. - # See https://github.com/databricks/cli/issues/4418 - single_user_name: - backend_default: true + lifecycle: [backend_default] + lifecycle.started: [backend_default] + enable_elastic_disk: [backend_default] + enable_local_disk_encryption: [backend_default] + node_type_id: [backend_default] + driver_node_type_id: [backend_default] + driver_instance_pool_id: [backend_default] + single_user_name: [backend_default] diff --git a/bundle/direct/dresources/configs/dashboards.generated.yml b/bundle/direct/dresources/configs/dashboards.generated.yml index 8437a83b8b4..d096a326e6d 100644 --- a/bundle/direct/dresources/configs/dashboards.generated.yml +++ b/bundle/direct/dresources/configs/dashboards.generated.yml @@ -1,15 +1,9 @@ # Generated, do not edit. fields: - create_time: - spec: output_only - dashboard_id: - spec: output_only - lifecycle_state: - spec: output_only - parent_path: - spec: immutable - path: - spec: output_only - update_time: - spec: output_only + create_time: [mutable_output] + dashboard_id: [mutable_output] + lifecycle_state: [mutable_output] + parent_path: [immutable] + path: [mutable_output] + update_time: [mutable_output] diff --git a/bundle/direct/dresources/configs/dashboards.yml b/bundle/direct/dresources/configs/dashboards.yml index 9e222666284..a6701843bc6 100644 --- a/bundle/direct/dresources/configs/dashboards.yml +++ b/bundle/direct/dresources/configs/dashboards.yml @@ -8,7 +8,5 @@ fields: hashed: true # "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). - dataset_catalog: - ignore_remote: input_only - dataset_schema: - ignore_remote: input_only + dataset_catalog: [input_only] + dataset_schema: [input_only] diff --git a/bundle/direct/dresources/configs/database_catalogs.generated.yml b/bundle/direct/dresources/configs/database_catalogs.generated.yml index b84ea7f63f1..32f88ddf384 100644 --- a/bundle/direct/dresources/configs/database_catalogs.generated.yml +++ b/bundle/direct/dresources/configs/database_catalogs.generated.yml @@ -1,7 +1,5 @@ # Generated, do not edit. fields: - create_database_if_not_exists: - spec: input_only - uid: - spec: output_only + create_database_if_not_exists: [input_only] + uid: [mutable_output] diff --git a/bundle/direct/dresources/configs/database_catalogs.yml b/bundle/direct/dresources/configs/database_catalogs.yml index 61e0154feea..0f36fcdd6a9 100644 --- a/bundle/direct/dresources/configs/database_catalogs.yml +++ b/bundle/direct/dresources/configs/database_catalogs.yml @@ -6,5 +6,4 @@ # output-only) so no-op deploys stay idempotent. action: immutable fields: - name: - action: id + name: [id] diff --git a/bundle/direct/dresources/configs/database_instances.generated.yml b/bundle/direct/dresources/configs/database_instances.generated.yml index 1a45824b18a..d089d36eeec 100644 --- a/bundle/direct/dresources/configs/database_instances.generated.yml +++ b/bundle/direct/dresources/configs/database_instances.generated.yml @@ -1,75 +1,39 @@ # Generated, do not edit. fields: - child_instance_refs: - spec: output_only - child_instance_refs[*].branch_time: - spec: output_only - child_instance_refs[*].effective_lsn: - spec: output_only - child_instance_refs[*].lsn: - spec: output_only - child_instance_refs[*].name: - spec: output_only - child_instance_refs[*].uid: - spec: output_only - creation_time: - spec: output_only - creator: - spec: output_only - custom_tags: - spec: input_only - custom_tags[*].key: - spec: input_only - custom_tags[*].value: - spec: input_only - effective_capacity: - spec: output_only - effective_custom_tags: - spec: output_only - effective_custom_tags[*].key: - spec: output_only - effective_custom_tags[*].value: - spec: output_only - effective_enable_pg_native_login: - spec: output_only - effective_enable_readable_secondaries: - spec: output_only - effective_node_count: - spec: output_only - effective_retention_window_in_days: - spec: output_only - effective_stopped: - spec: output_only - effective_usage_policy_id: - spec: output_only - enable_pg_native_login: - spec: input_only - enable_readable_secondaries: - spec: input_only - node_count: - spec: input_only - parent_instance_ref: - spec: immutable - parent_instance_ref.effective_lsn: - spec: output_only - parent_instance_ref.lsn: - spec: input_only - parent_instance_ref.uid: - spec: output_only - pg_version: - spec: output_only - read_only_dns: - spec: output_only - read_write_dns: - spec: output_only - retention_window_in_days: - spec: input_only - state: - spec: output_only - stopped: - spec: input_only - uid: - spec: output_only - usage_policy_id: - spec: input_only + child_instance_refs: [mutable_output] + child_instance_refs[*].branch_time: [mutable_output] + child_instance_refs[*].effective_lsn: [mutable_output] + child_instance_refs[*].lsn: [mutable_output] + child_instance_refs[*].name: [mutable_output] + child_instance_refs[*].uid: [mutable_output] + creation_time: [mutable_output] + creator: [mutable_output] + custom_tags: [input_only] + custom_tags[*].key: [input_only] + custom_tags[*].value: [input_only] + effective_capacity: [mutable_output] + effective_custom_tags: [mutable_output] + effective_custom_tags[*].key: [mutable_output] + effective_custom_tags[*].value: [mutable_output] + effective_enable_pg_native_login: [mutable_output] + effective_enable_readable_secondaries: [mutable_output] + effective_node_count: [mutable_output] + effective_retention_window_in_days: [mutable_output] + effective_stopped: [mutable_output] + effective_usage_policy_id: [mutable_output] + enable_pg_native_login: [input_only] + enable_readable_secondaries: [input_only] + node_count: [input_only] + parent_instance_ref: [immutable] + parent_instance_ref.effective_lsn: [mutable_output] + parent_instance_ref.lsn: [input_only] + parent_instance_ref.uid: [mutable_output] + pg_version: [mutable_output] + read_only_dns: [mutable_output] + read_write_dns: [mutable_output] + retention_window_in_days: [input_only] + state: [mutable_output] + stopped: [input_only] + uid: [mutable_output] + usage_policy_id: [input_only] diff --git a/bundle/direct/dresources/configs/database_instances.yml b/bundle/direct/dresources/configs/database_instances.yml index 4450ad01430..e4e7e46000c 100644 --- a/bundle/direct/dresources/configs/database_instances.yml +++ b/bundle/direct/dresources/configs/database_instances.yml @@ -1,3 +1,2 @@ fields: - name: - action: id + name: [id] diff --git a/bundle/direct/dresources/configs/experiments.generated.yml b/bundle/direct/dresources/configs/experiments.generated.yml index 2a9bf99f1de..ad4f8b369ae 100644 --- a/bundle/direct/dresources/configs/experiments.generated.yml +++ b/bundle/direct/dresources/configs/experiments.generated.yml @@ -1,7 +1,5 @@ # Generated, do not edit. fields: - trace_location: - spec: immutable - trace_location.uc_trace_location.effective_table_prefix: - spec: output_only + trace_location: [immutable] + trace_location.uc_trace_location.effective_table_prefix: [mutable_output] diff --git a/bundle/direct/dresources/configs/experiments.yml b/bundle/direct/dresources/configs/experiments.yml index 21a4dada2f7..e9019066c32 100644 --- a/bundle/direct/dresources/configs/experiments.yml +++ b/bundle/direct/dresources/configs/experiments.yml @@ -1,8 +1,6 @@ # TF implementation: https://github.com/databricks/terraform-provider-databricks/blob/6c106e8e7052bb2726148d66309fd460ed444236/mlflow/resource_mlflow_experiment.go#L22 fields: - artifact_location: - action: immutable - # Tags updates are not supported by TF. This mirrors that behaviour. + artifact_location: [immutable] tags: ignore_remote: terraform_compat action: ignore_local diff --git a/bundle/direct/dresources/configs/external_locations.generated.yml b/bundle/direct/dresources/configs/external_locations.generated.yml index 99e8eafdf87..57f11d475bc 100644 --- a/bundle/direct/dresources/configs/external_locations.generated.yml +++ b/bundle/direct/dresources/configs/external_locations.generated.yml @@ -1,19 +1,11 @@ # Generated, do not edit. fields: - effective_enable_file_events: - spec: output_only - effective_file_event_queue: - spec: output_only - file_event_queue.managed_aqs.managed_resource_id: - spec: output_only - file_event_queue.managed_pubsub.managed_resource_id: - spec: output_only - file_event_queue.managed_sqs.managed_resource_id: - spec: output_only - file_event_queue.provided_aqs.managed_resource_id: - spec: output_only - file_event_queue.provided_pubsub.managed_resource_id: - spec: output_only - file_event_queue.provided_sqs.managed_resource_id: - spec: output_only + effective_enable_file_events: [mutable_output] + effective_file_event_queue: [mutable_output] + file_event_queue.managed_aqs.managed_resource_id: [mutable_output] + file_event_queue.managed_pubsub.managed_resource_id: [mutable_output] + file_event_queue.managed_sqs.managed_resource_id: [mutable_output] + file_event_queue.provided_aqs.managed_resource_id: [mutable_output] + file_event_queue.provided_pubsub.managed_resource_id: [mutable_output] + file_event_queue.provided_sqs.managed_resource_id: [mutable_output] diff --git a/bundle/direct/dresources/configs/external_locations.yml b/bundle/direct/dresources/configs/external_locations.yml index 37e41e70660..efaaf5ec294 100644 --- a/bundle/direct/dresources/configs/external_locations.yml +++ b/bundle/direct/dresources/configs/external_locations.yml @@ -1,9 +1,5 @@ fields: - credential_name: - action: immutable - encryption_details: - action: immutable - file_event_queue: - action: immutable - name: - action: id_renameable + credential_name: [immutable] + encryption_details: [immutable] + file_event_queue: [immutable] + name: [renameable_id] diff --git a/bundle/direct/dresources/configs/genie_spaces.generated.yml b/bundle/direct/dresources/configs/genie_spaces.generated.yml index 606843be78c..a5b12837cac 100644 --- a/bundle/direct/dresources/configs/genie_spaces.generated.yml +++ b/bundle/direct/dresources/configs/genie_spaces.generated.yml @@ -1,5 +1,4 @@ # Generated, do not edit. fields: - etag: - spec: output_only + etag: [mutable_output] diff --git a/bundle/direct/dresources/configs/instance_pools.yml b/bundle/direct/dresources/configs/instance_pools.yml index 234b8a20694..b45af5d5181 100644 --- a/bundle/direct/dresources/configs/instance_pools.yml +++ b/bundle/direct/dresources/configs/instance_pools.yml @@ -9,19 +9,11 @@ fields: gcp_attributes: ignore_remote: managed # force_new and not accepted by /instance-pools/edit. - disk_spec: - action: immutable - node_type_flexibility: - action: immutable - preloaded_spark_versions: - action: immutable - preloaded_docker_images: - action: immutable - # Defaults to true server-side. - enable_elastic_disk: - backend_default: true - # Backend applies a default of 60 minutes when the field is omitted. + disk_spec: [immutable] + node_type_flexibility: [immutable] + preloaded_spark_versions: [immutable] + preloaded_docker_images: [immutable] + enable_elastic_disk: [backend_default] idle_instance_autotermination_minutes: backend_values: [60] - max_capacity: - backend_default: true + max_capacity: [backend_default] diff --git a/bundle/direct/dresources/configs/jobs.yml b/bundle/direct/dresources/configs/jobs.yml index 4bf01be8ec7..f6fcff96a27 100644 --- a/bundle/direct/dresources/configs/jobs.yml +++ b/bundle/direct/dresources/configs/jobs.yml @@ -44,23 +44,8 @@ fields: # 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 - trigger.table_update.condition: - ignore_remote: 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. - triggers[*].table_update.condition: - ignore_remote: input_only - # 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 + trigger.table_update.condition: [input_only] + triggers[*].table_update.condition: [input_only] tasks[*].new_cluster: remote_additions_when: policy_id tasks[*].for_each_task.task.new_cluster: @@ -69,46 +54,21 @@ fields: remote_additions_when: policy_id # Same as clusters.enable_elastic_disk — see clusters/resource_cluster.go#L331 # s.SchemaPath("enable_elastic_disk").SetComputed() - tasks[*].new_cluster.enable_elastic_disk: - backend_default: true - tasks[*].for_each_task.task.new_cluster.enable_elastic_disk: - backend_default: true - job_clusters[*].new_cluster.enable_elastic_disk: - backend_default: true - # Same as clusters.enable_local_disk_encryption — see clusters/resource_cluster.go#L332 - # s.SchemaPath("enable_local_disk_encryption").SetComputed() - tasks[*].new_cluster.enable_local_disk_encryption: - backend_default: true - tasks[*].for_each_task.task.new_cluster.enable_local_disk_encryption: - backend_default: true - job_clusters[*].new_cluster.enable_local_disk_encryption: - backend_default: true - # Same as clusters.node_type_id — see clusters/resource_cluster.go#L333 - # s.SchemaPath("node_type_id").SetComputed() - tasks[*].new_cluster.node_type_id: - backend_default: true - tasks[*].for_each_task.task.new_cluster.node_type_id: - backend_default: true - job_clusters[*].new_cluster.node_type_id: - backend_default: true - # Same as clusters.driver_node_type_id — see clusters/resource_cluster.go#L334 - # s.SchemaPath("driver_node_type_id").SetComputed() - tasks[*].new_cluster.driver_node_type_id: - backend_default: true - tasks[*].for_each_task.task.new_cluster.driver_node_type_id: - backend_default: true - job_clusters[*].new_cluster.driver_node_type_id: - backend_default: true - # Same as clusters.driver_instance_pool_id — see clusters/resource_cluster.go#L335 - # s.SchemaPath("driver_instance_pool_id").SetComputed() - tasks[*].new_cluster.driver_instance_pool_id: - backend_default: true - tasks[*].for_each_task.task.new_cluster.driver_instance_pool_id: - backend_default: true - job_clusters[*].new_cluster.driver_instance_pool_id: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L531 - # s.SchemaPath("format").SetComputed() + tasks[*].new_cluster.enable_elastic_disk: [backend_default] + tasks[*].for_each_task.task.new_cluster.enable_elastic_disk: [backend_default] + job_clusters[*].new_cluster.enable_elastic_disk: [backend_default] + tasks[*].new_cluster.enable_local_disk_encryption: [backend_default] + tasks[*].for_each_task.task.new_cluster.enable_local_disk_encryption: [backend_default] + job_clusters[*].new_cluster.enable_local_disk_encryption: [backend_default] + tasks[*].new_cluster.node_type_id: [backend_default] + tasks[*].for_each_task.task.new_cluster.node_type_id: [backend_default] + job_clusters[*].new_cluster.node_type_id: [backend_default] + tasks[*].new_cluster.driver_node_type_id: [backend_default] + tasks[*].for_each_task.task.new_cluster.driver_node_type_id: [backend_default] + job_clusters[*].new_cluster.driver_node_type_id: [backend_default] + tasks[*].new_cluster.driver_instance_pool_id: [backend_default] + tasks[*].for_each_task.task.new_cluster.driver_instance_pool_id: [backend_default] + job_clusters[*].new_cluster.driver_instance_pool_id: [backend_default] format: backend_values: ["MULTI_TASK", "SINGLE_TASK"] "tasks[*].run_if": @@ -119,35 +79,15 @@ fields: backend_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) - run_as: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L527 - # s.SchemaPath("run_as").SetComputed() - # 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() - tasks[*].notebook_task.source: - backend_default: true - tasks[*].for_each_task.task.notebook_task.source: - backend_default: true - tasks[*].spark_python_task.source: - backend_default: true - tasks[*].for_each_task.task.spark_python_task.source: - backend_default: true - tasks[*].sql_task.file.source: - backend_default: true - tasks[*].for_each_task.task.sql_task.file.source: - backend_default: true - tasks[*].dbt_task.source: - backend_default: true - tasks[*].for_each_task.task.dbt_task.source: - backend_default: true - # Same as clusters.data_security_mode: backend sets this when not specified - tasks[*].new_cluster.data_security_mode: - backend_default: true - tasks[*].for_each_task.task.new_cluster.data_security_mode: - backend_default: true - job_clusters[*].new_cluster.data_security_mode: - backend_default: true + run_as: [backend_default] + tasks[*].notebook_task.source: [backend_default] + tasks[*].for_each_task.task.notebook_task.source: [backend_default] + tasks[*].spark_python_task.source: [backend_default] + tasks[*].for_each_task.task.spark_python_task.source: [backend_default] + tasks[*].sql_task.file.source: [backend_default] + tasks[*].for_each_task.task.sql_task.file.source: [backend_default] + tasks[*].dbt_task.source: [backend_default] + tasks[*].for_each_task.task.dbt_task.source: [backend_default] + tasks[*].new_cluster.data_security_mode: [backend_default] + tasks[*].for_each_task.task.new_cluster.data_security_mode: [backend_default] + job_clusters[*].new_cluster.data_security_mode: [backend_default] diff --git a/bundle/direct/dresources/configs/mcp_services.generated.yml b/bundle/direct/dresources/configs/mcp_services.generated.yml index 178b3c04da3..3b6d4d84a43 100644 --- a/bundle/direct/dresources/configs/mcp_services.generated.yml +++ b/bundle/direct/dresources/configs/mcp_services.generated.yml @@ -1,7 +1,5 @@ # Generated, do not edit. fields: - config.source_connection.is_deleted: - spec: output_only - config.source_connection.options: - spec: output_only + config.source_connection.is_deleted: [mutable_output] + config.source_connection.options: [mutable_output] diff --git a/bundle/direct/dresources/configs/mcp_services.yml b/bundle/direct/dresources/configs/mcp_services.yml index ee9406b9630..7a537b62397 100644 --- a/bundle/direct/dresources/configs/mcp_services.yml +++ b/bundle/direct/dresources/configs/mcp_services.yml @@ -4,7 +4,5 @@ fields: # 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. - parent: - action: id - mcp_service_id: - action: id + parent: [id] + mcp_service_id: [id] diff --git a/bundle/direct/dresources/configs/model_provider_services.generated.yml b/bundle/direct/dresources/configs/model_provider_services.generated.yml index a8a833a7bb3..943a8481f1b 100644 --- a/bundle/direct/dresources/configs/model_provider_services.generated.yml +++ b/bundle/direct/dresources/configs/model_provider_services.generated.yml @@ -1,29 +1,16 @@ # Generated, do not edit. fields: - config.amazon_bedrock.direct.aws_access_key.secret_access_key.plaintext: - spec: input_only - config.anthropic.direct.api_key.plaintext: - spec: input_only - config.azure_openai.direct.api_key.plaintext: - spec: input_only - config.azure_openai.direct.entra_service_principal.client_secret.plaintext: - spec: input_only - config.custom.direct.api_key.plaintext: - spec: input_only - config.custom.direct.header_auth.api_key_value.plaintext: - spec: input_only - config.gemini_enterprise.direct.api_key.plaintext: - spec: input_only - config.inference_table.is_deleted: - spec: output_only - config.inference_table.table: - spec: output_only - config.microsoft_foundry.direct.api_key.plaintext: - spec: input_only - config.microsoft_foundry.direct.entra_service_principal.client_secret.plaintext: - spec: input_only - config.openai.direct.api_key.plaintext: - spec: input_only - config.provider_type: - spec: immutable + config.amazon_bedrock.direct.aws_access_key.secret_access_key.plaintext: [input_only] + config.anthropic.direct.api_key.plaintext: [input_only] + config.azure_openai.direct.api_key.plaintext: [input_only] + config.azure_openai.direct.entra_service_principal.client_secret.plaintext: [input_only] + config.custom.direct.api_key.plaintext: [input_only] + config.custom.direct.header_auth.api_key_value.plaintext: [input_only] + config.gemini_enterprise.direct.api_key.plaintext: [input_only] + config.inference_table.is_deleted: [mutable_output] + config.inference_table.table: [mutable_output] + config.microsoft_foundry.direct.api_key.plaintext: [input_only] + config.microsoft_foundry.direct.entra_service_principal.client_secret.plaintext: [input_only] + config.openai.direct.api_key.plaintext: [input_only] + config.provider_type: [immutable] diff --git a/bundle/direct/dresources/configs/model_provider_services.yml b/bundle/direct/dresources/configs/model_provider_services.yml index 26fab21fdb5..26462daf4f4 100644 --- a/bundle/direct/dresources/configs/model_provider_services.yml +++ b/bundle/direct/dresources/configs/model_provider_services.yml @@ -3,7 +3,5 @@ fields: # name (model-provider-services/{catalog}.{schema}.{model_provider_service}), # which is the ID the resource is fetched by. Both immutable; a local # change recreates. DoRead reconstructs them from the returned name. - parent: - action: id - model_provider_service_id: - action: id + parent: [id] + model_provider_service_id: [id] diff --git a/bundle/direct/dresources/configs/model_services.generated.yml b/bundle/direct/dresources/configs/model_services.generated.yml index 195e160424e..73fcf30ec87 100644 --- a/bundle/direct/dresources/configs/model_services.generated.yml +++ b/bundle/direct/dresources/configs/model_services.generated.yml @@ -1,15 +1,9 @@ # Generated, do not edit. fields: - config.inference_table.is_deleted: - spec: output_only - config.inference_table.table: - spec: output_only - config.routing.destinations[*].is_deleted: - spec: output_only - config.routing.destinations[*].provisioned_throughput_config.model: - spec: output_only - config.routing.fallback.destinations[*].is_deleted: - spec: output_only - config.routing.fallback.destinations[*].provisioned_throughput_config.model: - spec: output_only + config.inference_table.is_deleted: [mutable_output] + config.inference_table.table: [mutable_output] + config.routing.destinations[*].is_deleted: [mutable_output] + config.routing.destinations[*].provisioned_throughput_config.model: [mutable_output] + config.routing.fallback.destinations[*].is_deleted: [mutable_output] + config.routing.fallback.destinations[*].provisioned_throughput_config.model: [mutable_output] diff --git a/bundle/direct/dresources/configs/model_services.yml b/bundle/direct/dresources/configs/model_services.yml index f8d72e18405..b467e6565cd 100644 --- a/bundle/direct/dresources/configs/model_services.yml +++ b/bundle/direct/dresources/configs/model_services.yml @@ -4,7 +4,5 @@ fields: # 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. - parent: - action: id - model_service_id: - action: id + parent: [id] + model_service_id: [id] diff --git a/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml b/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml index e17195d3562..31ca8251ecf 100644 --- a/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml +++ b/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml @@ -1,33 +1,18 @@ # Generated, do not edit. fields: - config.served_entities[*].burst_scaling_enabled: - spec: input_only - config.served_entities[*].external_model.ai21labs_config.ai21labs_api_key_plaintext: - spec: input_only - config.served_entities[*].external_model.amazon_bedrock_config.aws_access_key_id_plaintext: - spec: input_only - config.served_entities[*].external_model.amazon_bedrock_config.aws_secret_access_key_plaintext: - spec: input_only - config.served_entities[*].external_model.anthropic_config.anthropic_api_key_plaintext: - spec: input_only - config.served_entities[*].external_model.cohere_config.cohere_api_key_plaintext: - spec: input_only - config.served_entities[*].external_model.custom_provider_config.api_key_auth.value_plaintext: - spec: input_only - config.served_entities[*].external_model.custom_provider_config.bearer_token_auth.token_plaintext: - spec: input_only - config.served_entities[*].external_model.databricks_model_serving_config.databricks_api_token_plaintext: - spec: input_only - config.served_entities[*].external_model.google_cloud_vertex_ai_config.private_key_plaintext: - spec: input_only - config.served_entities[*].external_model.openai_config.microsoft_entra_client_secret_plaintext: - spec: input_only - config.served_entities[*].external_model.openai_config.openai_api_key_plaintext: - spec: input_only - config.served_entities[*].external_model.palm_config.palm_api_key_plaintext: - spec: input_only - config.served_models[*].burst_scaling_enabled: - spec: input_only - telemetry_config.inference_table_config.name: - spec: output_only + config.served_entities[*].burst_scaling_enabled: [input_only] + config.served_entities[*].external_model.ai21labs_config.ai21labs_api_key_plaintext: [input_only] + config.served_entities[*].external_model.amazon_bedrock_config.aws_access_key_id_plaintext: [input_only] + config.served_entities[*].external_model.amazon_bedrock_config.aws_secret_access_key_plaintext: [input_only] + config.served_entities[*].external_model.anthropic_config.anthropic_api_key_plaintext: [input_only] + config.served_entities[*].external_model.cohere_config.cohere_api_key_plaintext: [input_only] + config.served_entities[*].external_model.custom_provider_config.api_key_auth.value_plaintext: [input_only] + config.served_entities[*].external_model.custom_provider_config.bearer_token_auth.token_plaintext: [input_only] + config.served_entities[*].external_model.databricks_model_serving_config.databricks_api_token_plaintext: [input_only] + config.served_entities[*].external_model.google_cloud_vertex_ai_config.private_key_plaintext: [input_only] + config.served_entities[*].external_model.openai_config.microsoft_entra_client_secret_plaintext: [input_only] + config.served_entities[*].external_model.openai_config.openai_api_key_plaintext: [input_only] + config.served_entities[*].external_model.palm_config.palm_api_key_plaintext: [input_only] + config.served_models[*].burst_scaling_enabled: [input_only] + telemetry_config.inference_table_config.name: [mutable_output] diff --git a/bundle/direct/dresources/configs/model_serving_endpoints.yml b/bundle/direct/dresources/configs/model_serving_endpoints.yml index c39921b8b73..be9ec7adfa8 100644 --- a/bundle/direct/dresources/configs/model_serving_endpoints.yml +++ b/bundle/direct/dresources/configs/model_serving_endpoints.yml @@ -1,21 +1,11 @@ # TF implementation: https://github.com/databricks/terraform-provider-databricks/blob/6c106e8e7052bb2726148d66309fd460ed444236/mlflow/resource_mlflow_experiment.go#L22 fields: - name: - action: id - # description is immutable, can't be updated via API - description: - action: immutable - config.auto_capture_config.catalog_name: - action: immutable - config.auto_capture_config.schema_name: - action: immutable - config.auto_capture_config.table_name_prefix: - action: immutable - route_optimized: - action: immutable - # 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. + name: [id] + description: [immutable] + config.auto_capture_config.catalog_name: [immutable] + config.auto_capture_config.schema_name: [immutable] + config.auto_capture_config.table_name_prefix: [immutable] + route_optimized: [immutable] budget_policy_id: ignore_remote: no_update_api action: ignore_local @@ -23,30 +13,13 @@ fields: # 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. - telemetry_config.table_names: - ignore_remote: input_only + telemetry_config.table_names: [input_only] rate_limits: action: ignore_local reason: not_implemented # Remote-only telemetry is reported as a change at this parent path. - telemetry_config: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L383 - # common.CustomizeSchemaPath(m, "config", "served_entities", "name").SetComputed() - config.served_entities[*].name: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L384 - # common.CustomizeSchemaPath(m, "config", "served_entities", "workload_type").SetComputed() - config.served_entities[*].workload_type: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L372 - # common.CustomizeSchemaPath(m, "config", "auto_capture_config", "enabled").SetComputed() - config.auto_capture_config.enabled: - backend_default: true - # 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. - config.traffic_config: - backend_default: true + telemetry_config: [backend_default] + config.served_entities[*].name: [backend_default] + config.served_entities[*].workload_type: [backend_default] + config.auto_capture_config.enabled: [backend_default] + config.traffic_config: [backend_default] diff --git a/bundle/direct/dresources/configs/pipelines.generated.yml b/bundle/direct/dresources/configs/pipelines.generated.yml index 196f4e54e54..73885e5f847 100644 --- a/bundle/direct/dresources/configs/pipelines.generated.yml +++ b/bundle/direct/dresources/configs/pipelines.generated.yml @@ -1,5 +1,4 @@ # Generated, do not edit. fields: - ingestion_definition.source_type: - spec: output_only + ingestion_definition.source_type: [mutable_output] diff --git a/bundle/direct/dresources/configs/pipelines.yml b/bundle/direct/dresources/configs/pipelines.yml index c65db7858c7..4acf6d42170 100644 --- a/bundle/direct/dresources/configs/pipelines.yml +++ b/bundle/direct/dresources/configs/pipelines.yml @@ -1,24 +1,12 @@ fields: - storage: - action: immutable - ingestion_definition.connection_name: - action: immutable - ingestion_definition.ingestion_gateway_id: - action: immutable - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L204 - gateway_definition.connection_id: - action: immutable - gateway_definition.connection_name: - action: immutable - gateway_definition.gateway_storage_catalog: - action: immutable - gateway_definition.gateway_storage_schema: - action: immutable - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L209 - ingestion_definition.ingest_from_uc_foreign_catalog: - action: 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. + storage: [immutable] + ingestion_definition.connection_name: [immutable] + ingestion_definition.ingestion_gateway_id: [immutable] + gateway_definition.connection_id: [immutable] + gateway_definition.connection_name: [immutable] + gateway_definition.gateway_storage_catalog: [immutable] + gateway_definition.gateway_storage_schema: [immutable] + ingestion_definition.ingest_from_uc_foreign_catalog: [immutable] deployment.version_id: ignore_remote: auto action: ignore_local @@ -37,31 +25,12 @@ fields: # 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. - run_as: - ignore_remote: 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. - allow_duplicate_names: - ignore_remote: input_only - # Pipelines adds ANSI mode to the configuration map when it is omitted. + run_as: [input_only] + allow_duplicate_names: [input_only] configuration['spark.sql.ansi.enabled']: backend_values: ["true"] - clusters[*].node_type_id: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L218 - # s.SchemaPath("cluster", "node_type_id").SetComputed() - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L219 - # s.SchemaPath("cluster", "driver_node_type_id").SetComputed() - clusters[*].driver_node_type_id: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L220 - # s.SchemaPath("cluster", "enable_local_disk_encryption").SetComputed() - clusters[*].enable_local_disk_encryption: - backend_default: true - # 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() - event_log.catalog: - backend_default: true - event_log.schema: - backend_default: true + clusters[*].node_type_id: [backend_default] + clusters[*].driver_node_type_id: [backend_default] + clusters[*].enable_local_disk_encryption: [backend_default] + event_log.catalog: [backend_default] + event_log.schema: [backend_default] diff --git a/bundle/direct/dresources/configs/postgres_branches.generated.yml b/bundle/direct/dresources/configs/postgres_branches.generated.yml index 00b19c5afbf..592755005bb 100644 --- a/bundle/direct/dresources/configs/postgres_branches.generated.yml +++ b/bundle/direct/dresources/configs/postgres_branches.generated.yml @@ -1,19 +1,11 @@ # Generated, do not edit. fields: - expire_time: - spec: input_only - is_protected: - spec: input_only - no_expiry: - spec: input_only - source_branch: - spec: [immutable, input_only] - source_branch_lsn: - spec: [immutable, input_only] - source_branch_time: - spec: [immutable, input_only] - source_snapshot: - spec: [immutable, input_only] - ttl: - spec: input_only + expire_time: [input_only] + is_protected: [input_only] + no_expiry: [input_only] + source_branch: [immutable, input_only] + source_branch_lsn: [immutable, input_only] + source_branch_time: [immutable, input_only] + source_snapshot: [immutable, input_only] + ttl: [input_only] diff --git a/bundle/direct/dresources/configs/postgres_branches.yml b/bundle/direct/dresources/configs/postgres_branches.yml index 90c165ee8b5..b945270cab5 100644 --- a/bundle/direct/dresources/configs/postgres_branches.yml +++ b/bundle/direct/dresources/configs/postgres_branches.yml @@ -1,14 +1,10 @@ fields: # parent and branch_id are immutable (part of hierarchical name, not in API spec) - parent: - action: id - branch_id: - action: id - # replace_existing only takes effect on create; toggling it later is a no-op. + parent: [id] + branch_id: [id] replace_existing: action: ignore_local reason: input_only # name ("projects/{project_id}/branches/{branch_id}") is backend-assigned at # creation and immutable; see the stable_output_fields doc in config.go and ES-2202624. - name: - stable_output: true + name: [immutable_output] diff --git a/bundle/direct/dresources/configs/postgres_catalogs.generated.yml b/bundle/direct/dresources/configs/postgres_catalogs.generated.yml index 2be112d055b..07b528649e1 100644 --- a/bundle/direct/dresources/configs/postgres_catalogs.generated.yml +++ b/bundle/direct/dresources/configs/postgres_catalogs.generated.yml @@ -1,9 +1,6 @@ # Generated, do not edit. fields: - branch: - spec: input_only - create_database_if_missing: - spec: input_only - postgres_database: - spec: [immutable, input_only] + branch: [input_only] + create_database_if_missing: [input_only] + postgres_database: [immutable, input_only] diff --git a/bundle/direct/dresources/configs/postgres_catalogs.yml b/bundle/direct/dresources/configs/postgres_catalogs.yml index c182891e2ef..9baddf30b87 100644 --- a/bundle/direct/dresources/configs/postgres_catalogs.yml +++ b/bundle/direct/dresources/configs/postgres_catalogs.yml @@ -5,5 +5,4 @@ action: immutable fields: # catalog_id is part of the hierarchical name and immutable. - catalog_id: - action: id + catalog_id: [id] diff --git a/bundle/direct/dresources/configs/postgres_databases.generated.yml b/bundle/direct/dresources/configs/postgres_databases.generated.yml index c6362094e4e..1a7c81aeaae 100644 --- a/bundle/direct/dresources/configs/postgres_databases.generated.yml +++ b/bundle/direct/dresources/configs/postgres_databases.generated.yml @@ -1,7 +1,5 @@ # Generated, do not edit. fields: - postgres_database: - spec: input_only - role: - spec: input_only + postgres_database: [input_only] + role: [input_only] diff --git a/bundle/direct/dresources/configs/postgres_databases.yml b/bundle/direct/dresources/configs/postgres_databases.yml index ca220fc7079..61f21be23cf 100644 --- a/bundle/direct/dresources/configs/postgres_databases.yml +++ b/bundle/direct/dresources/configs/postgres_databases.yml @@ -1,14 +1,10 @@ fields: # parent and database_id are immutable (part of hierarchical name, not in API spec) - parent: - action: id - database_id: - action: id - # replace_existing only takes effect on create; toggling it later is a no-op. + parent: [id] + database_id: [id] replace_existing: action: ignore_local reason: input_only # name ("projects/{p}/branches/{b}/databases/{database_id}") is backend-assigned # at creation and immutable; see the stable_output_fields doc in config.go and ES-2202624. - name: - stable_output: true + name: [immutable_output] diff --git a/bundle/direct/dresources/configs/postgres_endpoints.generated.yml b/bundle/direct/dresources/configs/postgres_endpoints.generated.yml index fb92a51a924..aac4189f2c9 100644 --- a/bundle/direct/dresources/configs/postgres_endpoints.generated.yml +++ b/bundle/direct/dresources/configs/postgres_endpoints.generated.yml @@ -1,19 +1,11 @@ # Generated, do not edit. fields: - autoscaling_limit_max_cu: - spec: input_only - autoscaling_limit_min_cu: - spec: input_only - disabled: - spec: input_only - endpoint_type: - spec: [immutable, input_only] - group: - spec: input_only - no_suspension: - spec: input_only - settings: - spec: input_only - suspend_timeout_duration: - spec: input_only + autoscaling_limit_max_cu: [input_only] + autoscaling_limit_min_cu: [input_only] + disabled: [input_only] + endpoint_type: [immutable, input_only] + group: [input_only] + no_suspension: [input_only] + settings: [input_only] + suspend_timeout_duration: [input_only] diff --git a/bundle/direct/dresources/configs/postgres_endpoints.yml b/bundle/direct/dresources/configs/postgres_endpoints.yml index 42ea9a50b3c..c8395a51728 100644 --- a/bundle/direct/dresources/configs/postgres_endpoints.yml +++ b/bundle/direct/dresources/configs/postgres_endpoints.yml @@ -1,14 +1,10 @@ fields: # parent and endpoint_id are immutable (part of hierarchical name, not in API spec) - parent: - action: id - endpoint_id: - action: id - # replace_existing only takes effect on create; toggling it later is a no-op. + parent: [id] + endpoint_id: [id] replace_existing: action: ignore_local reason: input_only # name ("projects/{p}/branches/{b}/endpoints/{endpoint_id}") is backend-assigned # at creation and immutable; see the stable_output_fields doc in config.go and ES-2202624. - name: - stable_output: true + name: [immutable_output] diff --git a/bundle/direct/dresources/configs/postgres_projects.generated.yml b/bundle/direct/dresources/configs/postgres_projects.generated.yml index bd47cc76d41..a87c9b4e6ef 100644 --- a/bundle/direct/dresources/configs/postgres_projects.generated.yml +++ b/bundle/direct/dresources/configs/postgres_projects.generated.yml @@ -1,23 +1,13 @@ # Generated, do not edit. fields: - budget_policy_id: - spec: input_only - custom_tags: - spec: input_only - custom_tags[*].key: - spec: input_only - custom_tags[*].value: - spec: input_only - default_branch: - spec: input_only - default_endpoint_settings: - spec: input_only - display_name: - spec: input_only - enable_pg_native_login: - spec: input_only - history_retention_duration: - spec: input_only - pg_version: - spec: [immutable, input_only] + budget_policy_id: [input_only] + custom_tags: [input_only] + custom_tags[*].key: [input_only] + custom_tags[*].value: [input_only] + default_branch: [input_only] + default_endpoint_settings: [input_only] + display_name: [input_only] + enable_pg_native_login: [input_only] + history_retention_duration: [input_only] + pg_version: [immutable, input_only] diff --git a/bundle/direct/dresources/configs/postgres_projects.yml b/bundle/direct/dresources/configs/postgres_projects.yml index 7ad7a50a6cb..30e06fa84fe 100644 --- a/bundle/direct/dresources/configs/postgres_projects.yml +++ b/bundle/direct/dresources/configs/postgres_projects.yml @@ -1,8 +1,4 @@ fields: # project_id is immutable (part of hierarchical name, not in API spec) - project_id: - action: id - # name ("projects/{project_id}") is backend-assigned at creation and immutable; - # see the stable_output_fields doc in config.go and ES-2202624. - name: - stable_output: true + project_id: [id] + name: [immutable_output] diff --git a/bundle/direct/dresources/configs/postgres_roles.generated.yml b/bundle/direct/dresources/configs/postgres_roles.generated.yml index 9f89810320e..8f1827119c0 100644 --- a/bundle/direct/dresources/configs/postgres_roles.generated.yml +++ b/bundle/direct/dresources/configs/postgres_roles.generated.yml @@ -1,13 +1,8 @@ # Generated, do not edit. fields: - attributes: - spec: input_only - auth_method: - spec: input_only - identity_type: - spec: input_only - membership_roles: - spec: input_only - postgres_role: - spec: input_only + attributes: [input_only] + auth_method: [input_only] + identity_type: [input_only] + membership_roles: [input_only] + postgres_role: [input_only] diff --git a/bundle/direct/dresources/configs/postgres_roles.yml b/bundle/direct/dresources/configs/postgres_roles.yml index 237e4581bde..70e3aaed9f4 100644 --- a/bundle/direct/dresources/configs/postgres_roles.yml +++ b/bundle/direct/dresources/configs/postgres_roles.yml @@ -6,21 +6,9 @@ fields: # name ("projects/{project_id}/branches/{branch_id}/roles/{role_id}") is # backend-assigned at creation and immutable; see the stable_output_fields doc # in config.go and ES-2202624. - name: - stable_output: true - # parent and role_id are immutable (together they form the hierarchical name). - parent: - action: immutable - role_id: - action: 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. - postgres_role: - action: immutable - auth_method: - action: immutable - identity_type: - action: immutable + name: [immutable_output] + parent: [immutable] + role_id: [immutable] + postgres_role: [immutable] + auth_method: [immutable] + identity_type: [immutable] diff --git a/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml b/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml index ffe9fe0564b..ad111437daa 100644 --- a/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml +++ b/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml @@ -2,9 +2,5 @@ fields: # branch composes the schedule's hierarchical name # ("{branch}/snapshot-schedule"); changing it targets a different branch's # schedule, so it recreates (delete + create). - branch: - action: id - # name ("{branch}/snapshot-schedule") is backend-assigned at creation and - # immutable; see the stable_output_fields doc in config.go and ES-2202624. - name: - stable_output: true + branch: [id] + name: [immutable_output] diff --git a/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml b/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml index 138f47acb4c..ad1799618e1 100644 --- a/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml +++ b/bundle/direct/dresources/configs/postgres_synced_tables.generated.yml @@ -1,41 +1,22 @@ # Generated, do not edit. fields: - accelerated_sync: - spec: input_only - branch: - spec: input_only - create_database_objects_if_missing: - spec: input_only - existing_pipeline_id: - spec: input_only - extra_columns: - spec: input_only - extra_columns[*].column_name: - spec: input_only - extra_columns[*].column_type: - spec: input_only - extra_columns[*].compute: - spec: input_only - extra_columns[*].maintenance: - spec: input_only - new_pipeline_spec: - spec: input_only - postgres_database: - spec: input_only - primary_key_columns: - spec: input_only - scheduling_policy: - spec: input_only - source_table_full_name: - spec: input_only - timeseries_key: - spec: input_only - type_overrides: - spec: input_only - type_overrides[*].column_name: - spec: input_only - type_overrides[*].pg_type: - spec: input_only - type_overrides[*].size: - spec: input_only + accelerated_sync: [input_only] + branch: [input_only] + create_database_objects_if_missing: [input_only] + existing_pipeline_id: [input_only] + extra_columns: [input_only] + extra_columns[*].column_name: [input_only] + extra_columns[*].column_type: [input_only] + extra_columns[*].compute: [input_only] + extra_columns[*].maintenance: [input_only] + new_pipeline_spec: [input_only] + postgres_database: [input_only] + primary_key_columns: [input_only] + scheduling_policy: [input_only] + source_table_full_name: [input_only] + timeseries_key: [input_only] + type_overrides: [input_only] + type_overrides[*].column_name: [input_only] + type_overrides[*].pg_type: [input_only] + type_overrides[*].size: [input_only] diff --git a/bundle/direct/dresources/configs/postgres_synced_tables.yml b/bundle/direct/dresources/configs/postgres_synced_tables.yml index 4135f8a9eb4..c1bd24baa0f 100644 --- a/bundle/direct/dresources/configs/postgres_synced_tables.yml +++ b/bundle/direct/dresources/configs/postgres_synced_tables.yml @@ -7,5 +7,4 @@ # edit still triggers a recreate. Same pattern as secret_scopes. action: immutable fields: - synced_table_id: - action: id + synced_table_id: [id] diff --git a/bundle/direct/dresources/configs/quality_monitors.yml b/bundle/direct/dresources/configs/quality_monitors.yml index f60105893cc..75fa89548a3 100644 --- a/bundle/direct/dresources/configs/quality_monitors.yml +++ b/bundle/direct/dresources/configs/quality_monitors.yml @@ -1,5 +1,3 @@ fields: - table_name: - action: id - assets_dir: - action: immutable + table_name: [id] + assets_dir: [immutable] diff --git a/bundle/direct/dresources/configs/registered_models.generated.yml b/bundle/direct/dresources/configs/registered_models.generated.yml index 95b314b4056..cccbc336c26 100644 --- a/bundle/direct/dresources/configs/registered_models.generated.yml +++ b/bundle/direct/dresources/configs/registered_models.generated.yml @@ -1,5 +1,4 @@ # Generated, do not edit. fields: - browse_only: - spec: output_only + browse_only: [mutable_output] diff --git a/bundle/direct/dresources/configs/registered_models.yml b/bundle/direct/dresources/configs/registered_models.yml index 582a8be8c64..102c5e4e36a 100644 --- a/bundle/direct/dresources/configs/registered_models.yml +++ b/bundle/direct/dresources/configs/registered_models.yml @@ -2,34 +2,17 @@ fields: # 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. - created_at: - ignore_remote: output_only - created_by: - ignore_remote: output_only - updated_at: - ignore_remote: output_only - updated_by: - ignore_remote: 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). - aliases: - ignore_remote: input_only - # The name can technically be updated without recreate. We recreate for now though - # to match TF implementation. + created_at: [mutable_output] + created_by: [mutable_output] + updated_at: [mutable_output] + updated_by: [mutable_output] + aliases: [input_only] name: action: id reason: terraform_compat - catalog_name: - action: id - schema_name: - action: id - storage_location: - action: immutable - # owner, full_name, metastore_id are Computed in TF (backend-set output fields). - owner: - backend_default: true - full_name: - backend_default: true - metastore_id: - backend_default: true + catalog_name: [id] + schema_name: [id] + storage_location: [immutable] + owner: [backend_default] + full_name: [backend_default] + metastore_id: [backend_default] diff --git a/bundle/direct/dresources/configs/schemas.yml b/bundle/direct/dresources/configs/schemas.yml index d07da2310bc..1349bf0e813 100644 --- a/bundle/direct/dresources/configs/schemas.yml +++ b/bundle/direct/dresources/configs/schemas.yml @@ -1,14 +1,6 @@ fields: # UC lowercases identifier names; remote returns "myschema" for config "MySchema". - name: - action: id - catalog_name: - action: id - storage_root: - action: immutable - compare: trim_slash - # 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". - properties['unity.catalog.managed.*.defaults.*']: - backend_default: true + name: [id] + catalog_name: [id] + storage_root: [immutable, trim_slash] + properties['unity.catalog.managed.*.defaults.*']: [backend_default] diff --git a/bundle/direct/dresources/configs/secret_scopes.permissions.yml b/bundle/direct/dresources/configs/secret_scopes.permissions.yml index cca2ea61f74..d3fac39d489 100644 --- a/bundle/direct/dresources/configs/secret_scopes.permissions.yml +++ b/bundle/direct/dresources/configs/secret_scopes.permissions.yml @@ -2,5 +2,4 @@ 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. - scope_name: - action: id_renameable + scope_name: [renameable_id] diff --git a/bundle/direct/dresources/configs/secret_scopes.yml b/bundle/direct/dresources/configs/secret_scopes.yml index 3a37dc4ee18..e6b42263bfc 100644 --- a/bundle/direct/dresources/configs/secret_scopes.yml +++ b/bundle/direct/dresources/configs/secret_scopes.yml @@ -1,9 +1,5 @@ fields: - scope: - action: id - scope_backend_type: - action: immutable - backend_azure_keyvault: - action: immutable - initial_manage_principal: - action: immutable + scope: [id] + scope_backend_type: [immutable] + backend_azure_keyvault: [immutable] + initial_manage_principal: [immutable] diff --git a/bundle/direct/dresources/configs/secrets.generated.yml b/bundle/direct/dresources/configs/secrets.generated.yml index ab3b54bc77e..f343cb9307d 100644 --- a/bundle/direct/dresources/configs/secrets.generated.yml +++ b/bundle/direct/dresources/configs/secrets.generated.yml @@ -1,29 +1,16 @@ # Generated, do not edit. fields: - catalog_name: - spec: immutable - create_time: - spec: output_only - created_by: - spec: output_only - effective_owner: - spec: output_only - effective_value: - spec: output_only - full_name: - spec: output_only - metastore_id: - spec: output_only - name: - spec: immutable - owner: - spec: input_only - schema_name: - spec: immutable - update_time: - spec: output_only - updated_by: - spec: output_only - value: - spec: input_only + catalog_name: [immutable] + create_time: [mutable_output] + created_by: [mutable_output] + effective_owner: [mutable_output] + effective_value: [mutable_output] + full_name: [mutable_output] + metastore_id: [mutable_output] + name: [immutable] + owner: [input_only] + schema_name: [immutable] + update_time: [mutable_output] + updated_by: [mutable_output] + value: [input_only] diff --git a/bundle/direct/dresources/configs/secrets.yml b/bundle/direct/dresources/configs/secrets.yml index d1f8887e122..9aa519ddfc6 100644 --- a/bundle/direct/dresources/configs/secrets.yml +++ b/bundle/direct/dresources/configs/secrets.yml @@ -1,5 +1,3 @@ fields: - value: - sensitive: true - effective_value: - sensitive: true + value: [sensitive] + effective_value: [sensitive] diff --git a/bundle/direct/dresources/configs/sql_warehouses.yml b/bundle/direct/dresources/configs/sql_warehouses.yml index 61f61bea438..c427df19895 100644 --- a/bundle/direct/dresources/configs/sql_warehouses.yml +++ b/bundle/direct/dresources/configs/sql_warehouses.yml @@ -16,18 +16,7 @@ fields: min_num_clusters: ignore_remote: managed # creator_name is readonly, can't be updated via API - creator_name: - ignore_remote: output_only - # 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). - lifecycle: - backend_default: true - lifecycle.started: - backend_default: true - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L69 - # m["enable_serverless_compute"].Computed = true - enable_serverless_compute: - backend_default: true + creator_name: [mutable_output] + lifecycle: [backend_default] + lifecycle.started: [backend_default] + enable_serverless_compute: [backend_default] diff --git a/bundle/direct/dresources/configs/synced_database_tables.generated.yml b/bundle/direct/dresources/configs/synced_database_tables.generated.yml index 00e1894669a..fafe0355d84 100644 --- a/bundle/direct/dresources/configs/synced_database_tables.generated.yml +++ b/bundle/direct/dresources/configs/synced_database_tables.generated.yml @@ -1,23 +1,13 @@ # Generated, do not edit. fields: - data_synchronization_status: - spec: output_only - database_instance_name: - spec: input_only - effective_database_instance_name: - spec: output_only - effective_logical_database_name: - spec: output_only - logical_database_name: - spec: input_only - spec.accelerated_sync: - spec: input_only - spec.create_database_objects_if_missing: - spec: input_only - spec.existing_pipeline_id: - spec: input_only - spec.new_pipeline_spec: - spec: input_only - unity_catalog_provisioning_state: - spec: output_only + data_synchronization_status: [mutable_output] + database_instance_name: [input_only] + effective_database_instance_name: [mutable_output] + effective_logical_database_name: [mutable_output] + logical_database_name: [input_only] + spec.accelerated_sync: [input_only] + spec.create_database_objects_if_missing: [input_only] + spec.existing_pipeline_id: [input_only] + spec.new_pipeline_spec: [input_only] + unity_catalog_provisioning_state: [mutable_output] diff --git a/bundle/direct/dresources/configs/synced_database_tables.yml b/bundle/direct/dresources/configs/synced_database_tables.yml index 863ddd38cf3..b66ff1256dc 100644 --- a/bundle/direct/dresources/configs/synced_database_tables.yml +++ b/bundle/direct/dresources/configs/synced_database_tables.yml @@ -6,5 +6,4 @@ # stay idempotent. Same pattern as postgres_synced_tables. action: immutable fields: - name: - action: id + name: [id] diff --git a/bundle/direct/dresources/configs/vector_search_endpoints.yml b/bundle/direct/dresources/configs/vector_search_endpoints.yml index b1748c15fef..fff3420477f 100644 --- a/bundle/direct/dresources/configs/vector_search_endpoints.yml +++ b/bundle/direct/dresources/configs/vector_search_endpoints.yml @@ -1,10 +1,6 @@ fields: # The endpoint API has no rename; the endpoint is fetched by name. - name: - action: id - endpoint_type: - action: immutable - # 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. + name: [id] + endpoint_type: [immutable] budget_policy_id: ignore_remote: effective_vs_requested diff --git a/bundle/direct/dresources/configs/vector_search_indexes.yml b/bundle/direct/dresources/configs/vector_search_indexes.yml index 15529eb87c3..704744a5edf 100644 --- a/bundle/direct/dresources/configs/vector_search_indexes.yml +++ b/bundle/direct/dresources/configs/vector_search_indexes.yml @@ -1,26 +1,10 @@ fields: - name: - action: id - # The index API has no rename or update path, so every config change - # has to go through delete + create. - endpoint_name: - action: immutable - index_type: - action: immutable - index_subtype: - action: immutable - primary_key: - action: immutable - delta_sync_index_spec: - action: immutable - direct_access_index_spec: - action: immutable - # 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. + name: [id] + endpoint_name: [immutable] + index_type: [immutable] + index_subtype: [immutable] + primary_key: [immutable] + delta_sync_index_spec: [immutable] + direct_access_index_spec: [immutable] direct_access_index_spec.schema_json: ignore_remote: normalized_by_backend diff --git a/bundle/direct/dresources/configs/volumes.yml b/bundle/direct/dresources/configs/volumes.yml index b01cffc5d7c..a5d878cf4fb 100644 --- a/bundle/direct/dresources/configs/volumes.yml +++ b/bundle/direct/dresources/configs/volumes.yml @@ -1,14 +1,6 @@ fields: - catalog_name: - action: id - schema_name: - action: id - # 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 - storage_location: - action: immutable - compare: trim_slash - volume_type: - action: immutable - name: - action: id_renameable + catalog_name: [id] + schema_name: [id] + storage_location: [immutable, trim_slash] + volume_type: [immutable] + name: [renameable_id] diff --git a/bundle/direct/dresources/field_policy.go b/bundle/direct/dresources/field_policy.go index c0f76b310de..5acdee4dc76 100644 --- a/bundle/direct/dresources/field_policy.go +++ b/bundle/direct/dresources/field_policy.go @@ -32,75 +32,164 @@ type FieldPolicyConfig struct { Fields map[string]FieldPolicy `yaml:"fields"` } -// FieldPolicy is one field's lifecycle policy: a single Action (the local-change -// decision) plus independent modifiers on other axes (remote drift, comparison, -// backend defaults, storage, redaction, reference resolution). The modifiers never -// conflict with the Action, so a field cannot be handed contradictory behaviours. +// FieldPolicy is one field's lifecycle policy. It is authored as a list of behaviour +// tokens (the common case) or, when a behaviour needs a value, as a map: +// +// volume_type: [immutable] +// storage_root: [immutable, trim_slash] +// create_time: [mutable_output] +// name: [immutable_output] +// value: { ignore_remote: etag_based, hashed: true } +// replace_existing: { ignore_local: input_only } +// +// A field carries at most one local-change action (id / renameable_id / immutable / +// ignore_local); the rest are independent modifiers, so nothing contradicts. type FieldPolicy struct { - // Spec lists the OpenAPI-declared behaviours for a field (one or a list of - // immutable / input_only / output_only). Each implies its own mechanism and a - // "spec:" reason: immutable recreates, input_only/output_only skip - // remote drift. This is what the generated configs emit; hand-written configs use - // Action / IgnoreRemote instead (their reasons are manual, not spec-derived). - Spec specBehaviours `yaml:"spec,omitempty"` - - // Action is the local-change decision. Empty = a normal in-place update. - Action action `yaml:"action,omitempty"` - - // Reason overrides the reason surfaced in the plan for Action. id/id_renameable/ - // immutable supply a default; ignore_local requires it (the justification varies). - Reason string `yaml:"reason,omitempty"` - - // IgnoreRemote, when non-empty, skips remote-only drift on the field; its value is - // the reason (which varies: managed, input_only, etag_based, spec:input_only, ...). - IgnoreRemote string `yaml:"ignore_remote,omitempty"` - - // Compare customizes equality before the action is decided. "" | "trim_slash". - Compare string `yaml:"compare,omitempty"` - - // BackendDefault skips a config-absent value the backend populated. BackendValues - // constrains it to specific remote values and implies BackendDefault. - BackendDefault bool `yaml:"backend_default,omitempty"` - BackendValues []any `yaml:"backend_values,omitempty"` + // Action is the local-change decision (from an id/renameable_id/immutable token, or + // the ignore_local map key). Empty means a normal in-place update. + Action action + + // Reason is the plan reason for ignore_local (the only action whose reason is not + // derived); other actions and the modifiers derive theirs. + Reason string + + // IgnoreRemote, when non-empty, skips remote-only drift; its value is the reason. + // From the input_only / mutable_output tokens (reason input_only / output_only, + // which Lower prefixes with "spec:" in generated configs) or the ignore_remote map + // key (a custom reason: managed, etag_based, auto, ...). + IgnoreRemote string + + // StableOutput marks a backend-assigned output that never changes (the + // immutable_output token): reference-safe, so ${resources.X.} resolves from + // the remote cache during an in-place update. See StableOutputFields in config.go. + StableOutput bool + + Compare string // trim_slash token -> normalize_slash + Hashed bool + Sensitive bool + BackendDefault bool + BackendValues []any + RemoteAdditionsWhen *structpath.PathNode +} - // Hashed persists the value to state as a content hash instead of the raw value. - Hashed bool `yaml:"hashed,omitempty"` +// Behaviour tokens (list form) and the map keys they mirror. +const ( + tokenID = "id" + tokenRenameableID = "renameable_id" + tokenImmutable = "immutable" + tokenInputOnly = "input_only" + tokenMutableOutput = "mutable_output" + tokenImmutableOutput = "immutable_output" + tokenTrimSlash = "trim_slash" + tokenHashed = "hashed" + tokenSensitive = "sensitive" + tokenBackendDefault = "backend_default" + + // Base reasons the tokens imply; Lower prefixes "spec:" in generated configs. + reasonIDField = "id_field" + reasonIDChanges = "id_changes" + reasonImmutable = "immutable" + reasonInputOnly = "input_only" + reasonOutputOnly = "output_only" + reasonTrimSlash = "uc_strips_trailing_slash" + compareTrimSlash = "trim_slash" +) - // Sensitive marks the field for redaction in logs and state. - Sensitive bool `yaml:"sensitive,omitempty"` +func (p *FieldPolicy) applyToken(t string) error { + switch t { + case tokenID: + p.Action = actionID + case tokenRenameableID: + p.Action = actionRenameableID + case tokenImmutable: + p.Action = actionImmutable + case tokenInputOnly: + p.IgnoreRemote = reasonInputOnly + case tokenMutableOutput: + p.IgnoreRemote = reasonOutputOnly + case tokenImmutableOutput: + p.StableOutput = true + case tokenTrimSlash: + p.Compare = compareTrimSlash + case tokenHashed: + p.Hashed = true + case tokenSensitive: + p.Sensitive = true + case tokenBackendDefault: + p.BackendDefault = true + default: + return fmt.Errorf("unknown behaviour %q", t) + } + return nil +} - // StableOutput marks an output-only field the backend assigns at creation and never - // changes, so a cross-resource reference resolves from the remote cache during a - // keeps-ID update. See StableOutputFields in config.go. - StableOutput bool `yaml:"stable_output,omitempty"` +// UnmarshalYAML accepts the list form (a sequence of tokens) or the map form (keys that +// carry a value). A duplicate field key is already a YAML error at the map above. +func (p *FieldPolicy) UnmarshalYAML(unmarshal func(any) error) error { + var tokens []string + if err := unmarshal(&tokens); err == nil { + for _, t := range tokens { + if err := p.applyToken(t); err != nil { + return err + } + } + return nil + } - // RemoteAdditionsWhen gates ignore_remote_additions on this object (its contents are - // backend-co-owned when the named sibling field is set). - RemoteAdditionsWhen *structpath.PathNode `yaml:"remote_additions_when,omitempty"` + var m struct { + Action action `yaml:"action"` + Reason string `yaml:"reason"` + IgnoreRemote string `yaml:"ignore_remote"` + IgnoreLocal string `yaml:"ignore_local"` + Compare string `yaml:"compare"` + Hashed bool `yaml:"hashed"` + Sensitive bool `yaml:"sensitive"` + StableOutput bool `yaml:"stable_output"` + BackendDefault bool `yaml:"backend_default"` + BackendValues []any `yaml:"backend_values"` + RemoteAdditionsWhen *structpath.PathNode `yaml:"remote_additions_when"` + } + if err := unmarshal(&m); err != nil { + return err + } + p.Action = m.Action + p.Reason = m.Reason + p.IgnoreRemote = m.IgnoreRemote + p.Compare = m.Compare + p.Hashed = m.Hashed + p.Sensitive = m.Sensitive + p.StableOutput = m.StableOutput + p.BackendDefault = m.BackendDefault + p.BackendValues = m.BackendValues + p.RemoteAdditionsWhen = m.RemoteAdditionsWhen + if m.IgnoreLocal != "" { + p.Action = actionIgnoreLocal + p.Reason = m.IgnoreLocal + } + return nil } -// action is the local-change decision for a field. Its UnmarshalYAML validates against -// validActions, so an unknown action is a parse-time error (with file/line). +// action is the local-change decision for a field. type action string const ( actionMutable action = "" // default: a normal in-place update actionImmutable action = "immutable" // recreate on local change actionID action = "id" // provided id: recreate on change, skip remote drift - actionIDRenameable action = "id_renameable" // updatable id: rename in place on change + actionRenameableID action = "renameable_id" // updatable id: rename in place on change actionIgnoreLocal action = "ignore_local" // skip local edits ) -// validActions is the single source of truth for which actions parse. lowerAction must -// handle each; TestFieldPolicyEveryActionLowers keeps the two in sync. var validActions = map[action]bool{ actionMutable: true, actionImmutable: true, actionID: true, - actionIDRenameable: true, + actionRenameableID: true, actionIgnoreLocal: true, } +// UnmarshalYAML validates the resource-root action (map key form); field tokens are +// validated in applyToken. func (a *action) UnmarshalYAML(unmarshal func(any) error) error { var s string if err := unmarshal(&s); err != nil { @@ -113,72 +202,27 @@ func (a *action) UnmarshalYAML(unmarshal func(any) error) error { return nil } -// OpenAPI behaviours a spec entry may carry, each mapping to a mechanism and a -// "spec:" reason in lowerSpec. -const ( - specImmutable = "immutable" - specInputOnly = "input_only" - specOutputOnly = "output_only" -) - -var validSpecBehaviours = map[string]bool{specImmutable: true, specInputOnly: true, specOutputOnly: true} - -// specBehaviours accepts either a single behaviour ("spec: output_only") or a list -// ("spec: [immutable, input_only]"), and rejects unknown behaviours at parse time. -type specBehaviours []string - -func (s *specBehaviours) UnmarshalYAML(unmarshal func(any) error) error { - var one string - if err := unmarshal(&one); err == nil { - *s = specBehaviours{one} - } else { - var many []string - if err := unmarshal(&many); err != nil { - return err - } - *s = many - } - for _, b := range *s { - if !validSpecBehaviours[b] { - return fmt.Errorf("unknown spec behaviour %q", b) - } - } - return nil -} - -const ( - compareTrimSlash = "trim_slash" - - // Reason strings the flat format spells out per entry; the field-keyed format - // derives them from the action / modifier so the common case needs no reason. - reasonIDField = "id_field" - reasonIDChanges = "id_changes" - reasonImmutable = "immutable" - reasonTrimSlash = "uc_strips_trailing_slash" -) - // Lower translates the field-keyed config into the flat ResourceLifecycleConfig the -// planner and every other consumer already use, so wiring in this format changes -// nothing downstream. -func (c FieldPolicyConfig) Lower() (ResourceLifecycleConfig, error) { +// planner and every other consumer already use. generated marks a *.generated.yml file, +// whose reasons are all spec-derived and so carry the "spec:" prefix. +func (c FieldPolicyConfig) Lower(generated bool) (ResourceLifecycleConfig, error) { var out ResourceLifecycleConfig // Resource-root rules (field omitted); mirror the flat loader's nil Field. - if err := lowerAction(&out, nil, c.Action, c.Reason); err != nil { + if err := lowerAction(&out, nil, c.Action, c.Reason, generated); err != nil { return out, err } if c.RemoteAdditionsWhen != nil { out.IgnoreRemoteAdditions = append(out.IgnoreRemoteAdditions, RemoteAdditionRule{Field: nil, WhenSet: c.RemoteAdditionsWhen}) } - // Deterministic order keeps the lowered lists stable for goldens and diffs. for _, field := range slices.Sorted(maps.Keys(c.Fields)) { p := c.Fields[field] pattern, err := structpath.ParsePattern(field) if err != nil { return out, fmt.Errorf("field %q: %w", field, err) } - if err := lowerField(&out, pattern, field, p); err != nil { + if err := lowerField(&out, pattern, field, p, generated); err != nil { return out, fmt.Errorf("field %q: %w", field, err) } } @@ -186,8 +230,15 @@ func (c FieldPolicyConfig) Lower() (ResourceLifecycleConfig, error) { return out, nil } +func specReason(base string, generated bool) string { + if generated { + return "spec:" + base + } + return base +} + // lowerAction appends the flat rule for one local-change action on pattern (nil = root). -func lowerAction(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, act action, reason string) error { +func lowerAction(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, act action, reason string, generated bool) error { switch act { case actionMutable: if reason != "" { @@ -195,47 +246,29 @@ func lowerAction(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, } case actionID: out.ProvidedIDFields = append(out.ProvidedIDFields, FieldRule{Field: pattern, Reason: reasonOr(reason, reasonIDField)}) - case actionIDRenameable: + case actionRenameableID: out.UpdatableIDFields = append(out.UpdatableIDFields, FieldRule{Field: pattern, Reason: reasonOr(reason, reasonIDChanges)}) case actionImmutable: - out.RecreateOnChanges = append(out.RecreateOnChanges, FieldRule{Field: pattern, Reason: reasonOr(reason, reasonImmutable)}) + out.RecreateOnChanges = append(out.RecreateOnChanges, FieldRule{Field: pattern, Reason: reasonOr(reason, specReason(reasonImmutable, generated))}) case actionIgnoreLocal: out.IgnoreLocalChanges = append(out.IgnoreLocalChanges, FieldRule{Field: pattern, Reason: reason}) default: - // Unreachable via YAML (UnmarshalYAML validates); guards a value added to - // validActions but not handled here. + // Unreachable: UnmarshalYAML validates actions against validActions. return fmt.Errorf("unhandled action %q", act) } return nil } -// lowerSpec appends the flat rules for a field's OpenAPI-declared behaviours, each with -// a "spec:" reason: immutable recreates, input_only/output_only skip remote drift. -func lowerSpec(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, spec specBehaviours) error { - for _, b := range spec { - switch b { - case specImmutable: - out.RecreateOnChanges = append(out.RecreateOnChanges, FieldRule{Field: pattern, Reason: "spec:" + specImmutable}) - case specInputOnly, specOutputOnly: - out.IgnoreRemoteChanges = append(out.IgnoreRemoteChanges, FieldRule{Field: pattern, Reason: "spec:" + b}) - default: - // Unreachable via YAML (UnmarshalYAML validates against validSpecBehaviours). - return fmt.Errorf("unhandled spec behaviour %q", b) - } - } - return nil -} - -func lowerField(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, field string, p FieldPolicy) error { - if err := lowerAction(out, pattern, p.Action, p.Reason); err != nil { - return err - } - if err := lowerSpec(out, pattern, p.Spec); err != nil { +func lowerField(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, field string, p FieldPolicy, generated bool) error { + if err := lowerAction(out, pattern, p.Action, p.Reason, generated); err != nil { return err } if p.IgnoreRemote != "" { - out.IgnoreRemoteChanges = append(out.IgnoreRemoteChanges, FieldRule{Field: pattern, Reason: p.IgnoreRemote}) + out.IgnoreRemoteChanges = append(out.IgnoreRemoteChanges, FieldRule{Field: pattern, Reason: specReason(p.IgnoreRemote, generated)}) + } + if p.StableOutput { + out.StableOutputFields = append(out.StableOutputFields, FieldRule{Field: pattern, Reason: ""}) } switch p.Compare { @@ -260,9 +293,6 @@ func lowerField(out *ResourceLifecycleConfig, pattern *structpath.PatternNode, f if p.Sensitive { out.SensitiveFields = append(out.SensitiveFields, FieldRule{Field: pattern, Reason: ""}) } - if p.StableOutput { - out.StableOutputFields = append(out.StableOutputFields, FieldRule{Field: pattern, Reason: ""}) - } if p.RemoteAdditionsWhen != nil { out.IgnoreRemoteAdditions = append(out.IgnoreRemoteAdditions, RemoteAdditionRule{Field: pattern, WhenSet: p.RemoteAdditionsWhen}) } diff --git a/bundle/direct/dresources/field_policy_test.go b/bundle/direct/dresources/field_policy_test.go index ede8b8bf8b2..d426f082980 100644 --- a/bundle/direct/dresources/field_policy_test.go +++ b/bundle/direct/dresources/field_policy_test.go @@ -13,65 +13,65 @@ import ( // only one policy, and a repeated key is a load error, not a silently-shadowed rule. func TestFieldPolicyRejectsDuplicateField(t *testing.T) { var fpc FieldPolicyConfig - err := yaml.Unmarshal([]byte("fields:\n name: { action: id }\n name: { action: immutable }\n"), &fpc) + err := yaml.Unmarshal([]byte("fields:\n name: [id]\n name: [immutable]\n"), &fpc) require.Error(t, err) assert.Contains(t, err.Error(), `already defined`) } -// TestFieldPolicyRejectsUnknownAction proves the action enum is enforced by the -// parser: an unknown value fails at yaml.Unmarshal, not later in Lower. -func TestFieldPolicyRejectsUnknownAction(t *testing.T) { +// TestFieldPolicyRejectsUnknownToken proves the behaviour vocabulary is enforced by the parser. +func TestFieldPolicyRejectsUnknownToken(t *testing.T) { var fpc FieldPolicyConfig - err := yaml.Unmarshal([]byte("fields:\n name: { action: teleport }\n"), &fpc) + err := yaml.Unmarshal([]byte("fields:\n name: [teleport]\n"), &fpc) require.Error(t, err) - assert.Contains(t, err.Error(), `unknown action "teleport"`) + assert.Contains(t, err.Error(), `unknown behaviour "teleport"`) } -// TestFieldPolicyEveryActionLowers keeps validActions and the lowerAction switch in -// sync: every value the parser accepts must lower without hitting the default guard. -func TestFieldPolicyEveryActionLowers(t *testing.T) { - for a := range validActions { - _, err := FieldPolicyConfig{Fields: map[string]FieldPolicy{"name": {Action: a}}}.Lower() - require.NoErrorf(t, err, "action %q", a) +func rulesOf(rs []FieldRule) []string { + var out []string + for _, r := range rs { + out = append(out, r.Field.String()+"|"+r.Reason) } + return out } -// TestFieldPolicyRejectsUnknownSpec proves the spec behaviour set is enforced by the parser. -func TestFieldPolicyRejectsUnknownSpec(t *testing.T) { - var fpc FieldPolicyConfig - err := yaml.Unmarshal([]byte("fields:\n name: { spec: writeonce }\n"), &fpc) - require.Error(t, err) - assert.Contains(t, err.Error(), `unknown spec behaviour "writeonce"`) +// TestFieldPolicyLowerVolumes pins the list-form lowering of a representative resource that +// exercises id, renameable_id, immutable, and the trim_slash comparison modifier. +func TestFieldPolicyLowerVolumes(t *testing.T) { + c := GetResourceConfig("volumes") + assert.ElementsMatch(t, []string{"catalog_name|id_field", "schema_name|id_field"}, rulesOf(c.ProvidedIDFields)) + assert.ElementsMatch(t, []string{"name|id_changes"}, rulesOf(c.UpdatableIDFields)) + assert.ElementsMatch(t, []string{"storage_location|immutable", "volume_type|immutable"}, rulesOf(c.RecreateOnChanges)) + assert.ElementsMatch(t, []string{"storage_location|uc_strips_trailing_slash"}, rulesOf(c.NormalizeSlash)) } -// TestFieldPolicySpecLowers pins each OpenAPI behaviour to its mechanism and spec: reason. -func TestFieldPolicySpecLowers(t *testing.T) { - c, err := FieldPolicyConfig{Fields: map[string]FieldPolicy{ - "a": {Spec: specBehaviours{specImmutable, specInputOnly}}, - "b": {Spec: specBehaviours{specOutputOnly}}, - }}.Lower() - require.NoError(t, err) - assert.Equal(t, "spec:immutable", c.RecreateOnChanges[0].Reason) - var remote []string - for _, r := range c.IgnoreRemoteChanges { - remote = append(remote, r.Field.String()+"|"+r.Reason) +// TestFieldPolicyTokensLower checks each behaviour token maps to the right flat rule, and +// that spec-derived reasons pick up the "spec:" prefix only in generated configs. +func TestFieldPolicyTokensLower(t *testing.T) { + parse := func(yml string, generated bool) ResourceLifecycleConfig { + var fpc FieldPolicyConfig + require.NoError(t, yaml.Unmarshal([]byte(yml), &fpc)) + rc, err := fpc.Lower(generated) + require.NoError(t, err) + return rc } - assert.ElementsMatch(t, []string{"a|spec:input_only", "b|spec:output_only"}, remote) + + hand := parse("fields:\n a: [immutable, input_only]\n b: [mutable_output]\n c: [immutable_output]\n", false) + assert.ElementsMatch(t, []string{"a|immutable"}, rulesOf(hand.RecreateOnChanges)) + assert.ElementsMatch(t, []string{"a|input_only", "b|output_only"}, rulesOf(hand.IgnoreRemoteChanges)) + assert.ElementsMatch(t, []string{"c|"}, rulesOf(hand.StableOutputFields)) + + gen := parse("fields:\n a: [immutable, input_only]\n b: [mutable_output]\n", true) + assert.ElementsMatch(t, []string{"a|spec:immutable"}, rulesOf(gen.RecreateOnChanges)) + assert.ElementsMatch(t, []string{"a|spec:input_only", "b|spec:output_only"}, rulesOf(gen.IgnoreRemoteChanges)) } -// TestFieldPolicyLowerVolumes pins the lowering of a representative resource that -// exercises id, id_renameable, immutable, and the trim_slash comparison modifier. -func TestFieldPolicyLowerVolumes(t *testing.T) { - rules := func(rs []FieldRule) []string { - var out []string - for _, r := range rs { - out = append(out, r.Field.String()+"|"+r.Reason) - } - return out - } - c := GetResourceConfig("volumes") - assert.ElementsMatch(t, []string{"catalog_name|id_field", "schema_name|id_field"}, rules(c.ProvidedIDFields)) - assert.ElementsMatch(t, []string{"name|id_changes"}, rules(c.UpdatableIDFields)) - assert.ElementsMatch(t, []string{"storage_location|immutable", "volume_type|immutable"}, rules(c.RecreateOnChanges)) - assert.ElementsMatch(t, []string{"storage_location|uc_strips_trailing_slash"}, rules(c.NormalizeSlash)) +// TestFieldPolicyMapForm checks the map form for value-bearing behaviours. +func TestFieldPolicyMapForm(t *testing.T) { + var fpc FieldPolicyConfig + require.NoError(t, yaml.Unmarshal([]byte("fields:\n v: { ignore_remote: etag_based, hashed: true }\n r: { ignore_local: input_only }\n"), &fpc)) + rc, err := fpc.Lower(false) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"v|etag_based"}, rulesOf(rc.IgnoreRemoteChanges)) + assert.ElementsMatch(t, []string{"r|input_only"}, rulesOf(rc.IgnoreLocalChanges)) + assert.Equal(t, []string{"v"}, rc.HashedFields) } diff --git a/bundle/direct/tools/generate_resources.py b/bundle/direct/tools/generate_resources.py index c744dbca5b4..4a96aa07f24 100644 --- a/bundle/direct/tools/generate_resources.py +++ b/bundle/direct/tools/generate_resources.py @@ -152,12 +152,13 @@ def quote_key(field): def generate(behaviors): - """Render one resource's field behaviors in the field-keyed format, or "" if none. + """Render one resource's field behaviors as a field -> [tokens] list, or "" if none. - Each field lists its OpenAPI behaviours under `spec` (immutable / input_only / - output_only); the loader maps each to its mechanism and a spec: reason. - immutable and the remote-ignored behaviours are filtered for prefixes independently, - matching the flat recreate_on_changes / ignore_remote_changes lists they replace. + Tokens: immutable (recreate), input_only / mutable_output (skip remote drift), + immutable_output (a backend-assigned value that never changes -> stable/reference-safe; + an output field can't recreate, so IMMUTABLE+OUTPUT_ONLY folds into this one token). + The immutable and remote-ignored behaviours are prefix-filtered independently, matching + the flat recreate_on_changes / ignore_remote_changes lists they replace. """ ignore_remote, recreate = [], [] for field, fb in sorted(behaviors.items()): @@ -168,24 +169,30 @@ def generate(behaviors): if "IMMUTABLE" in fb: recreate.append((field, "IMMUTABLE")) - ignore_remote = filter_prefixes(ignore_remote) - recreate = filter_prefixes(recreate) + remote = dict(filter_prefixes(ignore_remote)) # field -> OUTPUT_ONLY | INPUT_ONLY + immutable = {f for f, _ in filter_prefixes(recreate)} - if not ignore_remote and not recreate: + if not remote and not immutable: return "" - spec = {} # field -> ordered list of behaviours - for field, _ in recreate: - spec.setdefault(field, []).append("immutable") - for field, behavior in ignore_remote: - spec.setdefault(field, []).append(behavior.lower()) + tokens = {} + for field in sorted(set(remote) | immutable): + rem = remote.get(field) + if field in immutable and rem == "OUTPUT_ONLY": + tokens[field] = ["immutable_output"] # immutable server-set value: stable, not recreate + continue + toks = [] + if field in immutable: + toks.append("immutable") + if rem == "INPUT_ONLY": + toks.append("input_only") + elif rem == "OUTPUT_ONLY": + toks.append("mutable_output") + tokens[field] = toks lines = [HEADER, "", "fields:"] - for field in sorted(spec): - behaviours = spec[field] - value = behaviours[0] if len(behaviours) == 1 else "[" + ", ".join(behaviours) + "]" - lines.append(f" {quote_key(field)}:") - lines.append(f" spec: {value}") + for field in sorted(tokens): + lines.append(f" {quote_key(field)}: [{', '.join(tokens[field])}]") return "\n".join(lines) + "\n"