From 686d31b7d68f6b1f60d79fc570d548c3ee6af70a Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Mon, 31 Aug 2026 12:56:46 +0000 Subject: [PATCH 1/5] Add postgres_snapshot_schedules bundle resource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes Add the `postgres_snapshot_schedules` bundle resource, which manages the automatic-snapshot schedule of a Lakebase Postgres branch. The snapshot schedule is a per-branch singleton with no create or delete API — only `GetSnapshotSchedule` and `UpdateSnapshotSchedule`. The resource maps the lifecycle onto that single write call: - create/update set the branch's schedule via `UpdateSnapshotSchedule` (update_mask `schedule`, awaiting the long-running operation); - delete disables automatic snapshots by setting an empty cadence set, since DoDelete also fires when the resource is removed from config, not only on `bundle destroy`; - `branch` composes the schedule's hierarchical name and is a provided id field (a change recreates). Modeled on the existing `postgres_*` resources. Direct engine only: the schedule was added in databricks-sdk-go v0.177.0 and the pinned Terraform provider has no equivalent resource yet, so the acceptance and bind tests are pinned to the direct engine. ## Tests - direct-engine unit CRUD (`bundle/direct/dresources`); - acceptance (direct engine), four end-to-end scenarios, each reading the schedule back with `postgres get-snapshot-schedule`: - `basic`: create with a cadence, in-place cadence update, then remove the resource from config, which disables the schedule (reads back `schedule: null`); - `update`: add the schedule to an already-deployed branch, then remove it; - `out-of-band`: an out-of-band `update-snapshot-schedule` is detected as drift by `bundle plan` (`update postgres_snapshot_schedules.main_schedule`) and reconciled back by deploy; - `orphaned`: removing the branch from config while keeping the schedule that references it — the reference still resolves from the deployed state, so validate passes and plan sequences a branch delete alongside a schedule recreate (documented, not a validation error); - the invariant and bind/unbind suites; - resource-enumeration unit tests (bind support, run_as, permissions, target-mode) extended for the new type. Acceptance goldens regenerated via `-update`; the verify pass is green apart from pre-existing load-induced terraform-engine timeout flakes, each confirmed passing in isolation. Co-authored-by: Isaac --- .../bundles/postgres-snapshot-schedules.md | 1 + .../postgres_snapshot_schedule/databricks.yml | 11 + .../postgres_snapshot_schedule/out.test.toml | 2 + .../postgres_snapshot_schedule/output.txt | 30 +++ .../bind/postgres_snapshot_schedule/script | 6 + .../bind/postgres_snapshot_schedule/test.toml | 26 +++ .../postgres_snapshot_schedule.yml.tmpl | 22 ++ acceptance/bundle/refschema/out.fields.txt | 18 ++ .../basic/databricks.yml.tmpl | 26 +++ .../basic/out.requests.create.txt | 46 ++++ .../basic/out.requests.remove.txt | 28 +++ .../basic/out.requests.update.txt | 41 ++++ .../basic/out.test.toml | 4 + .../basic/output.txt | 93 ++++++++ .../postgres_snapshot_schedules/basic/script | 27 +++ .../orphaned/databricks.yml.tmpl | 26 +++ .../orphaned/out.test.toml | 4 + .../orphaned/output.txt | 61 +++++ .../orphaned/script | 27 +++ .../out-of-band/databricks.yml.tmpl | 26 +++ .../out-of-band/out.test.toml | 4 + .../out-of-band/output.txt | 80 +++++++ .../out-of-band/script | 22 ++ .../script.prepare | 6 + .../postgres_snapshot_schedules/test.toml | 16 ++ .../update/databricks.yml.tmpl | 26 +++ .../update/out.requests.add.txt | 74 ++++++ .../update/out.requests.remove.txt | 40 ++++ .../update/out.test.toml | 4 + .../update/output.txt | 95 ++++++++ .../postgres_snapshot_schedules/update/script | 28 +++ .../apply_bundle_permissions_test.go | 1 + .../resourcemutator/apply_target_mode_test.go | 7 + .../mutator/resourcemutator/run_as_test.go | 2 + bundle/config/resources.go | 133 +++++------ .../resources/postgres_snapshot_schedule.go | 73 ++++++ bundle/config/resources_test.go | 23 +- bundle/direct/dresources/all.go | 69 +++--- bundle/direct/dresources/all_test.go | 37 ++- .../dresources/postgres_snapshot_schedule.go | 131 +++++++++++ bundle/direct/dresources/resources.yml | 8 + bundle/internal/schema/annotations.yml | 13 ++ bundle/schema/jsonschema.json | 214 ++++++++++++++++++ libs/testserver/fake_workspace.go | 18 +- libs/testserver/handlers.go | 16 ++ libs/testserver/postgres.go | 61 +++++ 46 files changed, 1611 insertions(+), 115 deletions(-) create mode 100644 .nextchanges/bundles/postgres-snapshot-schedules.md create mode 100644 acceptance/bundle/deployment/bind/postgres_snapshot_schedule/databricks.yml create mode 100644 acceptance/bundle/deployment/bind/postgres_snapshot_schedule/out.test.toml create mode 100644 acceptance/bundle/deployment/bind/postgres_snapshot_schedule/output.txt create mode 100644 acceptance/bundle/deployment/bind/postgres_snapshot_schedule/script create mode 100644 acceptance/bundle/deployment/bind/postgres_snapshot_schedule/test.toml create mode 100644 acceptance/bundle/invariant/configs/postgres_snapshot_schedule.yml.tmpl create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/basic/databricks.yml.tmpl create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.create.txt create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.remove.txt create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.update.txt create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.test.toml create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/basic/output.txt create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/basic/script create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/databricks.yml.tmpl create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/out.test.toml create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/output.txt create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/script create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/databricks.yml.tmpl create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/out.test.toml create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/output.txt create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/script create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/script.prepare create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/test.toml create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/databricks.yml.tmpl create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.add.txt create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.remove.txt create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/out.test.toml create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/output.txt create mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/script create mode 100644 bundle/config/resources/postgres_snapshot_schedule.go create mode 100644 bundle/direct/dresources/postgres_snapshot_schedule.go diff --git a/.nextchanges/bundles/postgres-snapshot-schedules.md b/.nextchanges/bundles/postgres-snapshot-schedules.md new file mode 100644 index 0000000000..b54efd7611 --- /dev/null +++ b/.nextchanges/bundles/postgres-snapshot-schedules.md @@ -0,0 +1 @@ +Add the `postgres_snapshot_schedules` bundle resource for managing a Lakebase Postgres branch's automatic-snapshot schedule (direct deployment engine only). diff --git a/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/databricks.yml b/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/databricks.yml new file mode 100644 index 0000000000..d680805158 --- /dev/null +++ b/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/databricks.yml @@ -0,0 +1,11 @@ +bundle: + name: test-bundle + +resources: + postgres_snapshot_schedules: + schedule1: + branch: projects/test-project/branches/main + schedule: + - daily_schedule: + hour: 3 + retention: "604800s" diff --git a/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/out.test.toml b/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/out.test.toml new file mode 100644 index 0000000000..0938e67898 --- /dev/null +++ b/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/output.txt b/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/output.txt new file mode 100644 index 0000000000..71102ecfd2 --- /dev/null +++ b/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/output.txt @@ -0,0 +1,30 @@ + +>>> [CLI] bundle deployment bind schedule1 projects/test-project/branches/main/snapshot-schedule --auto-approve +Successfully bound postgres_snapshot_schedule with an id 'projects/test-project/branches/main/snapshot-schedule' +Run 'bundle deploy' to deploy changes to your workspace + +>>> [CLI] bundle summary +Name: test-bundle +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default +Resources: + Postgres snapshot schedules: + schedule1: + Name: + URL: (not deployed) + +>>> [CLI] bundle deployment unbind schedule1 + +>>> [CLI] bundle summary +Name: test-bundle +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default +Resources: + Postgres snapshot schedules: + schedule1: + Name: + URL: (not deployed) diff --git a/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/script b/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/script new file mode 100644 index 0000000000..ee70c6a35b --- /dev/null +++ b/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/script @@ -0,0 +1,6 @@ +SCHEDULE_NAME="projects/test-project/branches/main/snapshot-schedule" +trace $CLI bundle deployment bind schedule1 "${SCHEDULE_NAME}" --auto-approve +trace $CLI bundle summary + +trace $CLI bundle deployment unbind schedule1 +trace $CLI bundle summary diff --git a/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/test.toml b/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/test.toml new file mode 100644 index 0000000000..dc7816a8cd --- /dev/null +++ b/acceptance/bundle/deployment/bind/postgres_snapshot_schedule/test.toml @@ -0,0 +1,26 @@ +Cloud = false + +# The snapshot schedule was added to the Postgres API in databricks-sdk-go +# v0.177.0, which the pinned Terraform provider does not yet include, so there is +# no databricks_..._snapshot_schedule resource. Run the direct engine only. +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] + +Ignore = [ + ".databricks" +] + +[[Server]] +Pattern = "GET /api/2.0/postgres/projects/test-project/branches/main/snapshot-schedule" +Response.Body = ''' +{ + "name": "projects/test-project/branches/main/snapshot-schedule", + "schedule": [ + { + "daily_schedule": { + "hour": 3 + }, + "retention": "604800s" + } + ] +} +''' diff --git a/acceptance/bundle/invariant/configs/postgres_snapshot_schedule.yml.tmpl b/acceptance/bundle/invariant/configs/postgres_snapshot_schedule.yml.tmpl new file mode 100644 index 0000000000..5e36e89886 --- /dev/null +++ b/acceptance/bundle/invariant/configs/postgres_snapshot_schedule.yml.tmpl @@ -0,0 +1,22 @@ +bundle: + name: test-bundle-$UNIQUE_NAME + +resources: + postgres_projects: + project: + project_id: test-pg-project-$UNIQUE_NAME + display_name: Test Postgres Project + + postgres_branches: + foo: + parent: ${resources.postgres_projects.project.name} + branch_id: test-branch-$UNIQUE_NAME + no_expiry: true + + postgres_snapshot_schedules: + foo: + branch: ${resources.postgres_branches.foo.name} + schedule: + - daily_schedule: + hour: 3 + retention: "604800s" diff --git a/acceptance/bundle/refschema/out.fields.txt b/acceptance/bundle/refschema/out.fields.txt index 748939d3c5..b231604a2b 100644 --- a/acceptance/bundle/refschema/out.fields.txt +++ b/acceptance/bundle/refschema/out.fields.txt @@ -3556,6 +3556,24 @@ resources.postgres_roles.*.status.postgres_role string REMOTE resources.postgres_roles.*.status.role_id string REMOTE resources.postgres_roles.*.update_time *time.Time REMOTE resources.postgres_roles.*.url string INPUT +resources.postgres_snapshot_schedules.*.branch string ALL +resources.postgres_snapshot_schedules.*.id string INPUT +resources.postgres_snapshot_schedules.*.lifecycle resources.Lifecycle INPUT +resources.postgres_snapshot_schedules.*.lifecycle.prevent_destroy bool INPUT +resources.postgres_snapshot_schedules.*.modified_status string INPUT +resources.postgres_snapshot_schedules.*.name string REMOTE +resources.postgres_snapshot_schedules.*.schedule []postgres.ScheduleCadence ALL +resources.postgres_snapshot_schedules.*.schedule[*] postgres.ScheduleCadence ALL +resources.postgres_snapshot_schedules.*.schedule[*].daily_schedule *postgres.DailySchedule ALL +resources.postgres_snapshot_schedules.*.schedule[*].daily_schedule.hour int ALL +resources.postgres_snapshot_schedules.*.schedule[*].monthly_schedule *postgres.MonthlySchedule ALL +resources.postgres_snapshot_schedules.*.schedule[*].monthly_schedule.day int ALL +resources.postgres_snapshot_schedules.*.schedule[*].monthly_schedule.hour int ALL +resources.postgres_snapshot_schedules.*.schedule[*].retention duration.Duration ALL +resources.postgres_snapshot_schedules.*.schedule[*].weekly_schedule *postgres.WeeklySchedule ALL +resources.postgres_snapshot_schedules.*.schedule[*].weekly_schedule.day_of_week postgres.DayOfWeek ALL +resources.postgres_snapshot_schedules.*.schedule[*].weekly_schedule.hour int ALL +resources.postgres_snapshot_schedules.*.url string INPUT resources.postgres_synced_tables.*.accelerated_sync bool ALL resources.postgres_synced_tables.*.branch string ALL resources.postgres_synced_tables.*.create_database_objects_if_missing bool ALL diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/basic/databricks.yml.tmpl b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/databricks.yml.tmpl new file mode 100644 index 0000000000..a951ec2a7c --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/databricks.yml.tmpl @@ -0,0 +1,26 @@ +bundle: + name: deploy-pg-ss-basic-$UNIQUE_NAME + +sync: + paths: [] + +resources: + postgres_projects: + my_project: + project_id: test-pg-proj-$UNIQUE_NAME + display_name: "Test Project for Snapshot Schedules" + pg_version: 16 + + postgres_branches: + main: + parent: ${resources.postgres_projects.my_project.id} + branch_id: main + no_expiry: true + + postgres_snapshot_schedules: + main_schedule: + branch: ${resources.postgres_branches.main.id} + schedule: + - daily_schedule: + hour: 3 + retention: "604800s" diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.create.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.create.txt new file mode 100644 index 0000000000..28332f384d --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.create.txt @@ -0,0 +1,46 @@ +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" +} +{ + "method": "PATCH", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "q": { + "update_mask": "schedule" + }, + "body": { + "schedule": [ + { + "daily_schedule": { + "hour": 3 + }, + "retention": "604800s" + } + ] + } +} +{ + "method": "POST", + "path": "/api/2.0/postgres/projects", + "q": { + "project_id": "test-pg-proj-[UNIQUE_NAME]" + }, + "body": { + "spec": { + "display_name": "Test Project for Snapshot Schedules", + "pg_version": 16 + } + } +} +{ + "method": "POST", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches", + "q": { + "branch_id": "main" + }, + "body": { + "spec": { + "no_expiry": true + } + } +} diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.remove.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.remove.txt new file mode 100644 index 0000000000..9b0ebd4682 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.remove.txt @@ -0,0 +1,28 @@ +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" +} +{ + "method": "PATCH", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "q": { + "update_mask": "schedule" + }, + "body": {} +} diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.update.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.update.txt new file mode 100644 index 0000000000..dcaaf4977e --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.requests.update.txt @@ -0,0 +1,41 @@ +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" +} +{ + "method": "PATCH", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "q": { + "update_mask": "schedule" + }, + "body": { + "schedule": [ + { + "daily_schedule": { + "hour": 5 + }, + "retention": "604800s" + } + ] + } +} diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.test.toml b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.test.toml new file mode 100644 index 0000000000..1314e661c2 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/out.test.toml @@ -0,0 +1,4 @@ +Cloud = true +CloudEnvs.azure = false +CloudEnvs.gcp = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/basic/output.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/output.txt new file mode 100644 index 0000000000..1d39ab41bc --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/output.txt @@ -0,0 +1,93 @@ + +=== Create with a snapshot schedule +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-basic-[UNIQUE_NAME]/default/files... +Created postgres_branches.main +Created postgres_projects.my_project +Created postgres_snapshot_schedules.main_schedule +Files: 0 uploaded, 0 deleted +Resources: 3 created, 0 changed, 0 deleted, 0 unchanged + +>>> [CLI] postgres get-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule +{ + "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "schedule": [ + { + "daily_schedule": { + "hour": 3 + }, + "retention": "604800s" + } + ] +} + +=== Update the schedule cadence +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-basic-[UNIQUE_NAME]/default/files... +Updated postgres_snapshot_schedules.main_schedule +Files: 0 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 2 unchanged + +>>> [CLI] postgres get-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule +{ + "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "schedule": [ + { + "daily_schedule": { + "hour": 5 + }, + "retention": "604800s" + } + ] +} + +=== Remove the snapshot schedule resource (disables automatic snapshots) +>>> cat databricks.yml +bundle: + name: deploy-pg-ss-basic-[UNIQUE_NAME] + +sync: + paths: [] + +resources: + postgres_projects: + my_project: + project_id: test-pg-proj-[UNIQUE_NAME] + display_name: "Test Project for Snapshot Schedules" + pg_version: 16 + + postgres_branches: + main: + parent: ${resources.postgres_projects.my_project.id} + branch_id: main + no_expiry: true + + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-basic-[UNIQUE_NAME]/default/files... +Deleted postgres_snapshot_schedules.main_schedule +Files: 0 uploaded, 0 deleted +Resources: 0 created, 0 changed, 1 deleted, 2 unchanged + +>>> [CLI] postgres get-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule +{ + "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "schedule": null +} + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.postgres_branches.main + delete resources.postgres_projects.my_project + +This action will result in the deletion of the following Lakebase projects along with +all their branches, databases, and endpoints. All data stored in them will be permanently lost: + delete resources.postgres_projects.my_project + +This action will result in the deletion of the following Lakebase branches. +All data stored in them will be permanently lost: + delete resources.postgres_branches.main + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-basic-[UNIQUE_NAME]/default + +Destroy: 2 deleted diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/basic/script b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/script new file mode 100644 index 0000000000..0e7cbe6b7c --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/script @@ -0,0 +1,27 @@ +cleanup() { + trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +schedule_name="projects/test-pg-proj-${UNIQUE_NAME}/branches/main/snapshot-schedule" + +title "Create with a snapshot schedule" +envsubst < databricks.yml.tmpl > databricks.yml +rm -f out.requests.txt +trace $CLI bundle deploy +trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields +print_requests.py --del-body parent,project_id,branch_id --sort --get '//postgres' '^//workspace-files/' '^//workspace/' '^//telemetry-ext' '^//operations/' > out.requests.create.txt + +title "Update the schedule cadence" +sed "s/hour: 3/hour: 5/" databricks.yml > databricks.yml.new && mv databricks.yml.new databricks.yml +trace $CLI bundle deploy --auto-approve +trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields +print_requests.py --del-body parent,project_id,branch_id --sort --get '//postgres' '^//workspace-files/' '^//workspace/' '^//telemetry-ext' '^//operations/' > out.requests.update.txt + +title "Remove the snapshot schedule resource (disables automatic snapshots)" +sed '/postgres_snapshot_schedules:/,$d' databricks.yml > databricks.yml.new && mv databricks.yml.new databricks.yml +trace cat databricks.yml +trace $CLI bundle deploy --auto-approve +trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields +print_requests.py --del-body parent,project_id,branch_id --sort --get '//postgres' '^//workspace-files/' '^//workspace/' '^//telemetry-ext' '^//operations/' > out.requests.remove.txt diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/databricks.yml.tmpl b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/databricks.yml.tmpl new file mode 100644 index 0000000000..f2ad5af11a --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/databricks.yml.tmpl @@ -0,0 +1,26 @@ +bundle: + name: deploy-pg-ss-orphaned-$UNIQUE_NAME + +sync: + paths: [] + +resources: + postgres_projects: + my_project: + project_id: test-pg-proj-$UNIQUE_NAME + display_name: "Test Project for Snapshot Schedules" + pg_version: 16 + + postgres_branches: + main: + parent: ${resources.postgres_projects.my_project.id} + branch_id: main + no_expiry: true + + postgres_snapshot_schedules: + main_schedule: + branch: ${resources.postgres_branches.main.id} + schedule: + - daily_schedule: + hour: 3 + retention: "604800s" diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/out.test.toml b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/out.test.toml new file mode 100644 index 0000000000..1314e661c2 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/out.test.toml @@ -0,0 +1,4 @@ +Cloud = true +CloudEnvs.azure = false +CloudEnvs.gcp = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/output.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/output.txt new file mode 100644 index 0000000000..5cc6d36335 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/output.txt @@ -0,0 +1,61 @@ + +=== Deploy project + branch + snapshot schedule +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-orphaned-[UNIQUE_NAME]/default/files... +Created postgres_branches.main +Created postgres_projects.my_project +Created postgres_snapshot_schedules.main_schedule +Files: 0 uploaded, 0 deleted +Resources: 3 created, 0 changed, 0 deleted, 0 unchanged + +=== Remove the branch but keep the schedule that references it +>>> cat databricks.yml +bundle: + name: deploy-pg-ss-orphaned-[UNIQUE_NAME] + +sync: + paths: [] + +resources: + postgres_projects: + my_project: + project_id: test-pg-proj-[UNIQUE_NAME] + display_name: "Test Project for Snapshot Schedules" + pg_version: 16 + + postgres_snapshot_schedules: + main_schedule: + branch: ${resources.postgres_branches.main.id} + schedule: + - daily_schedule: + hour: 3 + retention: "604800s" +Name: deploy-pg-ss-orphaned-[UNIQUE_NAME] +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-orphaned-[UNIQUE_NAME]/default + +Validation OK! +delete postgres_branches.main +recreate postgres_snapshot_schedules.main_schedule + +Plan: 1 to add, 0 to change, 2 to delete, 1 unchanged + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.postgres_branches.main + delete resources.postgres_projects.my_project + delete resources.postgres_snapshot_schedules.main_schedule + +This action will result in the deletion of the following Lakebase projects along with +all their branches, databases, and endpoints. All data stored in them will be permanently lost: + delete resources.postgres_projects.my_project + +This action will result in the deletion of the following Lakebase branches. +All data stored in them will be permanently lost: + delete resources.postgres_branches.main + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-orphaned-[UNIQUE_NAME]/default + +Destroy: 3 deleted diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/script b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/script new file mode 100644 index 0000000000..f89a3ad4a8 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/script @@ -0,0 +1,27 @@ +cleanup() { + trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +title "Deploy project + branch + snapshot schedule" +envsubst < databricks.yml.tmpl > databricks.yml +trace $CLI bundle deploy + +# Remove ONLY the branch from config while keeping the snapshot schedule, which +# still references ${resources.postgres_branches.main.id}. The reference still +# resolves from the deployed state (the branch is present in state during the +# transition), so this does NOT fail as a dangling reference: validate passes and +# plan sequences a delete of the branch alongside a recreate of the schedule. +# Deploying that plan would fail at apply (the schedule cannot be created on a +# deleted branch); this test documents the plan-time behavior. Acceptable user +# error -- removing a parent while keeping a child that references it. +# errcode records the exit code so the outcome is captured deterministically. +title "Remove the branch but keep the schedule that references it" +sed '/^ postgres_branches:/,/^ postgres_snapshot_schedules:/{/^ postgres_snapshot_schedules:/!d}' databricks.yml > databricks.yml.new && mv databricks.yml.new databricks.yml +trace cat databricks.yml +errcode $CLI bundle validate +errcode $CLI bundle plan + +# Restore the full config so the destroy in cleanup can resolve references. +envsubst < databricks.yml.tmpl > databricks.yml diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/databricks.yml.tmpl b/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/databricks.yml.tmpl new file mode 100644 index 0000000000..e24897277b --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/databricks.yml.tmpl @@ -0,0 +1,26 @@ +bundle: + name: deploy-pg-ss-oob-$UNIQUE_NAME + +sync: + paths: [] + +resources: + postgres_projects: + my_project: + project_id: test-pg-proj-$UNIQUE_NAME + display_name: "Test Project for Snapshot Schedules" + pg_version: 16 + + postgres_branches: + main: + parent: ${resources.postgres_projects.my_project.id} + branch_id: main + no_expiry: true + + postgres_snapshot_schedules: + main_schedule: + branch: ${resources.postgres_branches.main.id} + schedule: + - daily_schedule: + hour: 3 + retention: "604800s" diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/out.test.toml b/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/out.test.toml new file mode 100644 index 0000000000..1314e661c2 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/out.test.toml @@ -0,0 +1,4 @@ +Cloud = true +CloudEnvs.azure = false +CloudEnvs.gcp = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/output.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/output.txt new file mode 100644 index 0000000000..41786d6076 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/output.txt @@ -0,0 +1,80 @@ + +=== Deploy with a snapshot schedule +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-oob-[UNIQUE_NAME]/default/files... +Created postgres_branches.main +Created postgres_projects.my_project +Created postgres_snapshot_schedules.main_schedule +Files: 0 uploaded, 0 deleted +Resources: 3 created, 0 changed, 0 deleted, 0 unchanged + +>>> [CLI] postgres get-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule +{ + "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "schedule": [ + { + "daily_schedule": { + "hour": 3 + }, + "retention": "604800s" + } + ] +} + +=== Change the schedule out of band via a direct API call +>>> [CLI] postgres update-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule schedule --json {"schedule":[{"daily_schedule":{"hour":9},"retention":"604800s"}]} +{ + "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "schedule": [ + { + "daily_schedule": { + "hour": 9 + }, + "retention": "604800s" + } + ] +} + +=== Plan detects the drift +>>> [CLI] bundle plan +update postgres_snapshot_schedules.main_schedule + +Plan: 0 to add, 1 to change, 0 to delete, 2 unchanged + +=== Deploy reconciles the schedule back to the configured cadence +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-oob-[UNIQUE_NAME]/default/files... +Updated postgres_snapshot_schedules.main_schedule +Files: 0 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 2 unchanged + +>>> [CLI] postgres get-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule +{ + "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "schedule": [ + { + "daily_schedule": { + "hour": 3 + }, + "retention": "604800s" + } + ] +} + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.postgres_branches.main + delete resources.postgres_projects.my_project + delete resources.postgres_snapshot_schedules.main_schedule + +This action will result in the deletion of the following Lakebase projects along with +all their branches, databases, and endpoints. All data stored in them will be permanently lost: + delete resources.postgres_projects.my_project + +This action will result in the deletion of the following Lakebase branches. +All data stored in them will be permanently lost: + delete resources.postgres_branches.main + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-oob-[UNIQUE_NAME]/default + +Destroy: 3 deleted diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/script b/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/script new file mode 100644 index 0000000000..59d2cf6254 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/out-of-band/script @@ -0,0 +1,22 @@ +cleanup() { + trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +schedule_name="projects/test-pg-proj-${UNIQUE_NAME}/branches/main/snapshot-schedule" + +title "Deploy with a snapshot schedule" +envsubst < databricks.yml.tmpl > databricks.yml +trace $CLI bundle deploy +trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields + +title "Change the schedule out of band via a direct API call" +trace $CLI postgres update-snapshot-schedule "${schedule_name}" schedule --json '{"schedule":[{"daily_schedule":{"hour":9},"retention":"604800s"}]}' | snapshot_schedule_fields + +title "Plan detects the drift" +trace $CLI bundle plan + +title "Deploy reconciles the schedule back to the configured cadence" +trace $CLI bundle deploy --auto-approve +trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/script.prepare b/acceptance/bundle/resources/postgres_snapshot_schedules/script.prepare new file mode 100644 index 0000000000..ce34a2474d --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/script.prepare @@ -0,0 +1,6 @@ +# snapshot_schedule_fields is the allow-list of snapshot-schedule fields the +# acceptance tests assert on. Anything the backend returns outside this list is +# dropped, so a new field in the API response does not break the golden. +snapshot_schedule_fields() { + jq '{name, schedule}' +} diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/test.toml b/acceptance/bundle/resources/postgres_snapshot_schedules/test.toml new file mode 100644 index 0000000000..888670d085 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/test.toml @@ -0,0 +1,16 @@ +Cloud = true + +# Lakebase v2 (postgres) is only available in AWS as of January 2026 +CloudEnvs.gcp = false +CloudEnvs.azure = false + +# The snapshot schedule was added to the Postgres API in databricks-sdk-go +# v0.177.0, which the pinned Terraform provider does not yet include, so there is +# no databricks_..._snapshot_schedule resource to deploy through. Run the direct +# engine only until the provider catches up. +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] + +Ignore = [ + "databricks.yml", + ".databricks", +] diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/databricks.yml.tmpl b/acceptance/bundle/resources/postgres_snapshot_schedules/update/databricks.yml.tmpl new file mode 100644 index 0000000000..0ea6ec1341 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/update/databricks.yml.tmpl @@ -0,0 +1,26 @@ +bundle: + name: deploy-pg-ss-update-$UNIQUE_NAME + +sync: + paths: [] + +resources: + postgres_projects: + my_project: + project_id: test-pg-proj-$UNIQUE_NAME + display_name: "Test Project for Snapshot Schedules" + pg_version: 16 + + postgres_branches: + main: + parent: ${resources.postgres_projects.my_project.id} + branch_id: main + no_expiry: true + + postgres_snapshot_schedules: + main_schedule: + branch: ${resources.postgres_branches.main.id} + schedule: + - daily_schedule: + hour: 3 + retention: "604800s" diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.add.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.add.txt new file mode 100644 index 0000000000..9c5f1edf5c --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.add.txt @@ -0,0 +1,74 @@ +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" +} +{ + "method": "PATCH", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "q": { + "update_mask": "schedule" + }, + "body": { + "schedule": [ + { + "daily_schedule": { + "hour": 3 + }, + "retention": "604800s" + } + ] + } +} +{ + "method": "POST", + "path": "/api/2.0/postgres/projects", + "q": { + "project_id": "test-pg-proj-[UNIQUE_NAME]" + }, + "body": { + "spec": { + "display_name": "Test Project for Snapshot Schedules", + "pg_version": 16 + } + } +} +{ + "method": "POST", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches", + "q": { + "branch_id": "main" + }, + "body": { + "spec": { + "no_expiry": true + } + } +} diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.remove.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.remove.txt new file mode 100644 index 0000000000..bea13bc3b2 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.remove.txt @@ -0,0 +1,40 @@ +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" +} +{ + "method": "GET", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" +} +{ + "method": "PATCH", + "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "q": { + "update_mask": "schedule" + }, + "body": {} +} diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.test.toml b/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.test.toml new file mode 100644 index 0000000000..1314e661c2 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.test.toml @@ -0,0 +1,4 @@ +Cloud = true +CloudEnvs.azure = false +CloudEnvs.gcp = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/output.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/update/output.txt new file mode 100644 index 0000000000..1672fc03e0 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/update/output.txt @@ -0,0 +1,95 @@ + +=== Deploy project + branch without a snapshot schedule +>>> cat databricks.yml +bundle: + name: deploy-pg-ss-update-[UNIQUE_NAME] + +sync: + paths: [] + +resources: + postgres_projects: + my_project: + project_id: test-pg-proj-[UNIQUE_NAME] + display_name: "Test Project for Snapshot Schedules" + pg_version: 16 + + postgres_branches: + main: + parent: ${resources.postgres_projects.my_project.id} + branch_id: main + no_expiry: true + + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-update-[UNIQUE_NAME]/default/files... +Created postgres_branches.main +Created postgres_projects.my_project +Files: 0 uploaded, 0 deleted +Resources: 2 created, 0 changed, 0 deleted, 0 unchanged + +>>> [CLI] postgres get-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule +{ + "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "schedule": null +} + +=== Add the snapshot schedule resource to the existing branch +>>> [CLI] bundle plan +create postgres_snapshot_schedules.main_schedule + +Plan: 1 to add, 0 to change, 0 to delete, 2 unchanged + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-update-[UNIQUE_NAME]/default/files... +Created postgres_snapshot_schedules.main_schedule +Files: 0 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 2 unchanged + +>>> [CLI] postgres get-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule +{ + "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "schedule": [ + { + "daily_schedule": { + "hour": 3 + }, + "retention": "604800s" + } + ] +} + +=== Remove the snapshot schedule resource from the existing branch +>>> [CLI] bundle plan +delete postgres_snapshot_schedules.main_schedule + +Plan: 0 to add, 0 to change, 1 to delete, 2 unchanged + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-update-[UNIQUE_NAME]/default/files... +Deleted postgres_snapshot_schedules.main_schedule +Files: 0 uploaded, 0 deleted +Resources: 0 created, 0 changed, 1 deleted, 2 unchanged + +>>> [CLI] postgres get-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule +{ + "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", + "schedule": null +} + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.postgres_branches.main + delete resources.postgres_projects.my_project + +This action will result in the deletion of the following Lakebase projects along with +all their branches, databases, and endpoints. All data stored in them will be permanently lost: + delete resources.postgres_projects.my_project + +This action will result in the deletion of the following Lakebase branches. +All data stored in them will be permanently lost: + delete resources.postgres_branches.main + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-update-[UNIQUE_NAME]/default + +Destroy: 2 deleted diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/script b/acceptance/bundle/resources/postgres_snapshot_schedules/update/script new file mode 100644 index 0000000000..6f69cc6810 --- /dev/null +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/update/script @@ -0,0 +1,28 @@ +cleanup() { + trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +schedule_name="projects/test-pg-proj-${UNIQUE_NAME}/branches/main/snapshot-schedule" + +title "Deploy project + branch without a snapshot schedule" +envsubst < databricks.yml.tmpl | sed '/postgres_snapshot_schedules:/,$d' > databricks.yml +trace cat databricks.yml +rm -f out.requests.txt +trace $CLI bundle deploy +trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields + +title "Add the snapshot schedule resource to the existing branch" +envsubst < databricks.yml.tmpl > databricks.yml +trace $CLI bundle plan +trace $CLI bundle deploy --auto-approve +trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields +print_requests.py --del-body parent,project_id,branch_id --sort --get '//postgres' '^//workspace-files/' '^//workspace/' '^//telemetry-ext' '^//operations/' > out.requests.add.txt + +title "Remove the snapshot schedule resource from the existing branch" +sed '/postgres_snapshot_schedules:/,$d' databricks.yml > databricks.yml.new && mv databricks.yml.new databricks.yml +trace $CLI bundle plan +trace $CLI bundle deploy --auto-approve +trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields +print_requests.py --del-body parent,project_id,branch_id --sort --get '//postgres' '^//workspace-files/' '^//workspace/' '^//telemetry-ext' '^//operations/' > out.requests.remove.txt diff --git a/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go b/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go index 99ac6759ee..27202a08da 100644 --- a/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go +++ b/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go @@ -32,6 +32,7 @@ var unsupportedResources = []string{ "postgres_endpoints", "postgres_catalogs", "postgres_roles", + "postgres_snapshot_schedules", "postgres_synced_tables", "vector_search_indexes", "job_runs", diff --git a/bundle/config/mutator/resourcemutator/apply_target_mode_test.go b/bundle/config/mutator/resourcemutator/apply_target_mode_test.go index 08c76f8433..eaf3351676 100644 --- a/bundle/config/mutator/resourcemutator/apply_target_mode_test.go +++ b/bundle/config/mutator/resourcemutator/apply_target_mode_test.go @@ -305,6 +305,13 @@ func mockBundle(mode config.Mode) *bundle.Bundle { }, }, }, + PostgresSnapshotSchedules: map[string]*resources.PostgresSnapshotSchedule{ + "postgres_snapshot_schedule1": { + PostgresSnapshotScheduleConfig: resources.PostgresSnapshotScheduleConfig{ + Branch: "projects/project1/branches/branch1", + }, + }, + }, VectorSearchEndpoints: map[string]*resources.VectorSearchEndpoint{ "vs_endpoint1": { CreateEndpoint: vectorsearch.CreateEndpoint{ diff --git a/bundle/config/mutator/resourcemutator/run_as_test.go b/bundle/config/mutator/resourcemutator/run_as_test.go index be9abfe683..1df273c44b 100644 --- a/bundle/config/mutator/resourcemutator/run_as_test.go +++ b/bundle/config/mutator/resourcemutator/run_as_test.go @@ -56,6 +56,7 @@ func allResourceTypes(t *testing.T) []string { "postgres_endpoints", "postgres_projects", "postgres_roles", + "postgres_snapshot_schedules", "postgres_synced_tables", "quality_monitors", "registered_models", @@ -191,6 +192,7 @@ var allowList = []string{ "postgres_endpoints", "postgres_projects", "postgres_roles", + "postgres_snapshot_schedules", "postgres_synced_tables", "registered_models", "experiments", diff --git a/bundle/config/resources.go b/bundle/config/resources.go index 633332a78c..b5e68d992e 100644 --- a/bundle/config/resources.go +++ b/bundle/config/resources.go @@ -15,37 +15,38 @@ type Resources struct { JobRuns map[string]*resources.JobRun `json:"job_runs,omitempty"` Pipelines map[string]*resources.Pipeline `json:"pipelines,omitempty"` - Models map[string]*resources.MlflowModel `json:"models,omitempty"` - Experiments map[string]*resources.MlflowExperiment `json:"experiments,omitempty"` - ModelServingEndpoints map[string]*resources.ModelServingEndpoint `json:"model_serving_endpoints,omitempty"` - RegisteredModels map[string]*resources.RegisteredModel `json:"registered_models,omitempty"` - QualityMonitors map[string]*resources.QualityMonitor `json:"quality_monitors,omitempty"` - Catalogs map[string]*resources.Catalog `json:"catalogs,omitempty"` - Schemas map[string]*resources.Schema `json:"schemas,omitempty"` - Volumes map[string]*resources.Volume `json:"volumes,omitempty"` - ExternalLocations map[string]*resources.ExternalLocation `json:"external_locations,omitempty"` - Clusters map[string]*resources.Cluster `json:"clusters,omitempty"` - Dashboards map[string]*resources.Dashboard `json:"dashboards,omitempty"` - GenieSpaces map[string]*resources.GenieSpace `json:"genie_spaces,omitempty"` - Apps map[string]*resources.App `json:"apps,omitempty"` - SecretScopes map[string]*resources.SecretScope `json:"secret_scopes,omitempty"` - Alerts map[string]*resources.Alert `json:"alerts,omitempty"` - SqlWarehouses map[string]*resources.SqlWarehouse `json:"sql_warehouses,omitempty"` - DatabaseInstances map[string]*resources.DatabaseInstance `json:"database_instances,omitempty"` - DatabaseCatalogs map[string]*resources.DatabaseCatalog `json:"database_catalogs,omitempty"` - SyncedDatabaseTables map[string]*resources.SyncedDatabaseTable `json:"synced_database_tables,omitempty"` - PostgresProjects map[string]*resources.PostgresProject `json:"postgres_projects,omitempty"` - PostgresBranches map[string]*resources.PostgresBranch `json:"postgres_branches,omitempty"` - PostgresEndpoints map[string]*resources.PostgresEndpoint `json:"postgres_endpoints,omitempty"` - PostgresCatalogs map[string]*resources.PostgresCatalog `json:"postgres_catalogs,omitempty"` - PostgresDatabases map[string]*resources.PostgresDatabase `json:"postgres_databases,omitempty"` - PostgresRoles map[string]*resources.PostgresRole `json:"postgres_roles,omitempty"` - PostgresSyncedTables map[string]*resources.PostgresSyncedTable `json:"postgres_synced_tables,omitempty"` - VectorSearchEndpoints map[string]*resources.VectorSearchEndpoint `json:"vector_search_endpoints,omitempty"` - VectorSearchIndexes map[string]*resources.VectorSearchIndex `json:"vector_search_indexes,omitempty"` - InstancePools map[string]*resources.InstancePool `json:"instance_pools,omitempty"` - Secrets map[string]*resources.Secret `json:"secrets,omitempty"` - ClusterPolicies map[string]*resources.ClusterPolicy `json:"cluster_policies,omitempty"` + Models map[string]*resources.MlflowModel `json:"models,omitempty"` + Experiments map[string]*resources.MlflowExperiment `json:"experiments,omitempty"` + ModelServingEndpoints map[string]*resources.ModelServingEndpoint `json:"model_serving_endpoints,omitempty"` + RegisteredModels map[string]*resources.RegisteredModel `json:"registered_models,omitempty"` + QualityMonitors map[string]*resources.QualityMonitor `json:"quality_monitors,omitempty"` + Catalogs map[string]*resources.Catalog `json:"catalogs,omitempty"` + Schemas map[string]*resources.Schema `json:"schemas,omitempty"` + Volumes map[string]*resources.Volume `json:"volumes,omitempty"` + ExternalLocations map[string]*resources.ExternalLocation `json:"external_locations,omitempty"` + Clusters map[string]*resources.Cluster `json:"clusters,omitempty"` + Dashboards map[string]*resources.Dashboard `json:"dashboards,omitempty"` + GenieSpaces map[string]*resources.GenieSpace `json:"genie_spaces,omitempty"` + Apps map[string]*resources.App `json:"apps,omitempty"` + SecretScopes map[string]*resources.SecretScope `json:"secret_scopes,omitempty"` + Alerts map[string]*resources.Alert `json:"alerts,omitempty"` + SqlWarehouses map[string]*resources.SqlWarehouse `json:"sql_warehouses,omitempty"` + DatabaseInstances map[string]*resources.DatabaseInstance `json:"database_instances,omitempty"` + DatabaseCatalogs map[string]*resources.DatabaseCatalog `json:"database_catalogs,omitempty"` + SyncedDatabaseTables map[string]*resources.SyncedDatabaseTable `json:"synced_database_tables,omitempty"` + PostgresProjects map[string]*resources.PostgresProject `json:"postgres_projects,omitempty"` + PostgresBranches map[string]*resources.PostgresBranch `json:"postgres_branches,omitempty"` + PostgresEndpoints map[string]*resources.PostgresEndpoint `json:"postgres_endpoints,omitempty"` + PostgresCatalogs map[string]*resources.PostgresCatalog `json:"postgres_catalogs,omitempty"` + PostgresDatabases map[string]*resources.PostgresDatabase `json:"postgres_databases,omitempty"` + PostgresRoles map[string]*resources.PostgresRole `json:"postgres_roles,omitempty"` + PostgresSyncedTables map[string]*resources.PostgresSyncedTable `json:"postgres_synced_tables,omitempty"` + PostgresSnapshotSchedules map[string]*resources.PostgresSnapshotSchedule `json:"postgres_snapshot_schedules,omitempty"` + VectorSearchEndpoints map[string]*resources.VectorSearchEndpoint `json:"vector_search_endpoints,omitempty"` + VectorSearchIndexes map[string]*resources.VectorSearchIndex `json:"vector_search_indexes,omitempty"` + InstancePools map[string]*resources.InstancePool `json:"instance_pools,omitempty"` + Secrets map[string]*resources.Secret `json:"secrets,omitempty"` + ClusterPolicies map[string]*resources.ClusterPolicy `json:"cluster_policies,omitempty"` } type ConfigResource interface { @@ -128,6 +129,7 @@ func (r *Resources) AllResources() []ResourceGroup { collectResourceMap(descriptions["postgres_databases"], r.PostgresDatabases), collectResourceMap(descriptions["postgres_roles"], r.PostgresRoles), collectResourceMap(descriptions["postgres_synced_tables"], r.PostgresSyncedTables), + collectResourceMap(descriptions["postgres_snapshot_schedules"], r.PostgresSnapshotSchedules), collectResourceMap(descriptions["vector_search_endpoints"], r.VectorSearchEndpoints), collectResourceMap(descriptions["vector_search_indexes"], r.VectorSearchIndexes), collectResourceMap(descriptions["instance_pools"], r.InstancePools), @@ -164,39 +166,40 @@ func (r *Resources) FindResourceByConfigKey(key string) (ConfigResource, error) // SupportedResources returns a map which keys correspond to the resource key in the bundle configuration. func SupportedResources() map[string]resources.ResourceDescription { return map[string]resources.ResourceDescription{ - "jobs": (&resources.Job{}).ResourceDescription(), - "job_runs": (&resources.JobRun{}).ResourceDescription(), - "pipelines": (&resources.Pipeline{}).ResourceDescription(), - "models": (&resources.MlflowModel{}).ResourceDescription(), - "experiments": (&resources.MlflowExperiment{}).ResourceDescription(), - "instance_pools": (&resources.InstancePool{}).ResourceDescription(), - "model_serving_endpoints": (&resources.ModelServingEndpoint{}).ResourceDescription(), - "registered_models": (&resources.RegisteredModel{}).ResourceDescription(), - "quality_monitors": (&resources.QualityMonitor{}).ResourceDescription(), - "catalogs": (&resources.Catalog{}).ResourceDescription(), - "schemas": (&resources.Schema{}).ResourceDescription(), - "external_locations": (&resources.ExternalLocation{}).ResourceDescription(), - "clusters": (&resources.Cluster{}).ResourceDescription(), - "dashboards": (&resources.Dashboard{}).ResourceDescription(), - "genie_spaces": (&resources.GenieSpace{}).ResourceDescription(), - "volumes": (&resources.Volume{}).ResourceDescription(), - "apps": (&resources.App{}).ResourceDescription(), - "secret_scopes": (&resources.SecretScope{}).ResourceDescription(), - "alerts": (&resources.Alert{}).ResourceDescription(), - "sql_warehouses": (&resources.SqlWarehouse{}).ResourceDescription(), - "database_instances": (&resources.DatabaseInstance{}).ResourceDescription(), - "database_catalogs": (&resources.DatabaseCatalog{}).ResourceDescription(), - "synced_database_tables": (&resources.SyncedDatabaseTable{}).ResourceDescription(), - "postgres_projects": (&resources.PostgresProject{}).ResourceDescription(), - "postgres_branches": (&resources.PostgresBranch{}).ResourceDescription(), - "postgres_endpoints": (&resources.PostgresEndpoint{}).ResourceDescription(), - "postgres_catalogs": (&resources.PostgresCatalog{}).ResourceDescription(), - "postgres_databases": (&resources.PostgresDatabase{}).ResourceDescription(), - "postgres_roles": (&resources.PostgresRole{}).ResourceDescription(), - "postgres_synced_tables": (&resources.PostgresSyncedTable{}).ResourceDescription(), - "vector_search_endpoints": (&resources.VectorSearchEndpoint{}).ResourceDescription(), - "vector_search_indexes": (&resources.VectorSearchIndex{}).ResourceDescription(), - "secrets": (&resources.Secret{}).ResourceDescription(), - "cluster_policies": (&resources.ClusterPolicy{}).ResourceDescription(), + "jobs": (&resources.Job{}).ResourceDescription(), + "job_runs": (&resources.JobRun{}).ResourceDescription(), + "pipelines": (&resources.Pipeline{}).ResourceDescription(), + "models": (&resources.MlflowModel{}).ResourceDescription(), + "experiments": (&resources.MlflowExperiment{}).ResourceDescription(), + "instance_pools": (&resources.InstancePool{}).ResourceDescription(), + "model_serving_endpoints": (&resources.ModelServingEndpoint{}).ResourceDescription(), + "registered_models": (&resources.RegisteredModel{}).ResourceDescription(), + "quality_monitors": (&resources.QualityMonitor{}).ResourceDescription(), + "catalogs": (&resources.Catalog{}).ResourceDescription(), + "schemas": (&resources.Schema{}).ResourceDescription(), + "external_locations": (&resources.ExternalLocation{}).ResourceDescription(), + "clusters": (&resources.Cluster{}).ResourceDescription(), + "dashboards": (&resources.Dashboard{}).ResourceDescription(), + "genie_spaces": (&resources.GenieSpace{}).ResourceDescription(), + "volumes": (&resources.Volume{}).ResourceDescription(), + "apps": (&resources.App{}).ResourceDescription(), + "secret_scopes": (&resources.SecretScope{}).ResourceDescription(), + "alerts": (&resources.Alert{}).ResourceDescription(), + "sql_warehouses": (&resources.SqlWarehouse{}).ResourceDescription(), + "database_instances": (&resources.DatabaseInstance{}).ResourceDescription(), + "database_catalogs": (&resources.DatabaseCatalog{}).ResourceDescription(), + "synced_database_tables": (&resources.SyncedDatabaseTable{}).ResourceDescription(), + "postgres_projects": (&resources.PostgresProject{}).ResourceDescription(), + "postgres_branches": (&resources.PostgresBranch{}).ResourceDescription(), + "postgres_endpoints": (&resources.PostgresEndpoint{}).ResourceDescription(), + "postgres_catalogs": (&resources.PostgresCatalog{}).ResourceDescription(), + "postgres_databases": (&resources.PostgresDatabase{}).ResourceDescription(), + "postgres_roles": (&resources.PostgresRole{}).ResourceDescription(), + "postgres_synced_tables": (&resources.PostgresSyncedTable{}).ResourceDescription(), + "postgres_snapshot_schedules": (&resources.PostgresSnapshotSchedule{}).ResourceDescription(), + "vector_search_endpoints": (&resources.VectorSearchEndpoint{}).ResourceDescription(), + "vector_search_indexes": (&resources.VectorSearchIndex{}).ResourceDescription(), + "secrets": (&resources.Secret{}).ResourceDescription(), + "cluster_policies": (&resources.ClusterPolicy{}).ResourceDescription(), } } diff --git a/bundle/config/resources/postgres_snapshot_schedule.go b/bundle/config/resources/postgres_snapshot_schedule.go new file mode 100644 index 0000000000..374aa55f35 --- /dev/null +++ b/bundle/config/resources/postgres_snapshot_schedule.go @@ -0,0 +1,73 @@ +package resources + +import ( + "context" + "net/url" + + "github.com/databricks/cli/libs/log" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/marshal" + "github.com/databricks/databricks-sdk-go/service/postgres" +) + +type PostgresSnapshotScheduleConfig struct { + // Branch is the branch whose automatic-snapshot schedule this resource manages. + // Format: "projects/{project_id}/branches/{branch_id}". The schedule's resource + // name (and this resource's ID) is "{branch}/snapshot-schedule". + Branch string `json:"branch"` + + // Schedule is the set of cadences at which automatic snapshots are taken. An + // empty set disables automatic snapshots. Order is not significant; when + // several cadences fire together a single snapshot is taken, retained for the + // longest of their retentions. + Schedule []postgres.ScheduleCadence `json:"schedule,omitempty"` + + // ForceSendFields tracks zero-value top-level fields (branch) for the SDK's + // marshal package. + ForceSendFields []string `json:"-" url:"-"` +} + +func (c *PostgresSnapshotScheduleConfig) UnmarshalJSON(b []byte) error { + return marshal.Unmarshal(b, c) +} + +func (c *PostgresSnapshotScheduleConfig) MarshalJSON() ([]byte, error) { + return marshal.Marshal(c) +} + +type PostgresSnapshotSchedule struct { + BaseResource + PostgresSnapshotScheduleConfig +} + +func (b *PostgresSnapshotSchedule) Exists(ctx context.Context, w *databricks.WorkspaceClient, name string) (bool, error) { + _, err := w.Postgres.GetSnapshotSchedule(ctx, postgres.GetSnapshotScheduleRequest{Name: name}) + if err != nil { + log.Debugf(ctx, "postgres snapshot schedule %s does not exist", name) + return false, err + } + return true, nil +} + +func (b *PostgresSnapshotSchedule) ResourceDescription() ResourceDescription { + return ResourceDescription{ + SingularName: "postgres_snapshot_schedule", + PluralName: "postgres_snapshot_schedules", + SingularTitle: "Postgres snapshot schedule", + PluralTitle: "Postgres snapshot schedules", + } +} + +func (b *PostgresSnapshotSchedule) GetName() string { + // Snapshot schedules don't have a user-visible name field. + return "" +} + +func (b *PostgresSnapshotSchedule) GetURL() string { + // The IDs in the API do not (yet) map to IDs in the web UI. + return "" +} + +func (b *PostgresSnapshotSchedule) InitializeURL(_ url.URL) { + // The IDs in the API do not (yet) map to IDs in the web UI. +} diff --git a/bundle/config/resources_test.go b/bundle/config/resources_test.go index 7e56f47a64..dfef773ff8 100644 --- a/bundle/config/resources_test.go +++ b/bundle/config/resources_test.go @@ -128,13 +128,14 @@ func TestBundleResourcePluralNamesResolveInWorkspaceURLs(t *testing.T) { // A job run does have a workspace URL, but it's addressed by two IDs // (job + run) so it can't be expressed as a single-ID pattern here; it's // built in JobRun.InitializeURL via workspaceurls.JobRunURL instead. - "job_runs": true, - "postgres_branches": true, - "postgres_databases": true, - "postgres_endpoints": true, - "postgres_projects": true, - "postgres_roles": true, - "secret_scopes": true, + "job_runs": true, + "postgres_branches": true, + "postgres_databases": true, + "postgres_endpoints": true, + "postgres_projects": true, + "postgres_roles": true, + "postgres_snapshot_schedules": true, + "secret_scopes": true, } supported := SupportedResources() @@ -334,6 +335,13 @@ func TestResourcesBindSupport(t *testing.T) { }, }, }, + PostgresSnapshotSchedules: map[string]*resources.PostgresSnapshotSchedule{ + "my_postgres_snapshot_schedule": { + PostgresSnapshotScheduleConfig: resources.PostgresSnapshotScheduleConfig{ + Branch: "projects/my-postgres-project/branches/my-postgres-branch", + }, + }, + }, VectorSearchEndpoints: map[string]*resources.VectorSearchEndpoint{ "my_vector_search_endpoint": { CreateEndpoint: vectorsearch.CreateEndpoint{ @@ -391,6 +399,7 @@ func TestResourcesBindSupport(t *testing.T) { m.GetMockPostgresAPI().EXPECT().GetDatabase(mock.Anything, mock.Anything).Return(nil, nil) m.GetMockPostgresAPI().EXPECT().GetRole(mock.Anything, mock.Anything).Return(nil, nil) m.GetMockPostgresAPI().EXPECT().GetSyncedTable(mock.Anything, mock.Anything).Return(nil, nil) + m.GetMockPostgresAPI().EXPECT().GetSnapshotSchedule(mock.Anything, mock.Anything).Return(nil, nil) m.GetMockPostgresAPI().EXPECT().GetRole(mock.Anything, mock.Anything).Return(nil, nil) m.GetMockVectorSearchEndpointsAPI().EXPECT().GetEndpoint(mock.Anything, mock.Anything).Return(nil, nil) m.GetMockVectorSearchIndexesAPI().EXPECT().GetIndexByIndexName(mock.Anything, mock.Anything).Return(nil, nil) diff --git a/bundle/direct/dresources/all.go b/bundle/direct/dresources/all.go index 391fb0684d..d482c8fb72 100644 --- a/bundle/direct/dresources/all.go +++ b/bundle/direct/dresources/all.go @@ -7,40 +7,41 @@ import ( ) var SupportedResources = map[string]any{ - "jobs": (*ResourceJob)(nil), - "job_runs": (*ResourceJobRun)(nil), - "pipelines": (*ResourcePipeline)(nil), - "experiments": (*ResourceExperiment)(nil), - "catalogs": (*ResourceCatalog)(nil), - "schemas": (*ResourceSchema)(nil), - "external_locations": (*ResourceExternalLocation)(nil), - "volumes": (*ResourceVolume)(nil), - "models": (*ResourceMlflowModel)(nil), - "apps": (*ResourceApp)(nil), - "sql_warehouses": (*ResourceSqlWarehouse)(nil), - "database_instances": (*ResourceDatabaseInstance)(nil), - "database_catalogs": (*ResourceDatabaseCatalog)(nil), - "synced_database_tables": (*ResourceSyncedDatabaseTable)(nil), - "postgres_projects": (*ResourcePostgresProject)(nil), - "postgres_branches": (*ResourcePostgresBranch)(nil), - "postgres_endpoints": (*ResourcePostgresEndpoint)(nil), - "postgres_catalogs": (*ResourcePostgresCatalog)(nil), - "postgres_databases": (*ResourcePostgresDatabase)(nil), - "postgres_roles": (*ResourcePostgresRole)(nil), - "postgres_synced_tables": (*ResourcePostgresSyncedTable)(nil), - "alerts": (*ResourceAlert)(nil), - "clusters": (*ResourceCluster)(nil), - "registered_models": (*ResourceRegisteredModel)(nil), - "dashboards": (*ResourceDashboard)(nil), - "genie_spaces": (*ResourceGenieSpace)(nil), - "secret_scopes": (*ResourceSecretScope)(nil), - "model_serving_endpoints": (*ResourceModelServingEndpoint)(nil), - "quality_monitors": (*ResourceQualityMonitor)(nil), - "vector_search_endpoints": (*ResourceVectorSearchEndpoint)(nil), - "vector_search_indexes": (*ResourceVectorSearchIndex)(nil), - "instance_pools": (*ResourceInstancePool)(nil), - "secrets": (*ResourceSecret)(nil), - "cluster_policies": (*ResourceClusterPolicy)(nil), + "jobs": (*ResourceJob)(nil), + "job_runs": (*ResourceJobRun)(nil), + "pipelines": (*ResourcePipeline)(nil), + "experiments": (*ResourceExperiment)(nil), + "catalogs": (*ResourceCatalog)(nil), + "schemas": (*ResourceSchema)(nil), + "external_locations": (*ResourceExternalLocation)(nil), + "volumes": (*ResourceVolume)(nil), + "models": (*ResourceMlflowModel)(nil), + "apps": (*ResourceApp)(nil), + "sql_warehouses": (*ResourceSqlWarehouse)(nil), + "database_instances": (*ResourceDatabaseInstance)(nil), + "database_catalogs": (*ResourceDatabaseCatalog)(nil), + "synced_database_tables": (*ResourceSyncedDatabaseTable)(nil), + "postgres_projects": (*ResourcePostgresProject)(nil), + "postgres_branches": (*ResourcePostgresBranch)(nil), + "postgres_endpoints": (*ResourcePostgresEndpoint)(nil), + "postgres_catalogs": (*ResourcePostgresCatalog)(nil), + "postgres_databases": (*ResourcePostgresDatabase)(nil), + "postgres_roles": (*ResourcePostgresRole)(nil), + "postgres_synced_tables": (*ResourcePostgresSyncedTable)(nil), + "postgres_snapshot_schedules": (*ResourcePostgresSnapshotSchedule)(nil), + "alerts": (*ResourceAlert)(nil), + "clusters": (*ResourceCluster)(nil), + "registered_models": (*ResourceRegisteredModel)(nil), + "dashboards": (*ResourceDashboard)(nil), + "genie_spaces": (*ResourceGenieSpace)(nil), + "secret_scopes": (*ResourceSecretScope)(nil), + "model_serving_endpoints": (*ResourceModelServingEndpoint)(nil), + "quality_monitors": (*ResourceQualityMonitor)(nil), + "vector_search_endpoints": (*ResourceVectorSearchEndpoint)(nil), + "vector_search_indexes": (*ResourceVectorSearchIndex)(nil), + "instance_pools": (*ResourceInstancePool)(nil), + "secrets": (*ResourceSecret)(nil), + "cluster_policies": (*ResourceClusterPolicy)(nil), // Permissions "jobs.permissions": (*ResourcePermissions)(nil), diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 36eaa3e27d..d385a8e7cd 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -792,6 +792,38 @@ var testDeps = map[string]prepareWorkspace{ }, nil }, + "postgres_snapshot_schedules": func(ctx context.Context, client *databricks.WorkspaceClient) (any, error) { + // Create parent project first + _, err := client.Postgres.CreateProject(ctx, postgres.CreateProjectRequest{ + ProjectId: "test-project-for-snapshot-schedule", + Project: postgres.Project{ + Spec: &postgres.ProjectSpec{ + DisplayName: "Test Project for Snapshot Schedule", + PgVersion: 16, + }, + }, + }) + if err != nil { + return nil, err + } + + // Create parent branch + _, err = client.Postgres.CreateBranch(ctx, postgres.CreateBranchRequest{ + Parent: "projects/test-project-for-snapshot-schedule", + BranchId: "test-branch-for-snapshot-schedule", + Branch: postgres.Branch{}, + }) + if err != nil { + return nil, err + } + + return &resources.PostgresSnapshotSchedule{ + PostgresSnapshotScheduleConfig: resources.PostgresSnapshotScheduleConfig{ + Branch: "projects/test-project-for-snapshot-schedule/branches/test-branch-for-snapshot-schedule", + }, + }, nil + }, + "postgres_endpoints": func(ctx context.Context, client *databricks.WorkspaceClient) (any, error) { // Create parent project first _, err := client.Postgres.CreateProject(ctx, postgres.CreateProjectRequest{ @@ -1129,7 +1161,10 @@ func testCRUD(t *testing.T, group string, adapter *Adapter, client *databricks.W require.NoError(t, err) } - deleteIsNoop := strings.HasSuffix(group, "permissions") || strings.HasSuffix(group, "grants") + // postgres_snapshot_schedules has no delete endpoint: DoDelete disables the + // schedule by setting an empty cadence set, and the schedule remains readable + // (it is intrinsic to the branch), so DoRead still succeeds afterwards. + deleteIsNoop := strings.HasSuffix(group, "permissions") || strings.HasSuffix(group, "grants") || group == "postgres_snapshot_schedules" // Apps DoDelete is fire-and-forget: the API returns success while the app // sits in DELETING state for up to ~20 minutes before the record is removed. // A GET on the DELETING app returns the app, not 404 -- the testserver diff --git a/bundle/direct/dresources/postgres_snapshot_schedule.go b/bundle/direct/dresources/postgres_snapshot_schedule.go new file mode 100644 index 0000000000..ff01bdba1d --- /dev/null +++ b/bundle/direct/dresources/postgres_snapshot_schedule.go @@ -0,0 +1,131 @@ +package dresources + +import ( + "context" + "strings" + + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/common/types/fieldmask" + "github.com/databricks/databricks-sdk-go/marshal" + "github.com/databricks/databricks-sdk-go/service/postgres" +) + +// snapshotScheduleSuffix is the final segment of a snapshot schedule's resource +// name: "projects/{project_id}/branches/{branch_id}/snapshot-schedule". +const snapshotScheduleSuffix = "/snapshot-schedule" + +// PostgresSnapshotScheduleRemote is the return type for DoRead. It carries all +// paths present in StateType (branch, schedule) so drift detection works, plus +// the schedule's resource name. +type PostgresSnapshotScheduleRemote struct { + Branch string `json:"branch,omitempty"` + Schedule []postgres.ScheduleCadence `json:"schedule,omitempty"` + Name string `json:"name,omitempty"` +} + +func (s *PostgresSnapshotScheduleRemote) UnmarshalJSON(b []byte) error { + return marshal.Unmarshal(b, s) +} + +func (s PostgresSnapshotScheduleRemote) MarshalJSON() ([]byte, error) { + return marshal.Marshal(s) +} + +type ResourcePostgresSnapshotSchedule struct { + client *databricks.WorkspaceClient +} + +type PostgresSnapshotScheduleState = resources.PostgresSnapshotScheduleConfig + +func (*ResourcePostgresSnapshotSchedule) New(client *databricks.WorkspaceClient) *ResourcePostgresSnapshotSchedule { + return &ResourcePostgresSnapshotSchedule{client: client} +} + +func (*ResourcePostgresSnapshotSchedule) PrepareState(input *resources.PostgresSnapshotSchedule) *PostgresSnapshotScheduleState { + return &PostgresSnapshotScheduleState{ + Branch: input.Branch, + Schedule: input.Schedule, + ForceSendFields: input.ForceSendFields, + } +} + +func (*ResourcePostgresSnapshotSchedule) RemapState(remote *PostgresSnapshotScheduleRemote) *PostgresSnapshotScheduleState { + return &PostgresSnapshotScheduleState{ + Branch: remote.Branch, + Schedule: remote.Schedule, + ForceSendFields: nil, + } +} + +// makePostgresSnapshotScheduleRemote converts the SDK SnapshotSchedule into the +// remote shape. The API addresses the schedule by "{branch}/snapshot-schedule"; +// branch is derived by stripping that suffix so it participates in drift detection. +func makePostgresSnapshotScheduleRemote(schedule *postgres.SnapshotSchedule) *PostgresSnapshotScheduleRemote { + return &PostgresSnapshotScheduleRemote{ + Branch: strings.TrimSuffix(schedule.Name, snapshotScheduleSuffix), + Schedule: schedule.Schedule, + Name: schedule.Name, + } +} + +func (r *ResourcePostgresSnapshotSchedule) DoRead(ctx context.Context, id string) (*PostgresSnapshotScheduleRemote, error) { + schedule, err := r.client.Postgres.GetSnapshotSchedule(ctx, postgres.GetSnapshotScheduleRequest{Name: id}) + if err != nil { + return nil, err + } + return makePostgresSnapshotScheduleRemote(schedule), nil +} + +// updateSnapshotSchedule sets the branch's schedule to the given cadences and +// waits for the long-running operation to complete. It is the single API call +// behind create, update, and delete: there is no Create/DeleteSnapshotSchedule +// endpoint, so the schedule is managed entirely through UpdateSnapshotSchedule. +// schedule is the only updatable path, so a static mask is used. +func (r *ResourcePostgresSnapshotSchedule) updateSnapshotSchedule(ctx context.Context, name string, cadences []postgres.ScheduleCadence) (*postgres.SnapshotSchedule, error) { + waiter, err := r.client.Postgres.UpdateSnapshotSchedule(ctx, postgres.UpdateSnapshotScheduleRequest{ + Name: name, + SnapshotSchedule: postgres.SnapshotSchedule{ + Schedule: cadences, + + // Name is carried in the request's Name field, not the body. + Name: "", + ForceSendFields: nil, + }, + UpdateMask: fieldmask.FieldMask{ + Paths: []string{"schedule"}, + }, + }) + if err != nil { + return nil, err + } + return waiter.Wait(ctx) +} + +func (r *ResourcePostgresSnapshotSchedule) DoCreate(ctx context.Context, config *PostgresSnapshotScheduleState) (string, *PostgresSnapshotScheduleRemote, error) { + // The schedule exists implicitly for every branch; "creating" the resource + // means setting its cadences via UpdateSnapshotSchedule. + result, err := r.updateSnapshotSchedule(ctx, config.Branch+snapshotScheduleSuffix, config.Schedule) + if err != nil { + return "", nil, err + } + remote := makePostgresSnapshotScheduleRemote(result) + return remote.Name, remote, nil +} + +func (r *ResourcePostgresSnapshotSchedule) DoUpdate(ctx context.Context, id string, config *PostgresSnapshotScheduleState, _ *PlanEntry) (*PostgresSnapshotScheduleRemote, error) { + result, err := r.updateSnapshotSchedule(ctx, id, config.Schedule) + if err != nil { + return nil, err + } + return makePostgresSnapshotScheduleRemote(result), nil +} + +func (r *ResourcePostgresSnapshotSchedule) DoDelete(ctx context.Context, id string, _ *PostgresSnapshotScheduleState) error { + // There is no DeleteSnapshotSchedule endpoint. DoDelete fires whenever the + // resource leaves the desired state (removed from config, or bundle destroy), + // so disable automatic snapshots by setting an empty cadence set — otherwise + // the branch would keep taking snapshots after the resource is gone. + _, err := r.updateSnapshotSchedule(ctx, id, nil) + return err +} diff --git a/bundle/direct/dresources/resources.yml b/bundle/direct/dresources/resources.yml index 07eab3b689..bfbc956bd1 100644 --- a/bundle/direct/dresources/resources.yml +++ b/bundle/direct/dresources/resources.yml @@ -789,6 +789,14 @@ resources: - field: identity_type reason: immutable + postgres_snapshot_schedules: + provided_id_fields: + # branch composes the schedule's hierarchical name + # ("{branch}/snapshot-schedule"); changing it targets a different branch's + # schedule, so it recreates (delete + create). + - field: branch + reason: id_field + vector_search_endpoints: provided_id_fields: # The endpoint API has no rename; the endpoint is fetched by name. diff --git a/bundle/internal/schema/annotations.yml b/bundle/internal/schema/annotations.yml index e47f89c505..5de96889db 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -1575,6 +1575,19 @@ resources: "role_id": "description": |- The user-specified role ID; becomes the final component of the role's resource name. Must be 4-63 characters, lowercase letters, numbers, and hyphens (RFC 1123). + "postgres_snapshot_schedules": + "description": |- + The Postgres snapshot schedule definitions for the bundle, where each key is the name of the snapshot schedule. Each entry configures the automatic-snapshot cadences for a branch of a Lakebase Autoscaling project. + "$fields": + "branch": + "description": |- + The branch whose automatic-snapshot schedule this manages. Format: projects/{project_id}/branches/{branch_id} + "lifecycle": + "description": |- + Settings that control the deployment lifecycle of the resource, such as preventing it from being destroyed. + "schedule": + "description": |- + The cadences at which automatic snapshots are taken. An empty set disables automatic snapshots. When several cadences fire together, one snapshot is taken and retained for the longest of their retentions. "postgres_synced_tables": "description": |- The Postgres synced table definitions for the bundle, where each key is the name of the synced table. Each entry continuously replicates a Unity Catalog Delta source table into a Postgres table on a Lakebase Autoscaling instance. diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index 5967696cb9..ec77bff0bf 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -2268,6 +2268,35 @@ } ] }, + "resources.PostgresSnapshotSchedule": { + "oneOf": [ + { + "type": "object", + "properties": { + "branch": { + "description": "The branch whose automatic-snapshot schedule this manages. Format: projects/{project_id}/branches/{branch_id}", + "$ref": "#/$defs/string" + }, + "lifecycle": { + "description": "Settings that control the deployment lifecycle of the resource, such as preventing it from being destroyed.", + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Lifecycle" + }, + "schedule": { + "description": "The cadences at which automatic snapshots are taken. An empty set disables automatic snapshots. When several cadences fire together, one snapshot is taken and retained for the longest of their retentions.", + "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/postgres.ScheduleCadence" + } + }, + "additionalProperties": false, + "required": [ + "branch" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.PostgresSyncedTable": { "oneOf": [ { @@ -3581,6 +3610,10 @@ "description": "The Postgres role definitions for the bundle, where each key is the name of the role. Each entry defines a role on a Lakebase Autoscaling branch, optionally backed by a Databricks identity.", "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config/resources.PostgresRole" }, + "postgres_snapshot_schedules": { + "description": "The Postgres snapshot schedule definitions for the bundle, where each key is the name of the snapshot schedule. Each entry configures the automatic-snapshot cadences for a branch of a Lakebase Autoscaling project.", + "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config/resources.PostgresSnapshotSchedule" + }, "postgres_synced_tables": { "description": "The Postgres synced table definitions for the bundle, where each key is the name of the synced table. Each entry continuously replicates a Unity Catalog Delta source table into a Postgres table on a Lakebase Autoscaling instance.", "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config/resources.PostgresSyncedTable" @@ -12736,6 +12769,57 @@ } ] }, + "postgres.DailySchedule": { + "oneOf": [ + { + "type": "object", + "description": "Take a snapshot once per day, at the configured hour.", + "properties": { + "hour": { + "description": "[Private Preview] The hour of the day, in UTC, at which to take the snapshot, in [0, 23].", + "$ref": "#/$defs/int", + "x-databricks-launch-stage": "PRIVATE_PREVIEW", + "doNotSuggest": true + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "postgres.DayOfWeek": { + "oneOf": [ + { + "type": "string", + "description": "The day of the week on which a weekly snapshot is taken.", + "enum": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ], + "enumDescriptions": [ + "[Private Preview]", + "[Private Preview]", + "[Private Preview]", + "[Private Preview]", + "[Private Preview]", + "[Private Preview]", + "[Private Preview]" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "postgres.EndpointGroupSpec": { "oneOf": [ { @@ -12801,6 +12885,36 @@ } ] }, + "postgres.MonthlySchedule": { + "oneOf": [ + { + "type": "object", + "description": "Take a snapshot once per month, on the configured day at the configured hour.", + "properties": { + "day": { + "description": "[Private Preview] The day of the month on which to take the snapshot, in [1, 31]. In shorter\nmonths the snapshot is taken on the last day instead (day 31 runs on Feb 28\nor 29, and on Apr 30), so every month gets exactly one snapshot.", + "$ref": "#/$defs/int", + "x-databricks-launch-stage": "PRIVATE_PREVIEW", + "doNotSuggest": true + }, + "hour": { + "description": "[Private Preview] The hour of the day, in UTC, at which to take the snapshot, in [0, 23].", + "$ref": "#/$defs/int", + "x-databricks-launch-stage": "PRIVATE_PREVIEW", + "doNotSuggest": true + } + }, + "additionalProperties": false, + "required": [ + "day" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "postgres.NewPipelineSpec": { "oneOf": [ { @@ -12986,6 +13100,48 @@ } ] }, + "postgres.ScheduleCadence": { + "oneOf": [ + { + "type": "object", + "description": "One cadence at which automatic snapshots are taken.", + "properties": { + "daily_schedule": { + "description": "[Private Preview] Take a snapshot once per day.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/postgres.DailySchedule", + "x-databricks-launch-stage": "PRIVATE_PREVIEW", + "doNotSuggest": true + }, + "monthly_schedule": { + "description": "[Private Preview] Take a snapshot once per month.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/postgres.MonthlySchedule", + "x-databricks-launch-stage": "PRIVATE_PREVIEW", + "doNotSuggest": true + }, + "retention": { + "description": "[Private Preview] How long snapshots from this cadence are kept before automatic deletion.\nMust be at least 1 hour. Applied when a snapshot is taken; not retroactive,\nso changing it affects only later snapshots.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/common/types/duration.Duration", + "x-databricks-launch-stage": "PRIVATE_PREVIEW", + "doNotSuggest": true + }, + "weekly_schedule": { + "description": "[Private Preview] Take a snapshot once per week.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/postgres.WeeklySchedule", + "x-databricks-launch-stage": "PRIVATE_PREVIEW", + "doNotSuggest": true + } + }, + "additionalProperties": false, + "required": [ + "retention" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "postgres.SyncedTableSyncedTableSpecExtraColumn": { "oneOf": [ { @@ -13112,6 +13268,36 @@ } ] }, + "postgres.WeeklySchedule": { + "oneOf": [ + { + "type": "object", + "description": "Take a snapshot once per week, on the configured day at the configured hour.", + "properties": { + "day_of_week": { + "description": "[Private Preview] The day of the week on which to take the snapshot.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/postgres.DayOfWeek", + "x-databricks-launch-stage": "PRIVATE_PREVIEW", + "doNotSuggest": true + }, + "hour": { + "description": "[Private Preview] The hour of the day, in UTC, at which to take the snapshot, in [0, 23].", + "$ref": "#/$defs/int", + "x-databricks-launch-stage": "PRIVATE_PREVIEW", + "doNotSuggest": true + } + }, + "additionalProperties": false, + "required": [ + "day_of_week" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "serving.Ai21LabsConfig": { "oneOf": [ { @@ -15506,6 +15692,20 @@ } ] }, + "resources.PostgresSnapshotSchedule": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.PostgresSnapshotSchedule" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.PostgresSyncedTable": { "oneOf": [ { @@ -16573,6 +16773,20 @@ } ] }, + "postgres.ScheduleCadence": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/postgres.ScheduleCadence" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "postgres.SyncedTableSyncedTableSpecExtraColumn": { "oneOf": [ { diff --git a/libs/testserver/fake_workspace.go b/libs/testserver/fake_workspace.go index 2f077b1b63..d840a8beae 100644 --- a/libs/testserver/fake_workspace.go +++ b/libs/testserver/fake_workspace.go @@ -229,14 +229,15 @@ type FakeWorkspace struct { DatabaseCatalogs map[string]database.DatabaseCatalog SyncedDatabaseTables map[string]database.SyncedDatabaseTable - PostgresProjects map[string]postgres.Project - PostgresBranches map[string]postgres.Branch - PostgresCatalogs map[string]postgres.Catalog - PostgresDatabases map[string]postgres.Database - PostgresEndpoints map[string]postgres.Endpoint - PostgresRoles map[string]postgres.Role - PostgresSyncedTables map[string]postgres.SyncedTable - PostgresOperations map[string]postgres.Operation + PostgresProjects map[string]postgres.Project + PostgresBranches map[string]postgres.Branch + PostgresCatalogs map[string]postgres.Catalog + PostgresDatabases map[string]postgres.Database + PostgresEndpoints map[string]postgres.Endpoint + PostgresRoles map[string]postgres.Role + PostgresSyncedTables map[string]postgres.SyncedTable + PostgresSnapshotSchedules map[string]postgres.SnapshotSchedule + PostgresOperations map[string]postgres.Operation // Branches and endpoints that the server provisioned implicitly together // with their parent (e.g. the production branch on a new project, or the @@ -492,6 +493,7 @@ func NewFakeWorkspace(url, token string) *FakeWorkspace { PostgresEndpoints: map[string]postgres.Endpoint{}, PostgresRoles: map[string]postgres.Role{}, PostgresSyncedTables: map[string]postgres.SyncedTable{}, + PostgresSnapshotSchedules: map[string]postgres.SnapshotSchedule{}, PostgresOperations: map[string]postgres.Operation{}, postgresImplicitBranches: map[string]bool{}, postgresImplicitEndpoints: map[string]bool{}, diff --git a/libs/testserver/handlers.go b/libs/testserver/handlers.go index 8c44d97eae..8fe25f2b4c 100644 --- a/libs/testserver/handlers.go +++ b/libs/testserver/handlers.go @@ -1089,6 +1089,11 @@ func AddDefaultHandlers(server *Server) { return req.Workspace.PostgresOperationGet(name) }) + server.Handle("GET", "/api/2.0/postgres/projects/{project_id}/branches/{branch_id}/snapshot-schedule/operations/{operation_id}", func(req Request) any { + name := "projects/" + req.Vars["project_id"] + "/branches/" + req.Vars["branch_id"] + "/snapshot-schedule/operations/" + req.Vars["operation_id"] + return req.Workspace.PostgresOperationGet(name) + }) + // Postgres Projects: server.Handle("POST", "/api/2.0/postgres/projects", func(req Request) any { projectID := req.URL.Query().Get("project_id") @@ -1142,6 +1147,17 @@ func AddDefaultHandlers(server *Server) { return req.Workspace.PostgresBranchDelete(name) }) + // Postgres Snapshot Schedules (a per-branch singleton; no create/delete): + server.Handle("GET", "/api/2.0/postgres/projects/{project_id}/branches/{branch_id}/snapshot-schedule", func(req Request) any { + name := "projects/" + req.Vars["project_id"] + "/branches/" + req.Vars["branch_id"] + "/snapshot-schedule" + return req.Workspace.PostgresSnapshotScheduleGet(name) + }) + + server.Handle("PATCH", "/api/2.0/postgres/projects/{project_id}/branches/{branch_id}/snapshot-schedule", func(req Request) any { + name := "projects/" + req.Vars["project_id"] + "/branches/" + req.Vars["branch_id"] + "/snapshot-schedule" + return req.Workspace.PostgresSnapshotScheduleUpdate(req, name) + }) + // Postgres Endpoints: server.Handle("POST", "/api/2.0/postgres/projects/{project_id}/branches/{branch_id}/endpoints", func(req Request) any { parent := "projects/" + req.Vars["project_id"] + "/branches/" + req.Vars["branch_id"] diff --git a/libs/testserver/postgres.go b/libs/testserver/postgres.go index ebc402fce0..d2e9c54156 100644 --- a/libs/testserver/postgres.go +++ b/libs/testserver/postgres.go @@ -516,6 +516,65 @@ func (s *FakeWorkspace) PostgresBranchDelete(name string) Response { } } +var snapshotScheduleUpdateMaskPaths = []string{"schedule"} + +// PostgresSnapshotScheduleGet retrieves a branch's snapshot schedule. The +// schedule is intrinsic to the branch: for a branch that never had one set, the +// API returns an empty schedule rather than 404, so the fake mirrors that. +func (s *FakeWorkspace) PostgresSnapshotScheduleGet(name string) Response { + defer s.LockUnlock()() + + branchName := strings.TrimSuffix(name, "/snapshot-schedule") + if _, exists := s.PostgresBranches[branchName]; !exists { + return postgresNotFoundResponse("branch") + } + + schedule, exists := s.PostgresSnapshotSchedules[name] + if !exists { + schedule = postgres.SnapshotSchedule{Name: name} + } + + return Response{ + Body: schedule, + } +} + +// PostgresSnapshotScheduleUpdate sets a branch's snapshot schedule. There is no +// create/delete endpoint; the schedule is managed entirely through this update. +// An empty schedule set disables automatic snapshots. +func (s *FakeWorkspace) PostgresSnapshotScheduleUpdate(req Request, name string) Response { + if resp := validateUpdateMask(req, snapshotScheduleUpdateMaskPaths); resp != nil { + return *resp + } + + defer s.LockUnlock()() + + branchName := strings.TrimSuffix(name, "/snapshot-schedule") + if _, exists := s.PostgresBranches[branchName]; !exists { + return postgresNotFoundResponse("branch") + } + + var updateSchedule postgres.SnapshotSchedule + if len(req.Body) > 0 { + if err := json.Unmarshal(req.Body, &updateSchedule); err != nil { + return Response{ + StatusCode: 400, + Body: fmt.Sprintf("cannot unmarshal request body: %v", err), + } + } + } + + schedule := postgres.SnapshotSchedule{ + Name: name, + Schedule: updateSchedule.Schedule, + } + s.PostgresSnapshotSchedules[name] = schedule + + return Response{ + Body: s.createOperationLocked(name, schedule), + } +} + // PostgresEndpointCreate creates a new postgres endpoint. // // When replaceExisting is true, an existing endpoint with the same ID is updated @@ -1472,6 +1531,8 @@ func (s *FakeWorkspace) createOperationLocked(resourceName string, response any) resourceType = "Catalog" case strings.HasPrefix(resourceName, "synced_tables/"): resourceType = "SyncedTable" + case strings.HasSuffix(resourceName, "/snapshot-schedule"): + resourceType = "SnapshotSchedule" case strings.Contains(resourceName, "/endpoints/"): resourceType = "Endpoint" case strings.Contains(resourceName, "/databases/"): From c57ed62a1dbb6b86bec5088c36cc92dffaec11a3 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 1 Sep 2026 08:34:21 +0000 Subject: [PATCH 2/5] Remove redundant snapshot_schedules acceptance subtests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the `update` subtest (create/update/remove is already covered by `basic`) and the `orphaned` subtest (deleting a branch while keeping a schedule that references it is user error — it plans but fails at apply, which we don't need to pin in a golden). Co-authored-by: Isaac --- .../orphaned/databricks.yml.tmpl | 26 ----- .../orphaned/out.test.toml | 4 - .../orphaned/output.txt | 61 ------------ .../orphaned/script | 27 ------ .../update/databricks.yml.tmpl | 26 ----- .../update/out.requests.add.txt | 74 --------------- .../update/out.requests.remove.txt | 40 -------- .../update/out.test.toml | 4 - .../update/output.txt | 95 ------------------- .../postgres_snapshot_schedules/update/script | 28 ------ 10 files changed, 385 deletions(-) delete mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/databricks.yml.tmpl delete mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/out.test.toml delete mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/output.txt delete mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/script delete mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/databricks.yml.tmpl delete mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.add.txt delete mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.remove.txt delete mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/out.test.toml delete mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/output.txt delete mode 100644 acceptance/bundle/resources/postgres_snapshot_schedules/update/script diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/databricks.yml.tmpl b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/databricks.yml.tmpl deleted file mode 100644 index f2ad5af11a..0000000000 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/databricks.yml.tmpl +++ /dev/null @@ -1,26 +0,0 @@ -bundle: - name: deploy-pg-ss-orphaned-$UNIQUE_NAME - -sync: - paths: [] - -resources: - postgres_projects: - my_project: - project_id: test-pg-proj-$UNIQUE_NAME - display_name: "Test Project for Snapshot Schedules" - pg_version: 16 - - postgres_branches: - main: - parent: ${resources.postgres_projects.my_project.id} - branch_id: main - no_expiry: true - - postgres_snapshot_schedules: - main_schedule: - branch: ${resources.postgres_branches.main.id} - schedule: - - daily_schedule: - hour: 3 - retention: "604800s" diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/out.test.toml b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/out.test.toml deleted file mode 100644 index 1314e661c2..0000000000 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/out.test.toml +++ /dev/null @@ -1,4 +0,0 @@ -Cloud = true -CloudEnvs.azure = false -CloudEnvs.gcp = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/output.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/output.txt deleted file mode 100644 index 5cc6d36335..0000000000 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/output.txt +++ /dev/null @@ -1,61 +0,0 @@ - -=== Deploy project + branch + snapshot schedule ->>> [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-orphaned-[UNIQUE_NAME]/default/files... -Created postgres_branches.main -Created postgres_projects.my_project -Created postgres_snapshot_schedules.main_schedule -Files: 0 uploaded, 0 deleted -Resources: 3 created, 0 changed, 0 deleted, 0 unchanged - -=== Remove the branch but keep the schedule that references it ->>> cat databricks.yml -bundle: - name: deploy-pg-ss-orphaned-[UNIQUE_NAME] - -sync: - paths: [] - -resources: - postgres_projects: - my_project: - project_id: test-pg-proj-[UNIQUE_NAME] - display_name: "Test Project for Snapshot Schedules" - pg_version: 16 - - postgres_snapshot_schedules: - main_schedule: - branch: ${resources.postgres_branches.main.id} - schedule: - - daily_schedule: - hour: 3 - retention: "604800s" -Name: deploy-pg-ss-orphaned-[UNIQUE_NAME] -Target: default -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-orphaned-[UNIQUE_NAME]/default - -Validation OK! -delete postgres_branches.main -recreate postgres_snapshot_schedules.main_schedule - -Plan: 1 to add, 0 to change, 2 to delete, 1 unchanged - ->>> [CLI] bundle destroy --auto-approve -The following resources will be deleted: - delete resources.postgres_branches.main - delete resources.postgres_projects.my_project - delete resources.postgres_snapshot_schedules.main_schedule - -This action will result in the deletion of the following Lakebase projects along with -all their branches, databases, and endpoints. All data stored in them will be permanently lost: - delete resources.postgres_projects.my_project - -This action will result in the deletion of the following Lakebase branches. -All data stored in them will be permanently lost: - delete resources.postgres_branches.main - -All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-orphaned-[UNIQUE_NAME]/default - -Destroy: 3 deleted diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/script b/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/script deleted file mode 100644 index f89a3ad4a8..0000000000 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/orphaned/script +++ /dev/null @@ -1,27 +0,0 @@ -cleanup() { - trace $CLI bundle destroy --auto-approve - rm -f out.requests.txt -} -trap cleanup EXIT - -title "Deploy project + branch + snapshot schedule" -envsubst < databricks.yml.tmpl > databricks.yml -trace $CLI bundle deploy - -# Remove ONLY the branch from config while keeping the snapshot schedule, which -# still references ${resources.postgres_branches.main.id}. The reference still -# resolves from the deployed state (the branch is present in state during the -# transition), so this does NOT fail as a dangling reference: validate passes and -# plan sequences a delete of the branch alongside a recreate of the schedule. -# Deploying that plan would fail at apply (the schedule cannot be created on a -# deleted branch); this test documents the plan-time behavior. Acceptable user -# error -- removing a parent while keeping a child that references it. -# errcode records the exit code so the outcome is captured deterministically. -title "Remove the branch but keep the schedule that references it" -sed '/^ postgres_branches:/,/^ postgres_snapshot_schedules:/{/^ postgres_snapshot_schedules:/!d}' databricks.yml > databricks.yml.new && mv databricks.yml.new databricks.yml -trace cat databricks.yml -errcode $CLI bundle validate -errcode $CLI bundle plan - -# Restore the full config so the destroy in cleanup can resolve references. -envsubst < databricks.yml.tmpl > databricks.yml diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/databricks.yml.tmpl b/acceptance/bundle/resources/postgres_snapshot_schedules/update/databricks.yml.tmpl deleted file mode 100644 index 0ea6ec1341..0000000000 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/update/databricks.yml.tmpl +++ /dev/null @@ -1,26 +0,0 @@ -bundle: - name: deploy-pg-ss-update-$UNIQUE_NAME - -sync: - paths: [] - -resources: - postgres_projects: - my_project: - project_id: test-pg-proj-$UNIQUE_NAME - display_name: "Test Project for Snapshot Schedules" - pg_version: 16 - - postgres_branches: - main: - parent: ${resources.postgres_projects.my_project.id} - branch_id: main - no_expiry: true - - postgres_snapshot_schedules: - main_schedule: - branch: ${resources.postgres_branches.main.id} - schedule: - - daily_schedule: - hour: 3 - retention: "604800s" diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.add.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.add.txt deleted file mode 100644 index 9c5f1edf5c..0000000000 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.add.txt +++ /dev/null @@ -1,74 +0,0 @@ -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" -} -{ - "method": "PATCH", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", - "q": { - "update_mask": "schedule" - }, - "body": { - "schedule": [ - { - "daily_schedule": { - "hour": 3 - }, - "retention": "604800s" - } - ] - } -} -{ - "method": "POST", - "path": "/api/2.0/postgres/projects", - "q": { - "project_id": "test-pg-proj-[UNIQUE_NAME]" - }, - "body": { - "spec": { - "display_name": "Test Project for Snapshot Schedules", - "pg_version": 16 - } - } -} -{ - "method": "POST", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches", - "q": { - "branch_id": "main" - }, - "body": { - "spec": { - "no_expiry": true - } - } -} diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.remove.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.remove.txt deleted file mode 100644 index bea13bc3b2..0000000000 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.requests.remove.txt +++ /dev/null @@ -1,40 +0,0 @@ -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" -} -{ - "method": "GET", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule" -} -{ - "method": "PATCH", - "path": "/api/2.0/postgres/projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", - "q": { - "update_mask": "schedule" - }, - "body": {} -} diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.test.toml b/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.test.toml deleted file mode 100644 index 1314e661c2..0000000000 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/update/out.test.toml +++ /dev/null @@ -1,4 +0,0 @@ -Cloud = true -CloudEnvs.azure = false -CloudEnvs.gcp = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/output.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/update/output.txt deleted file mode 100644 index 1672fc03e0..0000000000 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/update/output.txt +++ /dev/null @@ -1,95 +0,0 @@ - -=== Deploy project + branch without a snapshot schedule ->>> cat databricks.yml -bundle: - name: deploy-pg-ss-update-[UNIQUE_NAME] - -sync: - paths: [] - -resources: - postgres_projects: - my_project: - project_id: test-pg-proj-[UNIQUE_NAME] - display_name: "Test Project for Snapshot Schedules" - pg_version: 16 - - postgres_branches: - main: - parent: ${resources.postgres_projects.my_project.id} - branch_id: main - no_expiry: true - - ->>> [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-update-[UNIQUE_NAME]/default/files... -Created postgres_branches.main -Created postgres_projects.my_project -Files: 0 uploaded, 0 deleted -Resources: 2 created, 0 changed, 0 deleted, 0 unchanged - ->>> [CLI] postgres get-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule -{ - "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", - "schedule": null -} - -=== Add the snapshot schedule resource to the existing branch ->>> [CLI] bundle plan -create postgres_snapshot_schedules.main_schedule - -Plan: 1 to add, 0 to change, 0 to delete, 2 unchanged - ->>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-update-[UNIQUE_NAME]/default/files... -Created postgres_snapshot_schedules.main_schedule -Files: 0 uploaded, 0 deleted -Resources: 1 created, 0 changed, 0 deleted, 2 unchanged - ->>> [CLI] postgres get-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule -{ - "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", - "schedule": [ - { - "daily_schedule": { - "hour": 3 - }, - "retention": "604800s" - } - ] -} - -=== Remove the snapshot schedule resource from the existing branch ->>> [CLI] bundle plan -delete postgres_snapshot_schedules.main_schedule - -Plan: 0 to add, 0 to change, 1 to delete, 2 unchanged - ->>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-update-[UNIQUE_NAME]/default/files... -Deleted postgres_snapshot_schedules.main_schedule -Files: 0 uploaded, 0 deleted -Resources: 0 created, 0 changed, 1 deleted, 2 unchanged - ->>> [CLI] postgres get-snapshot-schedule projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule -{ - "name": "projects/test-pg-proj-[UNIQUE_NAME]/branches/main/snapshot-schedule", - "schedule": null -} - ->>> [CLI] bundle destroy --auto-approve -The following resources will be deleted: - delete resources.postgres_branches.main - delete resources.postgres_projects.my_project - -This action will result in the deletion of the following Lakebase projects along with -all their branches, databases, and endpoints. All data stored in them will be permanently lost: - delete resources.postgres_projects.my_project - -This action will result in the deletion of the following Lakebase branches. -All data stored in them will be permanently lost: - delete resources.postgres_branches.main - -All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-pg-ss-update-[UNIQUE_NAME]/default - -Destroy: 2 deleted diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/update/script b/acceptance/bundle/resources/postgres_snapshot_schedules/update/script deleted file mode 100644 index 6f69cc6810..0000000000 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/update/script +++ /dev/null @@ -1,28 +0,0 @@ -cleanup() { - trace $CLI bundle destroy --auto-approve - rm -f out.requests.txt -} -trap cleanup EXIT - -schedule_name="projects/test-pg-proj-${UNIQUE_NAME}/branches/main/snapshot-schedule" - -title "Deploy project + branch without a snapshot schedule" -envsubst < databricks.yml.tmpl | sed '/postgres_snapshot_schedules:/,$d' > databricks.yml -trace cat databricks.yml -rm -f out.requests.txt -trace $CLI bundle deploy -trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields - -title "Add the snapshot schedule resource to the existing branch" -envsubst < databricks.yml.tmpl > databricks.yml -trace $CLI bundle plan -trace $CLI bundle deploy --auto-approve -trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields -print_requests.py --del-body parent,project_id,branch_id --sort --get '//postgres' '^//workspace-files/' '^//workspace/' '^//telemetry-ext' '^//operations/' > out.requests.add.txt - -title "Remove the snapshot schedule resource from the existing branch" -sed '/postgres_snapshot_schedules:/,$d' databricks.yml > databricks.yml.new && mv databricks.yml.new databricks.yml -trace $CLI bundle plan -trace $CLI bundle deploy --auto-approve -trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields -print_requests.py --del-body parent,project_id,branch_id --sort --get '//postgres' '^//workspace-files/' '^//workspace/' '^//telemetry-ext' '^//operations/' > out.requests.remove.txt From 48d3fa7a110d26c405f8be947bf9f220097dbc19 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 1 Sep 2026 08:38:25 +0000 Subject: [PATCH 3/5] Add snapshotScheduleName helper to normalize a trailing slash DoCreate built the schedule resource name as `config.Branch + "/snapshot-schedule"`, which doubles the separator if a user writes `branch:` with a trailing slash. Route it through snapshotScheduleName, which trims trailing slashes first, and cover it with a unit test. Co-authored-by: Isaac --- .../dresources/postgres_snapshot_schedule.go | 9 ++++++++- .../postgres_snapshot_schedule_test.go | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 bundle/direct/dresources/postgres_snapshot_schedule_test.go diff --git a/bundle/direct/dresources/postgres_snapshot_schedule.go b/bundle/direct/dresources/postgres_snapshot_schedule.go index ff01bdba1d..3f73e8c729 100644 --- a/bundle/direct/dresources/postgres_snapshot_schedule.go +++ b/bundle/direct/dresources/postgres_snapshot_schedule.go @@ -15,6 +15,13 @@ import ( // name: "projects/{project_id}/branches/{branch_id}/snapshot-schedule". const snapshotScheduleSuffix = "/snapshot-schedule" +// snapshotScheduleName returns the schedule's resource name for a branch. It +// trims any trailing slash on the branch name (e.g. a user-supplied +// "projects/p/branches/b/") so the suffix is not doubled up. +func snapshotScheduleName(branch string) string { + return strings.TrimRight(branch, "/") + snapshotScheduleSuffix +} + // PostgresSnapshotScheduleRemote is the return type for DoRead. It carries all // paths present in StateType (branch, schedule) so drift detection works, plus // the schedule's resource name. @@ -105,7 +112,7 @@ func (r *ResourcePostgresSnapshotSchedule) updateSnapshotSchedule(ctx context.Co func (r *ResourcePostgresSnapshotSchedule) DoCreate(ctx context.Context, config *PostgresSnapshotScheduleState) (string, *PostgresSnapshotScheduleRemote, error) { // The schedule exists implicitly for every branch; "creating" the resource // means setting its cadences via UpdateSnapshotSchedule. - result, err := r.updateSnapshotSchedule(ctx, config.Branch+snapshotScheduleSuffix, config.Schedule) + result, err := r.updateSnapshotSchedule(ctx, snapshotScheduleName(config.Branch), config.Schedule) if err != nil { return "", nil, err } diff --git a/bundle/direct/dresources/postgres_snapshot_schedule_test.go b/bundle/direct/dresources/postgres_snapshot_schedule_test.go new file mode 100644 index 0000000000..261ceba17a --- /dev/null +++ b/bundle/direct/dresources/postgres_snapshot_schedule_test.go @@ -0,0 +1,19 @@ +package dresources + +import "testing" + +func TestSnapshotScheduleName(t *testing.T) { + const branch = "projects/p/branches/b" + want := branch + "/snapshot-schedule" + + cases := []string{ + branch, + branch + "/", + branch + "//", + } + for _, in := range cases { + if got := snapshotScheduleName(in); got != want { + t.Errorf("snapshotScheduleName(%q) = %q, want %q", in, got, want) + } + } +} From 9970a2c30a95af88a26165e00d7f872e17fb197f Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 1 Sep 2026 08:44:44 +0000 Subject: [PATCH 4/5] Assert no drift after removing the snapshot schedule Add a `bundle plan` after the removal step in the basic acceptance test; it reports "0 to add, 0 to change, 0 to delete" (project and branch unchanged), confirming the disable settled cleanly and leaves no lingering drift. Co-authored-by: Isaac --- .../resources/postgres_snapshot_schedules/basic/output.txt | 4 ++++ .../bundle/resources/postgres_snapshot_schedules/basic/script | 3 +++ 2 files changed, 7 insertions(+) diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/basic/output.txt b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/output.txt index 1d39ab41bc..c7eff1ef48 100644 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/basic/output.txt +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/output.txt @@ -75,6 +75,10 @@ Resources: 0 created, 0 changed, 1 deleted, 2 unchanged "schedule": null } +=== Plan again to confirm no drift after removal +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged + >>> [CLI] bundle destroy --auto-approve The following resources will be deleted: delete resources.postgres_branches.main diff --git a/acceptance/bundle/resources/postgres_snapshot_schedules/basic/script b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/script index 0e7cbe6b7c..77c51554a9 100644 --- a/acceptance/bundle/resources/postgres_snapshot_schedules/basic/script +++ b/acceptance/bundle/resources/postgres_snapshot_schedules/basic/script @@ -25,3 +25,6 @@ trace cat databricks.yml trace $CLI bundle deploy --auto-approve trace $CLI postgres get-snapshot-schedule "${schedule_name}" | snapshot_schedule_fields print_requests.py --del-body parent,project_id,branch_id --sort --get '//postgres' '^//workspace-files/' '^//workspace/' '^//telemetry-ext' '^//operations/' > out.requests.remove.txt + +title "Plan again to confirm no drift after removal" +trace $CLI bundle plan From 120176f2a9bfd12ce5a786e5c3e51440dae7031d Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 1 Sep 2026 08:51:56 +0000 Subject: [PATCH 5/5] task generate-check --- bundle/direct/dresources/apitypes.generated.yml | 2 ++ bundle/direct/dresources/resources.generated.yml | 2 ++ bundle/internal/validation/generated/enum_fields.go | 2 ++ bundle/internal/validation/generated/required_fields.go | 5 +++++ 4 files changed, 11 insertions(+) diff --git a/bundle/direct/dresources/apitypes.generated.yml b/bundle/direct/dresources/apitypes.generated.yml index ec2e3c2519..ebb50c43c4 100644 --- a/bundle/direct/dresources/apitypes.generated.yml +++ b/bundle/direct/dresources/apitypes.generated.yml @@ -46,6 +46,8 @@ postgres_projects: postgres.ProjectStatus postgres_roles: postgres.RoleRoleStatus +postgres_snapshot_schedules: postgres.UpdateBranchRequest + postgres_synced_tables: postgres.SyncedTableSyncedTableSpec quality_monitors: catalog.CreateMonitor diff --git a/bundle/direct/dresources/resources.generated.yml b/bundle/direct/dresources/resources.generated.yml index 264ba81eca..85a0466d49 100644 --- a/bundle/direct/dresources/resources.generated.yml +++ b/bundle/direct/dresources/resources.generated.yml @@ -370,6 +370,8 @@ resources: - field: postgres_role reason: spec:input_only + # postgres_snapshot_schedules: no api field behaviors + postgres_synced_tables: ignore_remote_changes: diff --git a/bundle/internal/validation/generated/enum_fields.go b/bundle/internal/validation/generated/enum_fields.go index e65f7ea434..cb3a8f085d 100644 --- a/bundle/internal/validation/generated/enum_fields.go +++ b/bundle/internal/validation/generated/enum_fields.go @@ -238,6 +238,8 @@ var EnumFields = map[string][]string{ "resources.postgres_roles.*.identity_type": {"GROUP", "SERVICE_PRINCIPAL", "USER"}, "resources.postgres_roles.*.membership_roles[*]": {"DATABRICKS_SUPERUSER"}, + "resources.postgres_snapshot_schedules.*.schedule[*].weekly_schedule.day_of_week": {"FRIDAY", "MONDAY", "SATURDAY", "SUNDAY", "THURSDAY", "TUESDAY", "WEDNESDAY"}, + "resources.postgres_synced_tables.*.extra_columns[*].maintenance": {"STORED_GENERATED"}, "resources.postgres_synced_tables.*.new_pipeline_spec.pipeline_channel": {"CURRENT", "PREVIEW"}, "resources.postgres_synced_tables.*.scheduling_policy": {"CONTINUOUS", "SNAPSHOT", "TRIGGERED"}, diff --git a/bundle/internal/validation/generated/required_fields.go b/bundle/internal/validation/generated/required_fields.go index fcdd9b11ab..7a8a027a10 100644 --- a/bundle/internal/validation/generated/required_fields.go +++ b/bundle/internal/validation/generated/required_fields.go @@ -269,6 +269,11 @@ var RequiredFields = map[string][]string{ "resources.postgres_roles.*": {"role_id", "parent"}, + "resources.postgres_snapshot_schedules.*": {"branch"}, + "resources.postgres_snapshot_schedules.*.schedule[*]": {"retention"}, + "resources.postgres_snapshot_schedules.*.schedule[*].monthly_schedule": {"day"}, + "resources.postgres_snapshot_schedules.*.schedule[*].weekly_schedule": {"day_of_week"}, + "resources.postgres_synced_tables.*": {"synced_table_id"}, "resources.postgres_synced_tables.*.extra_columns[*]": {"column_name", "column_type"}, "resources.postgres_synced_tables.*.type_overrides[*]": {"column_name", "pg_type"},