diff --git a/acceptance/bundle/refschema/out.fields.txt b/acceptance/bundle/refschema/out.fields.txt index decf27ca913..b9b2cb3e35c 100644 --- a/acceptance/bundle/refschema/out.fields.txt +++ b/acceptance/bundle/refschema/out.fields.txt @@ -2695,29 +2695,21 @@ resources.pipelines.*.permissions[*].group_name string ALL resources.pipelines.*.permissions[*].level iam.PermissionLevel ALL resources.pipelines.*.permissions[*].service_principal_name string ALL resources.pipelines.*.permissions[*].user_name string ALL -resources.postgres_branches.*.branch_id string INPUT STATE +resources.postgres_branches.*.branch_id string ALL resources.postgres_branches.*.create_time *time.Time REMOTE -resources.postgres_branches.*.expire_time *time.Time INPUT STATE +resources.postgres_branches.*.expire_time *time.Time ALL resources.postgres_branches.*.id string INPUT -resources.postgres_branches.*.is_protected bool INPUT STATE +resources.postgres_branches.*.is_protected bool ALL resources.postgres_branches.*.lifecycle resources.Lifecycle INPUT resources.postgres_branches.*.lifecycle.prevent_destroy bool INPUT resources.postgres_branches.*.modified_status string INPUT resources.postgres_branches.*.name string REMOTE -resources.postgres_branches.*.no_expiry bool INPUT STATE +resources.postgres_branches.*.no_expiry bool ALL resources.postgres_branches.*.parent string ALL resources.postgres_branches.*.replace_existing bool INPUT STATE -resources.postgres_branches.*.source_branch string INPUT STATE -resources.postgres_branches.*.source_branch_lsn string INPUT STATE -resources.postgres_branches.*.source_branch_time *time.Time INPUT STATE -resources.postgres_branches.*.spec *postgres.BranchSpec REMOTE -resources.postgres_branches.*.spec.expire_time *time.Time REMOTE -resources.postgres_branches.*.spec.is_protected bool REMOTE -resources.postgres_branches.*.spec.no_expiry bool REMOTE -resources.postgres_branches.*.spec.source_branch string REMOTE -resources.postgres_branches.*.spec.source_branch_lsn string REMOTE -resources.postgres_branches.*.spec.source_branch_time *time.Time REMOTE -resources.postgres_branches.*.spec.ttl *duration.Duration REMOTE +resources.postgres_branches.*.source_branch string ALL +resources.postgres_branches.*.source_branch_lsn string ALL +resources.postgres_branches.*.source_branch_time *time.Time ALL resources.postgres_branches.*.status *postgres.BranchStatus REMOTE resources.postgres_branches.*.status.branch_id string REMOTE resources.postgres_branches.*.status.current_state postgres.BranchStatusState REMOTE @@ -2730,45 +2722,31 @@ resources.postgres_branches.*.status.source_branch string REMOTE resources.postgres_branches.*.status.source_branch_lsn string REMOTE resources.postgres_branches.*.status.source_branch_time *time.Time REMOTE resources.postgres_branches.*.status.state_change_time *time.Time REMOTE -resources.postgres_branches.*.ttl *duration.Duration INPUT STATE +resources.postgres_branches.*.ttl *duration.Duration ALL resources.postgres_branches.*.uid string REMOTE resources.postgres_branches.*.update_time *time.Time REMOTE resources.postgres_branches.*.url string INPUT -resources.postgres_endpoints.*.autoscaling_limit_max_cu float64 INPUT STATE -resources.postgres_endpoints.*.autoscaling_limit_min_cu float64 INPUT STATE +resources.postgres_endpoints.*.autoscaling_limit_max_cu float64 ALL +resources.postgres_endpoints.*.autoscaling_limit_min_cu float64 ALL resources.postgres_endpoints.*.create_time *time.Time REMOTE -resources.postgres_endpoints.*.disabled bool INPUT STATE -resources.postgres_endpoints.*.endpoint_id string INPUT STATE -resources.postgres_endpoints.*.endpoint_type postgres.EndpointType INPUT STATE -resources.postgres_endpoints.*.group *postgres.EndpointGroupSpec INPUT STATE -resources.postgres_endpoints.*.group.enable_readable_secondaries bool INPUT STATE -resources.postgres_endpoints.*.group.max int INPUT STATE -resources.postgres_endpoints.*.group.min int INPUT STATE +resources.postgres_endpoints.*.disabled bool ALL +resources.postgres_endpoints.*.endpoint_id string ALL +resources.postgres_endpoints.*.endpoint_type postgres.EndpointType ALL +resources.postgres_endpoints.*.group *postgres.EndpointGroupSpec ALL +resources.postgres_endpoints.*.group.enable_readable_secondaries bool ALL +resources.postgres_endpoints.*.group.max int ALL +resources.postgres_endpoints.*.group.min int ALL resources.postgres_endpoints.*.id string INPUT resources.postgres_endpoints.*.lifecycle resources.Lifecycle INPUT resources.postgres_endpoints.*.lifecycle.prevent_destroy bool INPUT resources.postgres_endpoints.*.modified_status string INPUT resources.postgres_endpoints.*.name string REMOTE -resources.postgres_endpoints.*.no_suspension bool INPUT STATE +resources.postgres_endpoints.*.no_suspension bool ALL resources.postgres_endpoints.*.parent string ALL resources.postgres_endpoints.*.replace_existing bool INPUT STATE -resources.postgres_endpoints.*.settings *postgres.EndpointSettings INPUT STATE -resources.postgres_endpoints.*.settings.pg_settings map[string]string INPUT STATE -resources.postgres_endpoints.*.settings.pg_settings.* string INPUT STATE -resources.postgres_endpoints.*.spec *postgres.EndpointSpec REMOTE -resources.postgres_endpoints.*.spec.autoscaling_limit_max_cu float64 REMOTE -resources.postgres_endpoints.*.spec.autoscaling_limit_min_cu float64 REMOTE -resources.postgres_endpoints.*.spec.disabled bool REMOTE -resources.postgres_endpoints.*.spec.endpoint_type postgres.EndpointType REMOTE -resources.postgres_endpoints.*.spec.group *postgres.EndpointGroupSpec REMOTE -resources.postgres_endpoints.*.spec.group.enable_readable_secondaries bool REMOTE -resources.postgres_endpoints.*.spec.group.max int REMOTE -resources.postgres_endpoints.*.spec.group.min int REMOTE -resources.postgres_endpoints.*.spec.no_suspension bool REMOTE -resources.postgres_endpoints.*.spec.settings *postgres.EndpointSettings REMOTE -resources.postgres_endpoints.*.spec.settings.pg_settings map[string]string REMOTE -resources.postgres_endpoints.*.spec.settings.pg_settings.* string REMOTE -resources.postgres_endpoints.*.spec.suspend_timeout_duration *duration.Duration REMOTE +resources.postgres_endpoints.*.settings *postgres.EndpointSettings ALL +resources.postgres_endpoints.*.settings.pg_settings map[string]string ALL +resources.postgres_endpoints.*.settings.pg_settings.* string ALL resources.postgres_endpoints.*.status *postgres.EndpointStatus REMOTE resources.postgres_endpoints.*.status.autoscaling_limit_max_cu float64 REMOTE resources.postgres_endpoints.*.status.autoscaling_limit_min_cu float64 REMOTE @@ -2788,28 +2766,28 @@ resources.postgres_endpoints.*.status.settings *postgres.EndpointSettings REMOTE resources.postgres_endpoints.*.status.settings.pg_settings map[string]string REMOTE resources.postgres_endpoints.*.status.settings.pg_settings.* string REMOTE resources.postgres_endpoints.*.status.suspend_timeout_duration *duration.Duration REMOTE -resources.postgres_endpoints.*.suspend_timeout_duration *duration.Duration INPUT STATE +resources.postgres_endpoints.*.suspend_timeout_duration *duration.Duration ALL resources.postgres_endpoints.*.uid string REMOTE resources.postgres_endpoints.*.update_time *time.Time REMOTE resources.postgres_endpoints.*.url string INPUT -resources.postgres_projects.*.budget_policy_id string INPUT STATE +resources.postgres_projects.*.budget_policy_id string ALL resources.postgres_projects.*.create_time *time.Time REMOTE -resources.postgres_projects.*.custom_tags []postgres.ProjectCustomTag INPUT STATE -resources.postgres_projects.*.custom_tags[*] postgres.ProjectCustomTag INPUT STATE -resources.postgres_projects.*.custom_tags[*].key string INPUT STATE -resources.postgres_projects.*.custom_tags[*].value string INPUT STATE -resources.postgres_projects.*.default_branch string INPUT STATE -resources.postgres_projects.*.default_endpoint_settings *postgres.ProjectDefaultEndpointSettings INPUT STATE -resources.postgres_projects.*.default_endpoint_settings.autoscaling_limit_max_cu float64 INPUT STATE -resources.postgres_projects.*.default_endpoint_settings.autoscaling_limit_min_cu float64 INPUT STATE -resources.postgres_projects.*.default_endpoint_settings.no_suspension bool INPUT STATE -resources.postgres_projects.*.default_endpoint_settings.pg_settings map[string]string INPUT STATE -resources.postgres_projects.*.default_endpoint_settings.pg_settings.* string INPUT STATE -resources.postgres_projects.*.default_endpoint_settings.suspend_timeout_duration *duration.Duration INPUT STATE +resources.postgres_projects.*.custom_tags []postgres.ProjectCustomTag ALL +resources.postgres_projects.*.custom_tags[*] postgres.ProjectCustomTag ALL +resources.postgres_projects.*.custom_tags[*].key string ALL +resources.postgres_projects.*.custom_tags[*].value string ALL +resources.postgres_projects.*.default_branch string ALL +resources.postgres_projects.*.default_endpoint_settings *postgres.ProjectDefaultEndpointSettings ALL +resources.postgres_projects.*.default_endpoint_settings.autoscaling_limit_max_cu float64 ALL +resources.postgres_projects.*.default_endpoint_settings.autoscaling_limit_min_cu float64 ALL +resources.postgres_projects.*.default_endpoint_settings.no_suspension bool ALL +resources.postgres_projects.*.default_endpoint_settings.pg_settings map[string]string ALL +resources.postgres_projects.*.default_endpoint_settings.pg_settings.* string ALL +resources.postgres_projects.*.default_endpoint_settings.suspend_timeout_duration *duration.Duration ALL resources.postgres_projects.*.delete_time *time.Time REMOTE -resources.postgres_projects.*.display_name string INPUT STATE -resources.postgres_projects.*.enable_pg_native_login bool INPUT STATE -resources.postgres_projects.*.history_retention_duration *duration.Duration INPUT STATE +resources.postgres_projects.*.display_name string ALL +resources.postgres_projects.*.enable_pg_native_login bool ALL +resources.postgres_projects.*.history_retention_duration *duration.Duration ALL resources.postgres_projects.*.id string INPUT resources.postgres_projects.*.initial_endpoint_spec *postgres.InitialEndpointSpec REMOTE resources.postgres_projects.*.initial_endpoint_spec.group *postgres.EndpointGroupSpec REMOTE @@ -2820,27 +2798,9 @@ resources.postgres_projects.*.lifecycle resources.Lifecycle INPUT resources.postgres_projects.*.lifecycle.prevent_destroy bool INPUT resources.postgres_projects.*.modified_status string INPUT resources.postgres_projects.*.name string REMOTE -resources.postgres_projects.*.pg_version int INPUT STATE -resources.postgres_projects.*.project_id string INPUT STATE +resources.postgres_projects.*.pg_version int ALL +resources.postgres_projects.*.project_id string ALL resources.postgres_projects.*.purge_time *time.Time REMOTE -resources.postgres_projects.*.spec *postgres.ProjectSpec REMOTE -resources.postgres_projects.*.spec.budget_policy_id string REMOTE -resources.postgres_projects.*.spec.custom_tags []postgres.ProjectCustomTag REMOTE -resources.postgres_projects.*.spec.custom_tags[*] postgres.ProjectCustomTag REMOTE -resources.postgres_projects.*.spec.custom_tags[*].key string REMOTE -resources.postgres_projects.*.spec.custom_tags[*].value string REMOTE -resources.postgres_projects.*.spec.default_branch string REMOTE -resources.postgres_projects.*.spec.default_endpoint_settings *postgres.ProjectDefaultEndpointSettings REMOTE -resources.postgres_projects.*.spec.default_endpoint_settings.autoscaling_limit_max_cu float64 REMOTE -resources.postgres_projects.*.spec.default_endpoint_settings.autoscaling_limit_min_cu float64 REMOTE -resources.postgres_projects.*.spec.default_endpoint_settings.no_suspension bool REMOTE -resources.postgres_projects.*.spec.default_endpoint_settings.pg_settings map[string]string REMOTE -resources.postgres_projects.*.spec.default_endpoint_settings.pg_settings.* string REMOTE -resources.postgres_projects.*.spec.default_endpoint_settings.suspend_timeout_duration *duration.Duration REMOTE -resources.postgres_projects.*.spec.display_name string REMOTE -resources.postgres_projects.*.spec.enable_pg_native_login bool REMOTE -resources.postgres_projects.*.spec.history_retention_duration *duration.Duration REMOTE -resources.postgres_projects.*.spec.pg_version int REMOTE resources.postgres_projects.*.status *postgres.ProjectStatus REMOTE resources.postgres_projects.*.status.branch_logical_size_limit_bytes int64 REMOTE resources.postgres_projects.*.status.budget_policy_id string REMOTE diff --git a/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.no_change.direct.json b/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.no_change.direct.json index 0d0f8199726..ff7df005d4e 100644 --- a/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.no_change.direct.json +++ b/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.no_change.direct.json @@ -7,6 +7,7 @@ ], "action": "skip", "remote_state": { + "branch_id": "dev-branch", "create_time": "[TIMESTAMP]", "name": "[DEV_BRANCH_ID]", "parent": "projects/test-pg-proj-[UNIQUE_NAME]", diff --git a/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.restore.direct.json b/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.restore.direct.json index 7ad0c629ff0..64acdc88a68 100644 --- a/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.restore.direct.json +++ b/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.restore.direct.json @@ -15,6 +15,7 @@ } }, "remote_state": { + "branch_id": "dev-branch", "create_time": "[TIMESTAMP]", "name": "[DEV_BRANCH_ID]", "parent": "projects/test-pg-proj-[UNIQUE_NAME]", diff --git a/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.update.direct.json b/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.update.direct.json index 00633d0784c..6cc645dede6 100644 --- a/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.update.direct.json +++ b/acceptance/bundle/resources/postgres_branches/update_protected/out.plan.update.direct.json @@ -15,6 +15,7 @@ } }, "remote_state": { + "branch_id": "dev-branch", "create_time": "[TIMESTAMP]", "name": "[DEV_BRANCH_ID]", "parent": "projects/test-pg-proj-[UNIQUE_NAME]", diff --git a/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.no_change.direct.json b/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.no_change.direct.json index fb4482f3ed2..09deef4c5cb 100644 --- a/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.no_change.direct.json +++ b/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.no_change.direct.json @@ -8,6 +8,8 @@ "action": "skip", "remote_state": { "create_time": "[TIMESTAMP]", + "endpoint_id": "my-endpoint", + "endpoint_type": "", "name": "[MY_ENDPOINT_ID]", "parent": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main", "status": { diff --git a/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.restore.direct.json b/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.restore.direct.json index e03b47aace0..747dfb2b1cd 100644 --- a/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.restore.direct.json +++ b/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.restore.direct.json @@ -18,6 +18,8 @@ }, "remote_state": { "create_time": "[TIMESTAMP]", + "endpoint_id": "my-endpoint", + "endpoint_type": "", "name": "[MY_ENDPOINT_ID]", "parent": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main", "status": { diff --git a/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.update.direct.json b/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.update.direct.json index 0c7cc3909a1..1fc1d86a101 100644 --- a/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.update.direct.json +++ b/acceptance/bundle/resources/postgres_endpoints/update_autoscaling/out.plan.update.direct.json @@ -18,6 +18,8 @@ }, "remote_state": { "create_time": "[TIMESTAMP]", + "endpoint_id": "my-endpoint", + "endpoint_type": "", "name": "[MY_ENDPOINT_ID]", "parent": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main", "status": { diff --git a/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.no_change.direct.json b/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.no_change.direct.json index e088b88f2d3..70bd359168a 100644 --- a/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.no_change.direct.json +++ b/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.no_change.direct.json @@ -3,6 +3,7 @@ "remote_state": { "create_time": "[TIMESTAMP]", "name": "[MY_PROJECT_ID]", + "project_id": "test-pg-proj-[UNIQUE_NAME]", "status": { "branch_logical_size_limit_bytes": [NUMID], "default_branch": "[MY_PROJECT_ID]/branches/production", diff --git a/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.restore.direct.json b/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.restore.direct.json index a7d86eeb4ae..42b798bd65c 100644 --- a/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.restore.direct.json +++ b/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.restore.direct.json @@ -16,6 +16,7 @@ "remote_state": { "create_time": "[TIMESTAMP]", "name": "[MY_PROJECT_ID]", + "project_id": "test-pg-proj-[UNIQUE_NAME]", "status": { "branch_logical_size_limit_bytes": [NUMID], "default_branch": "[MY_PROJECT_ID]/branches/production", diff --git a/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.update.direct.json b/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.update.direct.json index 32cbdf74851..553b77c847a 100644 --- a/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.update.direct.json +++ b/acceptance/bundle/resources/postgres_projects/update_display_name/out.plan.update.direct.json @@ -16,6 +16,7 @@ "remote_state": { "create_time": "[TIMESTAMP]", "name": "[MY_PROJECT_ID]", + "project_id": "test-pg-proj-[UNIQUE_NAME]", "status": { "branch_logical_size_limit_bytes": [NUMID], "default_branch": "[MY_PROJECT_ID]/branches/production", diff --git a/bundle/direct/dresources/postgres_branch.go b/bundle/direct/dresources/postgres_branch.go index be8833acc95..f2b2a982ed5 100644 --- a/bundle/direct/dresources/postgres_branch.go +++ b/bundle/direct/dresources/postgres_branch.go @@ -6,9 +6,37 @@ import ( "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/common/types/fieldmask" + sdktime "github.com/databricks/databricks-sdk-go/common/types/time" + "github.com/databricks/databricks-sdk-go/marshal" "github.com/databricks/databricks-sdk-go/service/postgres" ) +// PostgresBranchRemote is the return type for DoRead. It embeds BranchSpec so that +// all paths in StateType are valid paths in RemoteType, enabling drift detection +// for spec fields once the backend echoes spec on GET. +type PostgresBranchRemote struct { + postgres.BranchSpec + + BranchId string `json:"branch_id,omitempty"` + Parent string `json:"parent,omitempty"` + + Name string `json:"name,omitempty"` + Status *postgres.BranchStatus `json:"status,omitempty"` + Uid string `json:"uid,omitempty"` + CreateTime *sdktime.Time `json:"create_time,omitempty"` + UpdateTime *sdktime.Time `json:"update_time,omitempty"` +} + +// Custom marshaler needed because embedded BranchSpec has its own MarshalJSON +// which would otherwise take over and ignore the additional fields. +func (s *PostgresBranchRemote) UnmarshalJSON(b []byte) error { + return marshal.Unmarshal(b, s) +} + +func (s PostgresBranchRemote) MarshalJSON() ([]byte, error) { + return marshal.Marshal(s) +} + type ResourcePostgresBranch struct { client *databricks.WorkspaceClient } @@ -28,40 +56,52 @@ func (*ResourcePostgresBranch) PrepareState(input *resources.PostgresBranch) *Po } } -func (*ResourcePostgresBranch) RemapState(remote *postgres.Branch) *PostgresBranchState { - // Extract branch_id from hierarchical name: "projects/{project_id}/branches/{branch_id}" - // TODO: log error when we have access to the context - components, _ := ParsePostgresName(remote.Name) - +func (*ResourcePostgresBranch) RemapState(remote *PostgresBranchRemote) *PostgresBranchState { return &PostgresBranchState{ - BranchId: components.BranchID, + BranchId: remote.BranchId, Parent: remote.Parent, // replace_existing is a create-time-only flag; the GET API never returns // it, so RemapState leaves it false. ReplaceExisting: false, - // The read API does not return the spec, only the status. - // This means we cannot detect remote drift for spec fields. - // Use an empty struct (not nil) so field-level diffing works correctly. - BranchSpec: postgres.BranchSpec{ - ExpireTime: nil, - IsProtected: false, - NoExpiry: false, - SourceBranch: "", - SourceBranchLsn: "", - SourceBranchTime: nil, - Ttl: nil, - ForceSendFields: nil, - }, + BranchSpec: remote.BranchSpec, } } -func (r *ResourcePostgresBranch) DoRead(ctx context.Context, id string) (*postgres.Branch, error) { - return r.client.Postgres.GetBranch(ctx, postgres.GetBranchRequest{Name: id}) +// makePostgresBranchRemote converts the SDK Branch into the embedded remote shape. +// GET does not echo spec today (only status is returned); the embedded spec fields +// stay at their zero values, and resources.yml suppresses phantom drift via +// ignore_remote_changes with reason spec:input_only. +func makePostgresBranchRemote(branch *postgres.Branch) *PostgresBranchRemote { + // Extract branch_id from hierarchical name: "projects/{project_id}/branches/{branch_id}" + // TODO: log error when we have access to the context + components, _ := ParsePostgresName(branch.Name) + var spec postgres.BranchSpec + if branch.Spec != nil { + spec = *branch.Spec + } + return &PostgresBranchRemote{ + BranchSpec: spec, + BranchId: components.BranchID, + Parent: branch.Parent, + Name: branch.Name, + Status: branch.Status, + Uid: branch.Uid, + CreateTime: branch.CreateTime, + UpdateTime: branch.UpdateTime, + } +} + +func (r *ResourcePostgresBranch) DoRead(ctx context.Context, id string) (*PostgresBranchRemote, error) { + branch, err := r.client.Postgres.GetBranch(ctx, postgres.GetBranchRequest{Name: id}) + if err != nil { + return nil, err + } + return makePostgresBranchRemote(branch), nil } -func (r *ResourcePostgresBranch) DoCreate(ctx context.Context, config *PostgresBranchState) (string, *postgres.Branch, error) { +func (r *ResourcePostgresBranch) DoCreate(ctx context.Context, config *PostgresBranchState) (string, *PostgresBranchRemote, error) { waiter, err := r.client.Postgres.CreateBranch(ctx, postgres.CreateBranchRequest{ BranchId: config.BranchId, Parent: config.Parent, @@ -90,10 +130,11 @@ func (r *ResourcePostgresBranch) DoCreate(ctx context.Context, config *PostgresB return "", nil, err } - return result.Name, result, nil + remote := makePostgresBranchRemote(result) + return remote.Name, remote, nil } -func (r *ResourcePostgresBranch) DoUpdate(ctx context.Context, id string, config *PostgresBranchState, entry *PlanEntry) (*postgres.Branch, error) { +func (r *ResourcePostgresBranch) DoUpdate(ctx context.Context, id string, config *PostgresBranchState, entry *PlanEntry) (*PostgresBranchRemote, error) { // Build update mask from fields that have action="update" in the changes map. // This excludes immutable fields and fields that haven't changed. // Prefix with "spec." because the API expects paths relative to the Branch object, @@ -124,7 +165,10 @@ func (r *ResourcePostgresBranch) DoUpdate(ctx context.Context, id string, config // Wait for the update to complete result, err := waiter.Wait(ctx) - return result, err + if err != nil { + return nil, err + } + return makePostgresBranchRemote(result), nil } func (r *ResourcePostgresBranch) DoDelete(ctx context.Context, id string) error { diff --git a/bundle/direct/dresources/postgres_endpoint.go b/bundle/direct/dresources/postgres_endpoint.go index 14698d7de2b..81821afeb43 100644 --- a/bundle/direct/dresources/postgres_endpoint.go +++ b/bundle/direct/dresources/postgres_endpoint.go @@ -11,6 +11,8 @@ import ( "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/common/types/fieldmask" + sdktime "github.com/databricks/databricks-sdk-go/common/types/time" + "github.com/databricks/databricks-sdk-go/marshal" "github.com/databricks/databricks-sdk-go/service/postgres" ) @@ -18,6 +20,32 @@ import ( // This value is a heuristic and is being discussed with the backend team. const endpointReconciliationTimeout = 2 * time.Minute +// PostgresEndpointRemote is the return type for DoRead. It embeds EndpointSpec so +// that all paths in StateType are valid paths in RemoteType, enabling drift +// detection for spec fields once the backend echoes spec on GET. +type PostgresEndpointRemote struct { + postgres.EndpointSpec + + EndpointId string `json:"endpoint_id,omitempty"` + Parent string `json:"parent,omitempty"` + + Name string `json:"name,omitempty"` + Status *postgres.EndpointStatus `json:"status,omitempty"` + Uid string `json:"uid,omitempty"` + CreateTime *sdktime.Time `json:"create_time,omitempty"` + UpdateTime *sdktime.Time `json:"update_time,omitempty"` +} + +// Custom marshaler needed because embedded EndpointSpec has its own MarshalJSON +// which would otherwise take over and ignore the additional fields. +func (s *PostgresEndpointRemote) UnmarshalJSON(b []byte) error { + return marshal.Unmarshal(b, s) +} + +func (s PostgresEndpointRemote) MarshalJSON() ([]byte, error) { + return marshal.Marshal(s) +} + type ResourcePostgresEndpoint struct { client *databricks.WorkspaceClient } @@ -37,44 +65,55 @@ func (*ResourcePostgresEndpoint) PrepareState(input *resources.PostgresEndpoint) } } -func (*ResourcePostgresEndpoint) RemapState(remote *postgres.Endpoint) *PostgresEndpointState { - // Extract endpoint_id from hierarchical name: "projects/{project_id}/branches/{branch_id}/endpoints/{endpoint_id}" - // TODO: log error when we have access to the context - components, _ := ParsePostgresName(remote.Name) - +func (*ResourcePostgresEndpoint) RemapState(remote *PostgresEndpointRemote) *PostgresEndpointState { return &PostgresEndpointState{ - EndpointId: components.EndpointID, + EndpointId: remote.EndpointId, Parent: remote.Parent, // replace_existing is a create-time-only flag; the GET API never returns // it, so RemapState leaves it false. ReplaceExisting: false, - // The read API does not return the spec, only the status. - // This means we cannot detect remote drift for spec fields. - // Use an empty struct (not nil) so field-level diffing works correctly. - EndpointSpec: postgres.EndpointSpec{ - AutoscalingLimitMaxCu: 0, - AutoscalingLimitMinCu: 0, - Disabled: false, - EndpointType: "", - Group: nil, - NoSuspension: false, - Settings: nil, - SuspendTimeoutDuration: nil, - ForceSendFields: nil, - }, + EndpointSpec: remote.EndpointSpec, + } +} + +// makePostgresEndpointRemote converts the SDK Endpoint into the embedded remote shape. +// GET does not echo spec today (only status is returned); the embedded spec fields +// stay at their zero values, and resources.yml suppresses phantom drift via +// ignore_remote_changes with reason spec:input_only. +func makePostgresEndpointRemote(endpoint *postgres.Endpoint) *PostgresEndpointRemote { + // Extract endpoint_id from hierarchical name: "projects/{project_id}/branches/{branch_id}/endpoints/{endpoint_id}" + // TODO: log error when we have access to the context + components, _ := ParsePostgresName(endpoint.Name) + var spec postgres.EndpointSpec + if endpoint.Spec != nil { + spec = *endpoint.Spec + } + return &PostgresEndpointRemote{ + EndpointSpec: spec, + EndpointId: components.EndpointID, + Parent: endpoint.Parent, + Name: endpoint.Name, + Status: endpoint.Status, + Uid: endpoint.Uid, + CreateTime: endpoint.CreateTime, + UpdateTime: endpoint.UpdateTime, } } -func (r *ResourcePostgresEndpoint) DoRead(ctx context.Context, id string) (*postgres.Endpoint, error) { - return r.client.Postgres.GetEndpoint(ctx, postgres.GetEndpointRequest{Name: id}) +func (r *ResourcePostgresEndpoint) DoRead(ctx context.Context, id string) (*PostgresEndpointRemote, error) { + endpoint, err := r.client.Postgres.GetEndpoint(ctx, postgres.GetEndpointRequest{Name: id}) + if err != nil { + return nil, err + } + return makePostgresEndpointRemote(endpoint), nil } // waitForReconciliation polls the endpoint until PendingState is empty. // This is needed because the operation can complete while internal reconciliation // is still in progress, which would cause subsequent operations to fail. -func (r *ResourcePostgresEndpoint) waitForReconciliation(ctx context.Context, name string) (*postgres.Endpoint, error) { +func (r *ResourcePostgresEndpoint) waitForReconciliation(ctx context.Context, name string) (*PostgresEndpointRemote, error) { deadline := time.Now().Add(endpointReconciliationTimeout) for { endpoint, err := r.client.Postgres.GetEndpoint(ctx, postgres.GetEndpointRequest{Name: name}) @@ -84,7 +123,7 @@ func (r *ResourcePostgresEndpoint) waitForReconciliation(ctx context.Context, na // If there's no pending state, reconciliation is complete if endpoint.Status == nil || endpoint.Status.PendingState == "" { - return endpoint, nil + return makePostgresEndpointRemote(endpoint), nil } // Check if we've exceeded the timeout @@ -101,7 +140,7 @@ func (r *ResourcePostgresEndpoint) waitForReconciliation(ctx context.Context, na } } -func (r *ResourcePostgresEndpoint) DoCreate(ctx context.Context, config *PostgresEndpointState) (string, *postgres.Endpoint, error) { +func (r *ResourcePostgresEndpoint) DoCreate(ctx context.Context, config *PostgresEndpointState) (string, *PostgresEndpointRemote, error) { waiter, err := r.client.Postgres.CreateEndpoint(ctx, postgres.CreateEndpointRequest{ EndpointId: config.EndpointId, Parent: config.Parent, @@ -131,15 +170,15 @@ func (r *ResourcePostgresEndpoint) DoCreate(ctx context.Context, config *Postgre } // Wait for reconciliation to complete - result, err = r.waitForReconciliation(ctx, result.Name) + remote, err := r.waitForReconciliation(ctx, result.Name) if err != nil { return "", nil, err } - return result.Name, result, nil + return remote.Name, remote, nil } -func (r *ResourcePostgresEndpoint) DoUpdate(ctx context.Context, id string, config *PostgresEndpointState, entry *PlanEntry) (*postgres.Endpoint, error) { +func (r *ResourcePostgresEndpoint) DoUpdate(ctx context.Context, id string, config *PostgresEndpointState, entry *PlanEntry) (*PostgresEndpointRemote, error) { // Build update mask from fields that have action="update" in the changes map. // This excludes immutable fields and fields that haven't changed. // Prefix with "spec." because the API expects paths relative to the Endpoint object, diff --git a/bundle/direct/dresources/postgres_project.go b/bundle/direct/dresources/postgres_project.go index 86c40d35255..d19834876c8 100644 --- a/bundle/direct/dresources/postgres_project.go +++ b/bundle/direct/dresources/postgres_project.go @@ -6,9 +6,39 @@ import ( "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/common/types/fieldmask" + sdktime "github.com/databricks/databricks-sdk-go/common/types/time" + "github.com/databricks/databricks-sdk-go/marshal" "github.com/databricks/databricks-sdk-go/service/postgres" ) +// PostgresProjectRemote is the return type for DoRead. It embeds ProjectSpec so +// that all paths in StateType are valid paths in RemoteType, enabling drift +// detection for spec fields once the backend echoes spec on GET. +type PostgresProjectRemote struct { + postgres.ProjectSpec + + ProjectId string `json:"project_id,omitempty"` + + InitialEndpointSpec *postgres.InitialEndpointSpec `json:"initial_endpoint_spec,omitempty"` + Name string `json:"name,omitempty"` + Status *postgres.ProjectStatus `json:"status,omitempty"` + Uid string `json:"uid,omitempty"` + CreateTime *sdktime.Time `json:"create_time,omitempty"` + DeleteTime *sdktime.Time `json:"delete_time,omitempty"` + PurgeTime *sdktime.Time `json:"purge_time,omitempty"` + UpdateTime *sdktime.Time `json:"update_time,omitempty"` +} + +// Custom marshaler needed because embedded ProjectSpec has its own MarshalJSON +// which would otherwise take over and ignore the additional fields. +func (s *PostgresProjectRemote) UnmarshalJSON(b []byte) error { + return marshal.Unmarshal(b, s) +} + +func (s PostgresProjectRemote) MarshalJSON() ([]byte, error) { + return marshal.Marshal(s) +} + type ResourcePostgresProject struct { client *databricks.WorkspaceClient } @@ -26,36 +56,48 @@ func (*ResourcePostgresProject) PrepareState(input *resources.PostgresProject) * } } -func (*ResourcePostgresProject) RemapState(remote *postgres.Project) *PostgresProjectState { +func (*ResourcePostgresProject) RemapState(remote *PostgresProjectRemote) *PostgresProjectState { + return &PostgresProjectState{ + ProjectId: remote.ProjectId, + ProjectSpec: remote.ProjectSpec, + } +} + +// makePostgresProjectRemote converts the SDK Project into the embedded remote shape. +// GET does not echo spec today (only status is returned); the embedded spec fields +// stay at their zero values, and resources.yml suppresses phantom drift via +// ignore_remote_changes with reason spec:input_only. +func makePostgresProjectRemote(project *postgres.Project) *PostgresProjectRemote { // Extract project_id from hierarchical name: "projects/{project_id}" // TODO: log error when we have access to the context - components, _ := ParsePostgresName(remote.Name) - - return &PostgresProjectState{ - ProjectId: components.ProjectID, - - // The read API does not return the spec, only the status. - // This means we cannot detect remote drift for spec fields. - // Use an empty struct (not nil) so field-level diffing works correctly. - ProjectSpec: postgres.ProjectSpec{ - BudgetPolicyId: "", - CustomTags: nil, - DefaultBranch: "", - DefaultEndpointSettings: nil, - DisplayName: "", - EnablePgNativeLogin: false, - HistoryRetentionDuration: nil, - PgVersion: 0, - ForceSendFields: nil, - }, + components, _ := ParsePostgresName(project.Name) + var spec postgres.ProjectSpec + if project.Spec != nil { + spec = *project.Spec + } + return &PostgresProjectRemote{ + ProjectSpec: spec, + ProjectId: components.ProjectID, + InitialEndpointSpec: project.InitialEndpointSpec, + Name: project.Name, + Status: project.Status, + Uid: project.Uid, + CreateTime: project.CreateTime, + DeleteTime: project.DeleteTime, + PurgeTime: project.PurgeTime, + UpdateTime: project.UpdateTime, } } -func (r *ResourcePostgresProject) DoRead(ctx context.Context, id string) (*postgres.Project, error) { - return r.client.Postgres.GetProject(ctx, postgres.GetProjectRequest{Name: id}) +func (r *ResourcePostgresProject) DoRead(ctx context.Context, id string) (*PostgresProjectRemote, error) { + project, err := r.client.Postgres.GetProject(ctx, postgres.GetProjectRequest{Name: id}) + if err != nil { + return nil, err + } + return makePostgresProjectRemote(project), nil } -func (r *ResourcePostgresProject) DoCreate(ctx context.Context, config *PostgresProjectState) (string, *postgres.Project, error) { +func (r *ResourcePostgresProject) DoCreate(ctx context.Context, config *PostgresProjectState) (string, *PostgresProjectRemote, error) { waiter, err := r.client.Postgres.CreateProject(ctx, postgres.CreateProjectRequest{ ProjectId: config.ProjectId, Project: postgres.Project{ @@ -83,10 +125,11 @@ func (r *ResourcePostgresProject) DoCreate(ctx context.Context, config *Postgres return "", nil, err } - return result.Name, result, nil + remote := makePostgresProjectRemote(result) + return remote.Name, remote, nil } -func (r *ResourcePostgresProject) DoUpdate(ctx context.Context, id string, config *PostgresProjectState, entry *PlanEntry) (*postgres.Project, error) { +func (r *ResourcePostgresProject) DoUpdate(ctx context.Context, id string, config *PostgresProjectState, entry *PlanEntry) (*PostgresProjectRemote, error) { // Build update mask from fields that have action="update" in the changes map. // This excludes immutable fields and fields that haven't changed. // Prefix with "spec." because the API expects paths relative to the Project object, @@ -119,7 +162,10 @@ func (r *ResourcePostgresProject) DoUpdate(ctx context.Context, id string, confi // Wait for the update to complete result, err := waiter.Wait(ctx) - return result, err + if err != nil { + return nil, err + } + return makePostgresProjectRemote(result), nil } func (r *ResourcePostgresProject) DoDelete(ctx context.Context, id string) error { diff --git a/bundle/direct/dresources/type_test.go b/bundle/direct/dresources/type_test.go index d666b966f04..f47d9ae2b47 100644 --- a/bundle/direct/dresources/type_test.go +++ b/bundle/direct/dresources/type_test.go @@ -44,38 +44,10 @@ var knownMissingInRemoteType = map[string][]string{ "scope_backend_type", }, "postgres_branches": { - "branch_id", - "expire_time", - "is_protected", - "no_expiry", "replace_existing", - "source_branch", - "source_branch_lsn", - "source_branch_time", - "ttl", }, "postgres_endpoints": { - "autoscaling_limit_max_cu", - "autoscaling_limit_min_cu", - "disabled", - "endpoint_id", - "endpoint_type", - "group", - "no_suspension", "replace_existing", - "settings", - "suspend_timeout_duration", - }, - "postgres_projects": { - "budget_policy_id", - "custom_tags", - "default_branch", - "default_endpoint_settings", - "display_name", - "enable_pg_native_login", - "history_retention_duration", - "pg_version", - "project_id", }, "vector_search_endpoints": { "target_qps",