diff --git a/bundle/direct/dresources/config.go b/bundle/direct/dresources/config.go index cc54d60a678..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) @@ -186,8 +187,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(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 8b9e18006be..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. -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: [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 5e204b2a15f..c0545ae4b59 100644 --- a/bundle/direct/dresources/configs/apps.generated.yml +++ b/bundle/direct/dresources/configs/apps.generated.yml @@ -1,54 +1,28 @@ # 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: [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 a6725316bec..d5014e67577 100644 --- a/bundle/direct/dresources/configs/apps.yml +++ b/bundle/direct/dresources/configs/apps.yml @@ -1,20 +1,8 @@ -provided_id_fields: - - field: name - reason: id_field -backend_defaults: - # Backend sets it "MEDIUM" when not specified in the config - - field: compute_size - # Backend enables token forwarding and reports true even when the config omits it, - # so an omitted field would otherwise plan an update on every deploy. An explicit - # value in the config still diffs normally. - - field: forward_user_access_token - # lifecycle.started is derived from remote compute status in RemapState, so the - # remote side always has a value. When the user omits lifecycle from config, - # both old and new are nil and backend_defaults correctly skips the remote value. - # When the user explicitly sets lifecycle.started, old/new are non-nil and normal - # drift detection applies (e.g. detecting out-of-band stop). - - field: lifecycle - - field: lifecycle.started -ignore_remote_changes: - - field: space # This field is not yet supported by Update APIs but exposed in the API spec. TODO: fix when update APIs supports it. - reason: managed +fields: + 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 143a74c447d..78aa49ba996 100644 --- a/bundle/direct/dresources/configs/catalogs.yml +++ b/bundle/direct/dresources/configs/catalogs.yml @@ -1,21 +1,10 @@ -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: - # UC auto-populates unity.catalog.managed..defaults.* keys after create. - - field: properties['unity.catalog.managed.*.defaults.*'] + 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 6f2ee1e1b9b..3c4b0ce9618 100644 --- a/bundle/direct/dresources/configs/cluster_policies.yml +++ b/bundle/direct/dresources/configs/cluster_policies.yml @@ -1,6 +1,6 @@ -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] diff --git a/bundle/direct/dresources/configs/clusters.yml b/bundle/direct/dresources/configs/clusters.yml index b19d9c0483c..a2093bba492 100644 --- a/bundle/direct/dresources/configs/clusters.yml +++ b/bundle/direct/dresources/configs/clusters.yml @@ -5,53 +5,28 @@ # 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 - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L331 - # s.SchemaPath("enable_elastic_disk").SetComputed() - - field: enable_elastic_disk - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L332 - # s.SchemaPath("enable_local_disk_encryption").SetComputed() - - field: enable_local_disk_encryption - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L333 - # s.SchemaPath("node_type_id").SetComputed() - - field: node_type_id - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L334 - # s.SchemaPath("driver_node_type_id").SetComputed() - - field: driver_node_type_id - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L335 - # s.SchemaPath("driver_instance_pool_id").SetComputed() - - field: driver_instance_pool_id - - # Terraform currently does not do this, but it is a field with backend default. - # See https://github.com/databricks/cli/issues/4418 - - field: single_user_name - -# We have custom handler for this in cluster.go -# https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/clusters/resource_cluster.go#L109-L118 -# DataSecurityModeDiffSuppressFunc: suppress when old != "" && new == "" -#- field: data_security_mode + 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 936b6e7f1c6..d096a326e6d 100644 --- a/bundle/direct/dresources/configs/dashboards.generated.yml +++ b/bundle/direct/dresources/configs/dashboards.generated.yml @@ -1,17 +1,9 @@ # Generated, do not edit. -recreate_on_changes: - - field: parent_path - reason: spec:immutable - -ignore_remote_changes: - - field: create_time - reason: spec:output_only - - field: dashboard_id - reason: spec:output_only - - field: lifecycle_state - reason: spec:output_only - - field: path - reason: spec:output_only - - field: update_time - reason: spec:output_only +fields: + 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 f59f0e38208..a6701843bc6 100644 --- a/bundle/direct/dresources/configs/dashboards.yml +++ b/bundle/direct/dresources/configs/dashboards.yml @@ -1,17 +1,12 @@ -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: [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 5eddcfd2a48..32f88ddf384 100644 --- a/bundle/direct/dresources/configs/database_catalogs.generated.yml +++ b/bundle/direct/dresources/configs/database_catalogs.generated.yml @@ -1,8 +1,5 @@ # 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: [input_only] + uid: [mutable_output] diff --git a/bundle/direct/dresources/configs/database_catalogs.yml b/bundle/direct/dresources/configs/database_catalogs.yml index 26eda6c377f..0f36fcdd6a9 100644 --- a/bundle/direct/dresources/configs/database_catalogs.yml +++ b/bundle/direct/dresources/configs/database_catalogs.yml @@ -1,11 +1,9 @@ -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: [id] diff --git a/bundle/direct/dresources/configs/database_instances.generated.yml b/bundle/direct/dresources/configs/database_instances.generated.yml index 66fdebab914..d089d36eeec 100644 --- a/bundle/direct/dresources/configs/database_instances.generated.yml +++ b/bundle/direct/dresources/configs/database_instances.generated.yml @@ -1,78 +1,39 @@ # Generated, do not edit. -recreate_on_changes: - - field: parent_instance_ref - reason: spec:immutable - -ignore_remote_changes: - - field: custom_tags - reason: spec:input_only - - field: custom_tags[*].key - reason: spec:input_only - - field: custom_tags[*].value - reason: spec:input_only - - field: enable_pg_native_login - reason: spec:input_only - - field: enable_readable_secondaries - reason: spec:input_only - - field: node_count - reason: spec:input_only - - field: parent_instance_ref.lsn - reason: spec:input_only - - field: retention_window_in_days - reason: spec:input_only - - field: stopped - reason: spec:input_only - - field: usage_policy_id - reason: spec:input_only - - - field: child_instance_refs - reason: spec:output_only - - field: child_instance_refs[*].branch_time - reason: spec:output_only - - field: child_instance_refs[*].effective_lsn - reason: spec:output_only - - field: child_instance_refs[*].lsn - reason: spec:output_only - - field: child_instance_refs[*].name - reason: spec:output_only - - field: child_instance_refs[*].uid - reason: spec:output_only - - field: creation_time - reason: spec:output_only - - field: creator - reason: spec:output_only - - field: effective_capacity - reason: spec:output_only - - field: effective_custom_tags - reason: spec:output_only - - field: effective_custom_tags[*].key - reason: spec:output_only - - field: effective_custom_tags[*].value - reason: spec:output_only - - field: effective_enable_pg_native_login - reason: spec:output_only - - field: effective_enable_readable_secondaries - reason: spec:output_only - - field: effective_node_count - reason: spec:output_only - - field: effective_retention_window_in_days - reason: spec:output_only - - field: effective_stopped - reason: spec:output_only - - field: effective_usage_policy_id - reason: spec:output_only - - field: parent_instance_ref.effective_lsn - reason: spec:output_only - - field: parent_instance_ref.uid - reason: spec:output_only - - field: pg_version - reason: spec:output_only - - field: read_only_dns - reason: spec:output_only - - field: read_write_dns - reason: spec:output_only - - field: state - reason: spec:output_only - - field: uid - reason: spec:output_only +fields: + 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 b6ab8dc9e8a..e4e7e46000c 100644 --- a/bundle/direct/dresources/configs/database_instances.yml +++ b/bundle/direct/dresources/configs/database_instances.yml @@ -1,3 +1,2 @@ -provided_id_fields: - - field: name - reason: id_field +fields: + name: [id] diff --git a/bundle/direct/dresources/configs/experiments.generated.yml b/bundle/direct/dresources/configs/experiments.generated.yml index afd372d58f2..ad4f8b369ae 100644 --- a/bundle/direct/dresources/configs/experiments.generated.yml +++ b/bundle/direct/dresources/configs/experiments.generated.yml @@ -1,9 +1,5 @@ # Generated, do not edit. -recreate_on_changes: - - field: trace_location - reason: spec:immutable - -ignore_remote_changes: - - field: trace_location.uc_trace_location.effective_table_prefix - reason: spec:output_only +fields: + 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 6a261514379..e9019066c32 100644 --- a/bundle/direct/dresources/configs/experiments.yml +++ b/bundle/direct/dresources/configs/experiments.yml @@ -1,11 +1,7 @@ # 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: - # Tags updates are not supported by TF. This mirrors that behaviour. - - field: tags - reason: terraform_compat -ignore_local_changes: - - field: tags +fields: + artifact_location: [immutable] + 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..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. -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: [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 55612ef68a6..efaaf5ec294 100644 --- a/bundle/direct/dresources/configs/external_locations.yml +++ b/bundle/direct/dresources/configs/external_locations.yml @@ -1,10 +1,5 @@ -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: [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 d9c3e63803d..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. -ignore_remote_changes: - - field: etag - reason: spec:output_only +fields: + etag: [mutable_output] 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..b45af5d5181 100644 --- a/bundle/direct/dresources/configs/instance_pools.yml +++ b/bundle/direct/dresources/configs/instance_pools.yml @@ -1,24 +1,19 @@ # 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: - # Defaults to true server-side. - - field: enable_elastic_disk - # Backend applies a default of 60 minutes when the field is omitted. - - field: idle_instance_autotermination_minutes - values: [60] - # GCP seeds max_capacity (1000) when omitted; unconstrained as the default is cloud-dependent. - - field: max_capacity + 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] 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..f6fcff96a27 100644 --- a/bundle/direct/dresources/configs/jobs.yml +++ b/bundle/direct/dresources/configs/jobs.yml @@ -4,143 +4,90 @@ # 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 - - # Same field via the multi-trigger list. Untested: creating a job with - # "triggers" fails with 404 FEATURE_DISABLED ("The 'triggers' field is in - # private preview and is not enabled for this workspace"), so there is no - # invariant config for it. Both shapes carry the same - # TableUpdateTriggerConfiguration message and the backend builds it from - # the same expression on read, so the field is dropped here too. - - field: triggers[*].table_update.condition - reason: input_only - -ignore_remote_additions: - # A cluster policy supplies cluster settings server-side: "fixed" elements always, - # "defaultValue" elements when the request sets apply_policy_default_values. The bundle - # is not the source for those, so the remote spec is legitimately a superset of what the - # config declares and an added field is not drift. Measured backend behavior is pinned by - # acceptance/bundle/resources/jobs/cluster_policy (fixed_addition, default_flag). - # https://github.com/databricks/cli/issues/5179 - # https://github.com/databricks/cli/issues/6512 - - field: tasks[*].new_cluster - when_set: policy_id - - field: tasks[*].for_each_task.task.new_cluster - when_set: policy_id - - field: job_clusters[*].new_cluster - when_set: policy_id - -backend_defaults: + 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: + 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 - - # Same as clusters.enable_local_disk_encryption — see clusters/resource_cluster.go#L332 - # s.SchemaPath("enable_local_disk_encryption").SetComputed() - - field: tasks[*].new_cluster.enable_local_disk_encryption - - field: tasks[*].for_each_task.task.new_cluster.enable_local_disk_encryption - - field: job_clusters[*].new_cluster.enable_local_disk_encryption - - # Same as clusters.node_type_id — see clusters/resource_cluster.go#L333 - # s.SchemaPath("node_type_id").SetComputed() - - field: tasks[*].new_cluster.node_type_id - - field: tasks[*].for_each_task.task.new_cluster.node_type_id - - field: job_clusters[*].new_cluster.node_type_id - - # Same as clusters.driver_node_type_id — see clusters/resource_cluster.go#L334 - # s.SchemaPath("driver_node_type_id").SetComputed() - - field: tasks[*].new_cluster.driver_node_type_id - - field: tasks[*].for_each_task.task.new_cluster.driver_node_type_id - - field: job_clusters[*].new_cluster.driver_node_type_id - - # Same as clusters.driver_instance_pool_id — see clusters/resource_cluster.go#L335 - # s.SchemaPath("driver_instance_pool_id").SetComputed() - - field: tasks[*].new_cluster.driver_instance_pool_id - - field: tasks[*].for_each_task.task.new_cluster.driver_instance_pool_id - - field: job_clusters[*].new_cluster.driver_instance_pool_id - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L531 - # s.SchemaPath("format").SetComputed() - - field: format - values: ["MULTI_TASK", "SINGLE_TASK"] - + 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": + 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"] - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L527 - # s.SchemaPath("run_as").SetComputed() - - field: run_as - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/jobs/resource_job.go#L521-L524 - # s.SchemaPath("task", "notebook_task", "source").SetSuppressDiff() - # s.SchemaPath("task", "spark_python_task", "source").SetSuppressDiff() - # s.SchemaPath("task", "sql_task", "file", "source").SetSuppressDiff() - # s.SchemaPath("task", "dbt_task", "source").SetSuppressDiff() - - field: tasks[*].notebook_task.source - - field: tasks[*].for_each_task.task.notebook_task.source - - field: tasks[*].spark_python_task.source - - field: tasks[*].for_each_task.task.spark_python_task.source - - field: tasks[*].sql_task.file.source - - field: tasks[*].for_each_task.task.sql_task.file.source - - field: tasks[*].dbt_task.source - - field: tasks[*].for_each_task.task.dbt_task.source - - # Same as clusters.data_security_mode: backend sets this when not specified - - field: tasks[*].new_cluster.data_security_mode - - field: tasks[*].for_each_task.task.new_cluster.data_security_mode - - field: job_clusters[*].new_cluster.data_security_mode + 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 f9371afff03..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. -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: [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 419cf45197c..7a537b62397 100644 --- a/bundle/direct/dresources/configs/mcp_services.yml +++ b/bundle/direct/dresources/configs/mcp_services.yml @@ -1,10 +1,8 @@ -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: [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 d87a09b25f8..943a8481f1b 100644 --- a/bundle/direct/dresources/configs/model_provider_services.generated.yml +++ b/bundle/direct/dresources/configs/model_provider_services.generated.yml @@ -1,32 +1,16 @@ # Generated, do not edit. -recreate_on_changes: - - field: config.provider_type - 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 +fields: + 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 f83495c340f..26462daf4f4 100644 --- a/bundle/direct/dresources/configs/model_provider_services.yml +++ b/bundle/direct/dresources/configs/model_provider_services.yml @@ -1,9 +1,7 @@ -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: [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 1d2a16b848f..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. -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: [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 d1b7a91e6e5..b467e6565cd 100644 --- a/bundle/direct/dresources/configs/model_services.yml +++ b/bundle/direct/dresources/configs/model_services.yml @@ -1,10 +1,8 @@ -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: [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 71df6d6f832..31ca8251ecf 100644 --- a/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml +++ b/bundle/direct/dresources/configs/model_serving_endpoints.generated.yml @@ -1,34 +1,18 @@ # 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: [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 c1f88a79a7c..be9ec7adfa8 100644 --- a/bundle/direct/dresources/configs/model_serving_endpoints.yml +++ b/bundle/direct/dresources/configs/model_serving_endpoints.yml @@ -1,54 +1,25 @@ # TF implementation: https://github.com/databricks/terraform-provider-databricks/blob/6c106e8e7052bb2726148d66309fd460ed444236/mlflow/resource_mlflow_experiment.go#L22 -provided_id_fields: - - field: name - reason: id_field -recreate_on_changes: - # description is immutable, can't be updated via API - - field: description - reason: immutable - - field: config.auto_capture_config.catalog_name - reason: immutable - - field: config.auto_capture_config.schema_name - reason: immutable - - field: config.auto_capture_config.table_name_prefix - reason: immutable - - field: route_optimized - reason: immutable -ignore_remote_changes: - # budget_policy_id is in ServingEndpointDetailed but GET never populates it - # (the API returns effective_budget_policy_id instead), so the remote always - # reports empty. This is not a backend default, so suppress remote changes. - - field: budget_policy_id +fields: + 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 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: [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 - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L383 - # common.CustomizeSchemaPath(m, "config", "served_entities", "name").SetComputed() - - field: config.served_entities[*].name - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L384 - # common.CustomizeSchemaPath(m, "config", "served_entities", "workload_type").SetComputed() - - field: config.served_entities[*].workload_type - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L372 - # common.CustomizeSchemaPath(m, "config", "auto_capture_config", "enabled").SetComputed() - - field: config.auto_capture_config.enabled - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#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 + 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/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..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. -ignore_remote_changes: - - field: ingestion_definition.source_type - reason: spec:output_only +fields: + ingestion_definition.source_type: [mutable_output] diff --git a/bundle/direct/dresources/configs/pipelines.yml b/bundle/direct/dresources/configs/pipelines.yml index 59b92f268d6..4acf6d42170 100644 --- a/bundle/direct/dresources/configs/pipelines.yml +++ b/bundle/direct/dresources/configs/pipelines.yml @@ -1,32 +1,23 @@ -recreate_on_changes: - - field: storage - reason: immutable - - field: ingestion_definition.connection_name - reason: immutable - - field: ingestion_definition.ingestion_gateway_id - reason: immutable - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L204 - - field: gateway_definition.connection_id - reason: immutable - - field: gateway_definition.connection_name - reason: immutable - - field: gateway_definition.gateway_storage_catalog - reason: immutable - - field: gateway_definition.gateway_storage_schema - reason: immutable - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L209 - - field: ingestion_definition.ingest_from_uc_foreign_catalog - reason: immutable - -# See jobs above: version_id is set on every deploy, so it is ignored as a -# local/remote change. deployment_id is left out so a change to it still shows. -ignore_remote_changes: - - field: deployment.version_id +fields: + 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 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 +25,12 @@ 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 - # 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: - # Pipelines adds ANSI mode to the configuration map when it is omitted. - - field: configuration['spark.sql.ansi.enabled'] - values: ["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 - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L220 - # s.SchemaPath("cluster", "enable_local_disk_encryption").SetComputed() - - field: clusters[*].enable_local_disk_encryption - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/pipelines/resource_pipeline.go#L229-L230 - # s.SchemaPath("event_log", "catalog").SetComputed() - # s.SchemaPath("event_log", "schema").SetComputed() - - field: event_log.catalog - - field: event_log.schema + run_as: [input_only] + allow_duplicate_names: [input_only] + configuration['spark.sql.ansi.enabled']: + backend_values: ["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 07e938094ce..592755005bb 100644 --- a/bundle/direct/dresources/configs/postgres_branches.generated.yml +++ b/bundle/direct/dresources/configs/postgres_branches.generated.yml @@ -1,29 +1,11 @@ # Generated, do not edit. -recreate_on_changes: - - field: source_branch - reason: spec:immutable - - field: source_branch_lsn - reason: spec:immutable - - field: source_branch_time - reason: spec:immutable - - field: source_snapshot - reason: spec:immutable - -ignore_remote_changes: - - field: expire_time - reason: spec:input_only - - field: is_protected - reason: spec:input_only - - field: no_expiry - reason: spec:input_only - - field: source_branch - reason: spec:input_only - - field: source_branch_lsn - reason: spec:input_only - - field: source_branch_time - reason: spec:input_only - - field: source_snapshot - reason: spec:input_only - - field: ttl - reason: spec:input_only +fields: + 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 e256c724cd2..b945270cab5 100644 --- a/bundle/direct/dresources/configs/postgres_branches.yml +++ b/bundle/direct/dresources/configs/postgres_branches.yml @@ -1,14 +1,10 @@ -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: - # 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" -stable_output_fields: + 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. - - field: name + name: [immutable_output] diff --git a/bundle/direct/dresources/configs/postgres_catalogs.generated.yml b/bundle/direct/dresources/configs/postgres_catalogs.generated.yml index 63f0569c669..07b528649e1 100644 --- a/bundle/direct/dresources/configs/postgres_catalogs.generated.yml +++ b/bundle/direct/dresources/configs/postgres_catalogs.generated.yml @@ -1,13 +1,6 @@ # Generated, do not edit. -recreate_on_changes: - - field: postgres_database - reason: spec:immutable - -ignore_remote_changes: - - field: branch - reason: spec:input_only - - field: create_database_if_missing - reason: spec:input_only - - field: postgres_database - reason: spec:input_only +fields: + 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 c4f6945071f..9baddf30b87 100644 --- a/bundle/direct/dresources/configs/postgres_catalogs.yml +++ b/bundle/direct/dresources/configs/postgres_catalogs.yml @@ -1,10 +1,8 @@ -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: [id] diff --git a/bundle/direct/dresources/configs/postgres_databases.generated.yml b/bundle/direct/dresources/configs/postgres_databases.generated.yml index 9a9eb664188..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. -ignore_remote_changes: - - field: postgres_database - reason: spec:input_only - - field: role - reason: spec:input_only +fields: + 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 f9d65fbc398..61f21be23cf 100644 --- a/bundle/direct/dresources/configs/postgres_databases.yml +++ b/bundle/direct/dresources/configs/postgres_databases.yml @@ -1,14 +1,10 @@ -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: - # 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" -stable_output_fields: + 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. - - field: name + name: [immutable_output] diff --git a/bundle/direct/dresources/configs/postgres_endpoints.generated.yml b/bundle/direct/dresources/configs/postgres_endpoints.generated.yml index 2560c7f5ef7..aac4189f2c9 100644 --- a/bundle/direct/dresources/configs/postgres_endpoints.generated.yml +++ b/bundle/direct/dresources/configs/postgres_endpoints.generated.yml @@ -1,23 +1,11 @@ # Generated, do not edit. -recreate_on_changes: - - field: endpoint_type - reason: spec:immutable - -ignore_remote_changes: - - field: autoscaling_limit_max_cu - reason: spec:input_only - - field: autoscaling_limit_min_cu - reason: spec:input_only - - field: disabled - reason: spec:input_only - - field: endpoint_type - reason: spec:input_only - - field: group - reason: spec:input_only - - field: no_suspension - reason: spec:input_only - - field: settings - reason: spec:input_only - - field: suspend_timeout_duration - reason: spec:input_only +fields: + 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 a9da4a06c9c..c8395a51728 100644 --- a/bundle/direct/dresources/configs/postgres_endpoints.yml +++ b/bundle/direct/dresources/configs/postgres_endpoints.yml @@ -1,14 +1,10 @@ -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: - # 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" -stable_output_fields: + 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. - - field: name + name: [immutable_output] diff --git a/bundle/direct/dresources/configs/postgres_projects.generated.yml b/bundle/direct/dresources/configs/postgres_projects.generated.yml index cf29cbf156a..a87c9b4e6ef 100644 --- a/bundle/direct/dresources/configs/postgres_projects.generated.yml +++ b/bundle/direct/dresources/configs/postgres_projects.generated.yml @@ -1,27 +1,13 @@ # Generated, do not edit. -recreate_on_changes: - - field: pg_version - reason: spec:immutable - -ignore_remote_changes: - - field: budget_policy_id - reason: spec:input_only - - field: custom_tags - reason: spec:input_only - - field: custom_tags[*].key - reason: spec:input_only - - field: custom_tags[*].value - reason: spec:input_only - - field: default_branch - reason: spec:input_only - - field: default_endpoint_settings - reason: spec:input_only - - field: display_name - reason: spec:input_only - - field: enable_pg_native_login - reason: spec:input_only - - field: history_retention_duration - reason: spec:input_only - - field: pg_version - reason: spec:input_only +fields: + 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 4c5079e7079..30e06fa84fe 100644 --- a/bundle/direct/dresources/configs/postgres_projects.yml +++ b/bundle/direct/dresources/configs/postgres_projects.yml @@ -1,8 +1,4 @@ -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: - # 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 + 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 d5b37104344..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. -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: [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 2096b0d3d47..70e3aaed9f4 100644 --- a/bundle/direct/dresources/configs/postgres_roles.yml +++ b/bundle/direct/dresources/configs/postgres_roles.yml @@ -1,26 +1,14 @@ -ignore_local_changes: +fields: # 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" -stable_output_fields: + replace_existing: + action: ignore_local + reason: input_only # 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: - # parent and role_id are immutable (together they form the hierarchical name). - - field: parent - reason: immutable - - field: role_id - reason: immutable - # The PATCH update_mask only accepts spec.attributes and spec.membership_roles; - # the backend rejects spec.postgres_role, spec.auth_method, and spec.identity_type - # with 400 INVALID_PARAMETER_VALUE "Unknown field path in update_mask". These spec - # fields are not marked immutable in the OpenAPI definition yet, so the generator - # doesn't catch them — declare the constraint manually until upstream is fixed. - - field: postgres_role - reason: immutable - - field: auth_method - reason: immutable - - field: identity_type - reason: immutable + 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 d81b0d30548..ad111437daa 100644 --- a/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml +++ b/bundle/direct/dresources/configs/postgres_snapshot_schedules.yml @@ -1,10 +1,6 @@ -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: - # 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 + 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 082f93bd66e..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. -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: [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 2a76ea3a253..c1bd24baa0f 100644 --- a/bundle/direct/dresources/configs/postgres_synced_tables.yml +++ b/bundle/direct/dresources/configs/postgres_synced_tables.yml @@ -5,8 +5,6 @@ # 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: [id] diff --git a/bundle/direct/dresources/configs/quality_monitors.yml b/bundle/direct/dresources/configs/quality_monitors.yml index 98f5b059016..75fa89548a3 100644 --- a/bundle/direct/dresources/configs/quality_monitors.yml +++ b/bundle/direct/dresources/configs/quality_monitors.yml @@ -1,6 +1,3 @@ -provided_id_fields: - - field: table_name - reason: id_field -recreate_on_changes: - - field: assets_dir - reason: immutable +fields: + 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 9e376d45246..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. -ignore_remote_changes: - - field: browse_only - reason: spec:output_only +fields: + browse_only: [mutable_output] diff --git a/bundle/direct/dresources/configs/registered_models.yml b/bundle/direct/dresources/configs/registered_models.yml index 5d6bd9fd8de..102c5e4e36a 100644 --- a/bundle/direct/dresources/configs/registered_models.yml +++ b/bundle/direct/dresources/configs/registered_models.yml @@ -1,34 +1,18 @@ -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 - # Aliases are managed on model versions through a separate API, and DoRead - # passes IncludeAliases=false, so GET never echoes them back. Without this a - # config that sets aliases reports a perpetual update (remote stays empty). - - field: aliases - reason: input_only -provided_id_fields: - # The name can technically be updated without recreate. We recreate for now though - # to match TF implementation. - - field: name + 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 - - field: catalog_name - reason: id_field - - field: schema_name - reason: id_field -recreate_on_changes: - - field: storage_location - reason: immutable -backend_defaults: - # owner, full_name, metastore_id are Computed in TF (backend-set output fields). - - field: owner - - field: full_name - - field: metastore_id + 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 8ef27e71d15..1349bf0e813 100644 --- a/bundle/direct/dresources/configs/schemas.yml +++ b/bundle/direct/dresources/configs/schemas.yml @@ -1,17 +1,6 @@ -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: - # 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.*'] + 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 61c734eb8c1..d3fac39d489 100644 --- a/bundle/direct/dresources/configs/secret_scopes.permissions.yml +++ b/bundle/direct/dresources/configs/secret_scopes.permissions.yml @@ -1,6 +1,5 @@ # 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: [renameable_id] diff --git a/bundle/direct/dresources/configs/secret_scopes.yml b/bundle/direct/dresources/configs/secret_scopes.yml index 12f6429a3d3..e6b42263bfc 100644 --- a/bundle/direct/dresources/configs/secret_scopes.yml +++ b/bundle/direct/dresources/configs/secret_scopes.yml @@ -1,10 +1,5 @@ -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: [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 34877e8fb10..f343cb9307d 100644 --- a/bundle/direct/dresources/configs/secrets.generated.yml +++ b/bundle/direct/dresources/configs/secrets.generated.yml @@ -1,32 +1,16 @@ # Generated, do not edit. -recreate_on_changes: - - field: catalog_name - reason: spec:immutable - - field: name - reason: spec:immutable - - field: schema_name - reason: spec:immutable - -ignore_remote_changes: - - field: owner - reason: spec:input_only - - field: value - reason: spec:input_only - - - field: create_time - reason: spec:output_only - - field: created_by - reason: spec:output_only - - field: effective_owner - reason: spec:output_only - - field: effective_value - reason: spec:output_only - - field: full_name - reason: spec:output_only - - field: metastore_id - reason: spec:output_only - - field: update_time - reason: spec:output_only - - field: updated_by - reason: spec:output_only +fields: + 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 22ca3d7f49f..9aa519ddfc6 100644 --- a/bundle/direct/dresources/configs/secrets.yml +++ b/bundle/direct/dresources/configs/secrets.yml @@ -1,3 +1,3 @@ -sensitive_fields: - - field: value - - field: effective_value +fields: + value: [sensitive] + effective_value: [sensitive] diff --git a/bundle/direct/dresources/configs/sql_warehouses.yml b/bundle/direct/dresources/configs/sql_warehouses.yml index 582126d2d89..c427df19895 100644 --- a/bundle/direct/dresources/configs/sql_warehouses.yml +++ b/bundle/direct/dresources/configs/sql_warehouses.yml @@ -1,37 +1,22 @@ -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: - # lifecycle.started is derived from remote warehouse state in RemapState, so the - # remote side always has a value. When the user omits lifecycle from config, - # both old and new are nil and backend_defaults correctly skips the remote value. - # When the user explicitly sets lifecycle.started, old/new are non-nil and normal - # drift detection applies (e.g. detecting out-of-band stop). - - field: lifecycle - - field: lifecycle.started - - # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/sql/resource_sql_endpoint.go#L69 - # m["enable_serverless_compute"].Computed = true - - field: enable_serverless_compute + 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 2958becd442..fafe0355d84 100644 --- a/bundle/direct/dresources/configs/synced_database_tables.generated.yml +++ b/bundle/direct/dresources/configs/synced_database_tables.generated.yml @@ -1,24 +1,13 @@ # 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: [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 deb5126e6e1..b66ff1256dc 100644 --- a/bundle/direct/dresources/configs/synced_database_tables.yml +++ b/bundle/direct/dresources/configs/synced_database_tables.yml @@ -1,11 +1,9 @@ -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: [id] diff --git a/bundle/direct/dresources/configs/vector_search_endpoints.yml b/bundle/direct/dresources/configs/vector_search_endpoints.yml index 34fe753bd2a..fff3420477f 100644 --- a/bundle/direct/dresources/configs/vector_search_endpoints.yml +++ b/bundle/direct/dresources/configs/vector_search_endpoints.yml @@ -1,12 +1,6 @@ -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: - # 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 + 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 9e5e044030c..704744a5edf 100644 --- a/bundle/direct/dresources/configs/vector_search_indexes.yml +++ b/bundle/direct/dresources/configs/vector_search_indexes.yml @@ -1,28 +1,10 @@ -provided_id_fields: - - field: name - reason: id_field -recreate_on_changes: - # The index API has no rename or update path, so every config change - # has to go through delete + create. - - field: endpoint_name - reason: immutable - - field: index_type - reason: immutable - - field: index_subtype - reason: immutable - - field: primary_key - reason: immutable - - field: delta_sync_index_spec - reason: immutable - - field: direct_access_index_spec - reason: immutable -ignore_remote_changes: - # The backend rewrites schema_json on create: user-facing type names - # ("integer", "long", "short", "byte") are stored in Unity Catalog as - # Spark type names ("int", "bigint", "smallint", "tinyint") and the - # columns come back in sorted key order, so GET never echoes the user's - # literal input. Without this rule the rewrite reads as a change to the - # immutable direct_access_index_spec and plans a destructive recreate - # that drops all upserted vectors. - - field: direct_access_index_spec.schema_json - reason: normalized_by_backend +fields: + 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 d4b68e95875..a5d878cf4fb 100644 --- a/bundle/direct/dresources/configs/volumes.yml +++ b/bundle/direct/dresources/configs/volumes.yml @@ -1,18 +1,6 @@ -provided_id_fields: - - field: catalog_name - reason: id_field - - field: schema_name - reason: id_field -recreate_on_changes: - - field: storage_location - reason: immutable - - field: volume_type - reason: immutable -updatable_id_fields: - - field: name - reason: id_changes -normalize_slash: - # UC strips trailing slashes on create; matches the Terraform provider's suppressLocationDiff. - # https://github.com/databricks/terraform-provider-databricks/blob/v1.65.1/catalog/resource_volume.go#L25 - - field: storage_location - reason: uc_strips_trailing_slash +fields: + 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 new file mode 100644 index 00000000000..5acdee4dc76 --- /dev/null +++ b/bundle/direct/dresources/field_policy.go @@ -0,0 +1,323 @@ +package dresources + +import ( + "encoding/json" + "fmt" + "maps" + "slices" + + "github.com/databricks/cli/libs/structs/structpath" +) + +// 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. +// +// 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. 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 { + // 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 +} + +// 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" +) + +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 +} + +// 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 + } + + 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. +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 + actionRenameableID action = "renameable_id" // updatable id: rename in place on change + actionIgnoreLocal action = "ignore_local" // skip local edits +) + +var validActions = map[action]bool{ + actionMutable: true, + actionImmutable: true, + actionID: 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 { + return err + } + if !validActions[action(s)] { + return fmt.Errorf("unknown action %q", s) + } + *a = action(s) + return nil +} + +// Lower translates the field-keyed config into the flat ResourceLifecycleConfig the +// 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, generated); err != nil { + return out, err + } + if c.RemoteAdditionsWhen != nil { + out.IgnoreRemoteAdditions = append(out.IgnoreRemoteAdditions, RemoteAdditionRule{Field: nil, WhenSet: c.RemoteAdditionsWhen}) + } + + 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, generated); err != nil { + return out, fmt.Errorf("field %q: %w", field, err) + } + } + + 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, generated bool) error { + switch act { + case actionMutable: + if reason != "" { + return fmt.Errorf("reason %q set without an action", reason) + } + case actionID: + out.ProvidedIDFields = append(out.ProvidedIDFields, FieldRule{Field: pattern, Reason: reasonOr(reason, reasonIDField)}) + 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, specReason(reasonImmutable, generated))}) + case actionIgnoreLocal: + out.IgnoreLocalChanges = append(out.IgnoreLocalChanges, FieldRule{Field: pattern, Reason: reason}) + default: + // Unreachable: UnmarshalYAML validates actions against validActions. + return fmt.Errorf("unhandled action %q", act) + } + return 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: specReason(p.IgnoreRemote, generated)}) + } + if p.StableOutput { + out.StableOutputFields = append(out.StableOutputFields, FieldRule{Field: pattern, Reason: ""}) + } + + 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.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.RemoteAdditionsWhen != nil { + out.IgnoreRemoteAdditions = append(out.IgnoreRemoteAdditions, RemoteAdditionRule{Field: pattern, WhenSet: p.RemoteAdditionsWhen}) + } + return nil +} + +func reasonOr(reason, def string) string { + if reason == "" { + return def + } + return reason +} + +// 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 + } + out := make([]json.RawMessage, 0, len(vals)) + for _, v := range vals { + raw, err := json.Marshal(v) + if err != nil { + return nil, err + } + out = append(out, raw) + } + return out, 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..d426f082980 --- /dev/null +++ b/bundle/direct/dresources/field_policy_test.go @@ -0,0 +1,77 @@ +package dresources + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" +) + +// 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: [id]\n name: [immutable]\n"), &fpc) + require.Error(t, err) + assert.Contains(t, err.Error(), `already defined`) +} + +// 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: [teleport]\n"), &fpc) + require.Error(t, err) + assert.Contains(t, err.Error(), `unknown behaviour "teleport"`) +} + +func rulesOf(rs []FieldRule) []string { + var out []string + for _, r := range rs { + out = append(out, r.Field.String()+"|"+r.Reason) + } + return out +} + +// 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)) +} + +// 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 + } + + 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)) +} + +// 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 01302fedc79..4a96aa07f24 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,14 @@ 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 as a field -> [tokens] list, or "" if none. + + 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()): if "OUTPUT_ONLY" in fb: @@ -177,19 +169,32 @@ 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 "" - lines = [] - if recreate: - write_field_group(lines, "recreate_on_changes", recreate) - if ignore_remote: - write_field_group(lines, "ignore_remote_changes", ignore_remote) - - return HEADER + "\n\n" + "\n".join(lines) + "\n" + 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(tokens): + lines.append(f" {quote_key(field)}: [{', '.join(tokens[field])}]") + + return "\n".join(lines) + "\n" def write_files(outdir, resource_behaviors):