Skip to content

feat(certificatemanager): add Certificate and CertificateMapEntry - #189

Merged
naxty merged 2 commits into
mainfrom
naxty/gcpCertManagerBatch2
Sep 3, 2026
Merged

feat(certificatemanager): add Certificate and CertificateMapEntry#189
naxty merged 2 commits into
mainfrom
naxty/gcpCertManagerBatch2

Conversation

@naxty

@naxty naxty commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The rest of the free surface of this service, taking it from three types to five.

Type Case CRUD Discovery
GCP::CertificateManager::Certificate certmanager-certificate 8/8 4/4
GCP::CertificateManager::CertificateMapEntry certmanager-certificate-map-entry 8/8 4/4

Verified locally against the same service account CI uses. A managed
certificate carries no private key, so the case describes one without committing
key material — it never leaves PROVISIONING, because example.com is not ours,
and no phase depends on it doing so. CertificateIssuanceConfig is left out: it
needs a CA pool, which bills.

Four things the API forced

Each found by probing it, not by reading docs.

managed is not a name a forma can use. It is a fixed property of
formae.Resource, and a resource redeclaring it does not evaluate. The schema
calls it managedCertificate; the plugin renames it in both directions, and
those two transformers are the only places that know.

A reference resolves short; the API speaks in paths. A forma names another
resource by short id — that is all a resolvable yields — while the API wants a
full path and answers with one, carrying the project number where the forma
used the project id. So the request expands and the response shortens.

Doing only the first half is worse than doing neither: the declared side
stays short, state holds a path, and since both fields are immutable every
re-apply plans a replacement of a certificate that a map entry still
references — which the API then refuses. That is exactly how this failed before
the response half existed, and a unit test now pins the round trip as the
identity.

Output-only fields nested inside managed. state and
authorizationAttemptInfo describe issuance progress. A schema hint is only
emitted for a top-level field, so a nested one can never be marked
hasProviderDefault, and Verify rejects it as undeclared. They are dropped on
read.

A parentless List addressed a collection that does not exist, so the entry
was never discovered. Certificate Manager accepts - as a wildcard parent,
enumerating entries across every map in one call — cheaper than the walker the
same problem needed in Service Directory and Cloud SQL.

Deleting a DnsAuthorization while a certificate still references it is reported
as retryable rather than fatal: the certificate's own delete is a
long-running operation, so the refusal is a race that clears itself. Same
treatment Cloud SQL gives a database whose sessions have not been reaped.

Also: the Certificate Manager sweep did not exist

These resources accumulated through the session — certificates, maps and
authorizations left by conformance runs — because clean-environment.sh had no
section for them. Added, in dependency order (an entry pins its map and its
certificate; a certificate pins its authorization), and verified against the
live project
by using it to clear what this work left behind: 3 certificates,
3 maps and 7 authorizations down to zero.

The rest of the free surface of this service, taking it from three types to
five. A managed certificate carries no private key, so the case describes one
without committing key material - the certificate never leaves PROVISIONING,
because example.com is not ours, and no phase depends on it doing so.

Four things the API forced, each found by probing it:

The API field is "managed". A forma cannot use that name: managed is a fixed
property of formae.Resource and a resource redeclaring it does not evaluate. The
schema calls it managedCertificate and the plugin renames it both ways.

A forma names another resource by short id - that is all a resolvable yields -
while the API wants a full path and answers with one, carrying the project
number where the forma used the project id. So the request expands and the
response shortens. Doing only the first is worse than doing neither: the
declared side stays short, state holds a path, and since both fields are
immutable every re-apply plans a replacement of a certificate that a map entry
still references, which the API then refuses. That is exactly how this failed
before the response half existed.

State and authorizationAttemptInfo are output-only and live inside managed. A
schema hint is only emitted for a top-level field, so a nested one can never be
marked hasProviderDefault and Verify rejects it as undeclared; they are dropped
on read.

A parentless List addresses a collection that does not exist, and the entry was
never discovered. Certificate Manager accepts "-" as a wildcard parent, which
enumerates entries across every map in one call - cheaper than the walker the
same problem needed in Service Directory and Cloud SQL.

Deleting a DnsAuthorization while a certificate still references it is reported
as retryable rather than fatal: the certificate's delete is a long-running
operation, so the refusal is a race that clears itself.

Also adds the Certificate Manager sweep, which did not exist - these resources
accumulated through the session. Verified against the live project by clearing
what this work left behind.

Verified locally against the service account CI uses:

  certmanager-certificate            CRUD 8/8  discovery 4/4
  certmanager-certificate-map-entry  CRUD 8/8  discovery 4/4
naxty added a commit that referenced this pull request Sep 2, 2026
Following the naming standardisation, an audit of every type the plugin ships
against every section of this script. Ten services could create resources that
nothing ever collected, and four of them bill:

  Filestore    provisioned capacity per GB-hour; one leaked at 1024GB before
  Redis        instances and clusters hold memory, charged per hour
  privateca    a CA pool is billed monthly; two ENTERPRISE pools leaked before
  Datastream   a private connection reserves a peering range and is billable

The rest are free but noisy: Pub/Sub (162 topics accumulated once), Cloud DNS,
Cloud Scheduler, Cloud Tasks, Network Connectivity and Eventarc.

Three sweeps that looked present were not working:

Cloud SQL matched the literal "formae-test-sql", so the instances built by the
backup-run and ssl-cert fixtures - named formae-test-backuprun- and
formae-test-sslcert- - were never collected. Cloud SQL instances are not cheap.

BigQuery matched "formae_plugin_sdk_test_", a prefix that no longer exists after
the rename in the previous commit. Renaming fixtures without repointing the
sweeps would have started a fresh leak, which is the same mistake in the other
direction.

The VPC access connector sweep matched a single hand-written prefix. It now uses
the shared pattern like everything else.

Eventarc needed splitting: gcloud knows triggers, channels and channel-connections
and nothing else, so the Advanced types - message buses, pipelines, enrollments,
Google API sources - are swept over REST. A message bus is capped at one per
region, so a leaked one blocks the next run rather than costing money. Every
gcloud subcommand added here was checked to exist first: one that does not fails
into "|| true" and silently collects nothing, which is the failure this whole
change is about.

Still unswept, deliberately: Container, GKE Hub and GKE Backup create nothing
because their cases cannot run without a cluster; KMS keyrings cannot be deleted
by anyone; Essential Contacts and Org Policy are free configuration. Certificate
Manager is swept by #189.

Not verified live - there are no working credentials on this machine right now.
Every command is guarded by "|| true" and every listing is filtered by SWEEP_RE
and KEEP_RE, so the failure mode is collecting nothing rather than deleting
something it should not.
naxty added a commit that referenced this pull request Sep 3, 2026
…lected nothing

* fix(ci): sweep the services that had no sweep at all

Following the naming standardisation, an audit of every type the plugin ships
against every section of this script. Ten services could create resources that
nothing ever collected, and four of them bill:

  Filestore    provisioned capacity per GB-hour; one leaked at 1024GB before
  Redis        instances and clusters hold memory, charged per hour
  privateca    a CA pool is billed monthly; two ENTERPRISE pools leaked before
  Datastream   a private connection reserves a peering range and is billable

The rest are free but noisy: Pub/Sub (162 topics accumulated once), Cloud DNS,
Cloud Scheduler, Cloud Tasks, Network Connectivity and Eventarc.

Three sweeps that looked present were not working:

Cloud SQL matched the literal "formae-test-sql", so the instances built by the
backup-run and ssl-cert fixtures - named formae-test-backuprun- and
formae-test-sslcert- - were never collected. Cloud SQL instances are not cheap.

BigQuery matched "formae_plugin_sdk_test_", a prefix that no longer exists after
the rename in the previous commit. Renaming fixtures without repointing the
sweeps would have started a fresh leak, which is the same mistake in the other
direction.

The VPC access connector sweep matched a single hand-written prefix. It now uses
the shared pattern like everything else.

Eventarc needed splitting: gcloud knows triggers, channels and channel-connections
and nothing else, so the Advanced types - message buses, pipelines, enrollments,
Google API sources - are swept over REST. A message bus is capped at one per
region, so a leaked one blocks the next run rather than costing money. Every
gcloud subcommand added here was checked to exist first: one that does not fails
into "|| true" and silently collects nothing, which is the failure this whole
change is about.

Still unswept, deliberately: Container, GKE Hub and GKE Backup create nothing
because their cases cannot run without a cluster; KMS keyrings cannot be deleted
by anyone; Essential Contacts and Org Policy are free configuration. Certificate
Manager is swept by #189.

Not verified live - there are no working credentials on this machine right now.
Every command is guarded by "|| true" and every listing is filtered by SWEEP_RE
and KEEP_RE, so the failure mode is collecting nothing rather than deleting
something it should not.

* fix(ci): sweep the resources named before the convention

A live survey of the project found 135 leaked resources, and most of them predate
the naming standardisation: 85 Pub/Sub topics, 21 secrets and 2 networks still
carry formae-plugin-sdk-test-. SWEEP_RE matched only the new shape, so merging
this branch as it stood would have left every one of them in the project for good
- fixing the drift and abandoning what the drift produced.

"plugin" joins the pattern for as long as those exist. It still cannot reach
formae-tester@ or formae-byo-cert, which is the property that matters.

The survey also confirmed what the Cloud SQL prefix was costing: 21 leaked
instances, all named formae-test-sslcert- or formae-test-backuprun-, none of
which the old literal "formae-test-sql" ever matched.

* feat(ci): add find-leaks.sh, and give both sweepers one set of patterns

Nothing in the repo could answer "what has leaked?" without running the full
cleanup, which deletes as it goes. A live survey of the project found 135 leaked
resources - 21 Cloud SQL instances and a Redis instance among them, both billed by
the hour - and none of it was visible from the tooling.

find-leaks.sh surveys by default and deletes only with --delete, so the list can
be read before anything is removed. It groups billable resources first, because
that is the part worth looking at.

Both scripts now read scripts/ci/sweep-patterns.sh instead of each carrying its
own copies. Separate copies are precisely what drifted: the sweeps matched
prefixes the fixtures had stopped using, and the project filled up unseen. One
definition, two readers.

Two details the survey forced, both about not lying:

gcloud exits 0 with empty output when it cannot see a collection, so a non-zero
status is not the only failure - the sweep reads stderr as well and reports
UNREADABLE rather than "clean". And the credential preflight is a real read of a
real resource: gcloud projects describe and gcloud auth print-access-token both
succeed from cache while every request comes back UNAUTHENTICATED, which reports
an unreadable project as an empty one.

REUSE compliant; every tracked shell script still parses.

* fix(ci): sweep packet mirroring policies, and delete Cloud SQL over REST

Two findings from clearing the backlog by hand.

A packet mirroring policy pins the network it mirrors: the network delete answers
"already being used by ... packetMirrorings" and fails. Nothing swept that
collection, so two leaked policies held two networks in the project indefinitely,
and the network sweep looked broken when it was not. It runs before networks now,
where the ordering comment for firewalls and subnetworks already said it belongs.

gcloud cannot delete a Cloud SQL instance in this project at all:

  Invalid request: Final Backup Retention Days can not be set if
  enable_final_backup is disabled

It exits non-zero every time, so a sweep built on it removes nothing while
appearing to try. Twenty-one instances accumulated behind that, all of them
running and billed. clean-environment.sh already used the REST call;
find-leaks.sh now does too.

The project is clean as of this commit: 135 leaked resources removed - 21 Cloud
SQL instances, one Redis instance, 85 Pub/Sub topics, 21 secrets, 7 networks and
the policies and subnets that pinned them. The survey reports zero.
…atch2

# Conflicts:
#	CHANGELOG.md
#	scripts/ci/clean-environment.sh
@naxty
naxty merged commit 1c6aa6c into main Sep 3, 2026
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