Skip to content

feat(spanner): add Instance, Database and BackupSchedule - #167

Merged
naxty merged 6 commits into
naxty/gcpBatchResources2from
naxty/gcpSpannerBatch
Aug 31, 2026
Merged

feat(spanner): add Instance, Database and BackupSchedule#167
naxty merged 6 commits into
naxty/gcpBatchResources2from
naxty/gcpSpannerBatch

Conversation

@naxty

@naxty naxty commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Adds Cloud Spanner. Stacked on #165 — review that first; this PR's own diff is
the three Spanner commits.

Type Result
GCP::Spanner::Instance crud 8/8, discovery 4/4
GCP::Spanner::Database crud 8/8 (Update skipped — nothing patchable), discovery 4/4
GCP::Spanner::BackupSchedule crud 8/8, discovery 4/4

Verified against europe-central2 on released formae (0.89.0) and on a
binary built from formae main, which additionally requires an extracted forma
to re-apply as a zero-operation simulate. All three green on both.

Notes for review

Three things the config-driven engine could not express, each handled in a
transformer rather than a bespoke provisioner:

  • Instance create is {instanceId, instance:{…}} — the id sits in the body
    beside the object, not in a query parameter, so RequestWrapper +
    CreateIDParam would have put it in the wrong place.
  • Instance patch carries fieldMask in the body, not ?updateMask=, and
    rejects config in the mask — so the mask is computed from a fixed
    mutable-field list.
  • Database create has no name field: the id goes into a CREATE DATABASE
    statement, backtick-quoted for GoogleSQL and double-quoted for PostgreSQL.

config is normalised both ways — a forma writes regional-europe-central2 and
the plugin qualifies it — so a forma carries no project id and stays portable
between targets.

backupSchedules.patch accepts exactly three mask paths (encryption_config,
retention_duration, spec.cron_spec.text), so UpdateMaskFromBody cannot
express it: the mask reaches a field inside spec. The mask is fixed instead,
deliberately leaving encryption_config out so an update never rewrites a
server value nobody declared.

Spanner fills in spec.cronSpec.timeZone and creationWindow; both are marked
hasProviderDefault or Verify fails on properties no forma wrote.

displayName is constrained to 4–30 characters in the schema, because the API
rejects anything outside that and a fixture should fail at eval rather than
mid-apply.

Cost and cleanup

Instances are billed by the hour, and the database and backup-schedule fixtures
each build one as a prerequisite that conformance Destroy does not remove — so
clean-environment.sh sweeps Spanner instances, verified against the live
project. spanner-* gets a 15-minute per-operation timeout in
scripts/ci/run-conformance-case.sh.

What I could not verify

  • Backup is deliberately absent: expireTime is required, absolute and capped
    at a year, so any fixture hardcodes a date that silently rots into a CI
    failure.
  • Running both phases back-to-back under one fixed FORMAE_TEST_RUN_ID showed
    no name collision on the surviving instance, so no phase split was added.

Correction (2026-08-28): this body reported CRUD as 8/8. That was wrong. The
Replace step reports [~]skipped, not passed: the harness only runs it when a
testdata/<case>-replace.pkl fixture exists, and no case in this repo has one (0 of
118). Read every 8/8 above as 7 passed, Replace not exercised. The same applies
to every GCP resource already on main — the Replace path has never been covered
here. Nothing in this PR regressed.

naxty added 3 commits August 28, 2026 00:52
Schemas and provisioners only; fixtures and conformance runs follow.

Instances are project-scoped - a Spanner instance's region is its config, not
a path segment. Two envelopes the config-driven engine cannot express are built
by a request transformer: create is {instanceId, instance:{...}}, with the id
beside the object rather than in a query parameter, and patch carries its field
mask in the body as fieldMask rather than in the query string.

A database has no name field on create; the id goes into a CREATE DATABASE
statement, quoted with backticks for GoogleSQL and double quotes for
PostgreSQL. dropDatabase is a plain DELETE on the resource URL, so the generic
engine handles it.

Backup schedules sit two collections deep and are synchronous. Spanner rejects
a wildcard for both nested collections - instances/-/databases and
databases/-/backupSchedules answer 400 "Invalid List... request" - so
databases and schedules are discovered by walking the collections above them.
All three cases are crud 8/8 and discovery 4/4 against europe-central2. Two
failures found and fixed on the way, both real:

displayName is rejected outside 4-30 characters, which the fixtures broke and
the schema now states as a constraint rather than a comment.

backupSchedules.patch accepts exactly three mask paths - encryption_config,
retention_duration and spec.cron_spec.text - so UpdateMaskFromBody could not
express it: the mask reaches a field inside spec, and that transformer lists
the body's top-level fields. The mask is fixed instead, leaving out
encryption_config so an update never rewrites a server value nobody declared.

The nested cron fields GCP fills in (timeZone, creationWindow) are marked as
provider defaults; without that a schedule reads back with two fields no forma
declared and Verify fails.

clean-environment.sh sweeps Spanner instances. Unlike most leftovers these are
billed by the hour: the database and backup-schedule fixtures each build one as
a prerequisite and conformance Destroy only removes the resource under test, so
every run of those two cases leaves one running. Deleting the instance cascades
its databases and schedules, so one sweep covers all three cases.

ci.yml and nightly.yml both give spanner-* a 15 minute per-operation timeout.
Running both phases back to back under one fixed FORMAE_TEST_RUN_ID, the way CI
does, showed no name collision on the surviving instance, so no phase split.
Covers the two request envelopes, the DDL quoting per dialect, both directions
of the config id normalisation, the native-ID parser at all three depths, and
that the parent-walking List survives registration.
@naxty
naxty merged commit fafb7ce into naxty/gcpBatchResources2 Aug 31, 2026
naxty added a commit that referenced this pull request Aug 31, 2026
…ustConfig

Restores #171. That pull request was stacked, and it merged into its parent
branch rather than into main - GitHub marks a stacked pull request merged once
its commits reach its base, and its base was a branch. #167, #168, #169 and #170
went the same way but were recovered when #173 landed, because #173 descended
from them. Nothing descended from #171, so certificate manager was the one batch
left behind: main has carried a Merged badge and no code since.

This is that content on top of current main, nothing else - the three types,
their schemas, six fixtures, and the package registration.

Verified locally against the service account CI uses, all three CRUD 7/7 with
Replace skipped and discovery 4/4. Two things it needed, both recorded in the
CHANGELOG: roles/certificatemanager.owner rather than editor, which carries no
delete permission at all; and a trust config that is not empty, since the API
rejects one carrying neither a trust store nor an allowlisted certificate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant