Skip to content

feat(bigtable): add AppProfile, and fix five defects in existing Bigtable code - #170

Merged
naxty merged 13 commits into
naxty/gcpDnsBatchfrom
naxty/gcpBigtableBatch
Aug 31, 2026
Merged

feat(bigtable): add AppProfile, and fix five defects in existing Bigtable code#170
naxty merged 13 commits into
naxty/gcpDnsBatchfrom
naxty/gcpBigtableBatch

Conversation

@naxty

@naxty naxty commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Adds GCP::Bigtable::AppProfile — and fixes five defects in Bigtable code that
shipped before this PR. Stacked on #169.

Case Result
bigtable-app-profile crud 8/8, discovery 4/4

Verified through debug-conformance.yml; the local key is 403 on Bigtable.

One type, five fixes. That ratio is the point of this PR: every one of these
was found by writing a conformance fixture for something that already existed.

The fixes

  1. Status now routes through base.StatusWithRead. BigtableProvisioner
    embeds *base.BaseResource and overrode only Create, so it inherited the
    raw Status, which reports success and no properties. A completed async
    create therefore left the resource with nothing to read — so a reference to a
    Bigtable instance never resolved, and a table declared alongside its instance
    failed with "instance is required for nested resources". Affects
    Instance, Cluster and Table.
  2. Create unwraps wrapped property values, as base.Create already did
    twice. Without it any property carrying a reference read as empty.
  3. An app-profile create is synchronous. appProfiles.create answers with
    the resource, not an Operation, so polling looked for an operation id that
    was never there and asked the bare base URL, which answers 404.
  4. The native-ID parser no longer switches on each collection by name. An
    unlisted collection parsed to an empty resource type and read nothing,
    silently — and I was about to add the fifth and sixth special case.
  5. base.ResourceConfig gains DeleteQueryParams. Bigtable refuses an
    app-profile delete without ignoreWarnings=true, and its own discovery
    document calls the parameter "Required" while flagging it optional.

Also: Bigtable::Table accepts a resolvable for instance. As a plain
String a table could only ever name an instance that already existed, so it
could not be declared in the same forma as its instance — which is why the type
still has no conformance case.

And materialized_view.pkl now describes the real API. It demanded a
cluster (materialized views are instance-scoped) and omitted query, which is
required, so the module was declarable and broken on contact.

What I could not verify

  • LogicalView and MaterializedView are not in this PR. Both create in
    some runs and fail at Create in others, and settling that needs Bigtable SQL
    semantics that cannot be tested locally — each attempt costs a CI cycle on a
    concurrency group the nightly also needs. LogicalView is removed entirely;
    MaterializedView keeps its corrected schema and goes back to being a
    recorded knownParityGaps entry, so the repo is left better than it was found.
  • The bigtable package had no unit tests; the parser has some now.

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 10 commits August 28, 2026 05:32
MaterializedView's schema module already existed with no provisioner, recorded
in knownParityGaps - and it described a resource the API does not have. It
demanded a cluster, though materialized views are instance-scoped, and omitted
query, which is required. Rewritten to match the API; the parity test then
refused to pass until its known-gap entry was removed, which is what that test
is for.

The three new types deliberately bypass BigtableProvisioner. It derives the
create-id query parameter by trimming a trailing "s" and appending "_id",
which is right for instances and clusters but would ask for "appProfile_id"
where the API wants "appProfileId". They use the generic engine's
CreateIDParam, which sends exactly what it is given.

Two fixes to existing code fell out of writing the fixtures:

Bigtable::Table declared instance as a plain String, so a table could only ever
name an instance that already existed and could not be declared in the same
forma as one. That is why the type still has no conformance case. It now takes
a resolvable, as every other nested type here does.

The native-ID parser switched on each collection by name with no default, so an
unlisted collection parsed to an empty resource type and read nothing. Adding
appProfiles and logicalViews as the fifth and sixth special case would have
kept the trap; every instance-scoped collection now shares one branch. The
package had no unit tests, so the parser has some now.
Two fixture errors, both mine.

An app profile with multiClusterRoutingUseAny needs an instance with more than
one cluster to route across; the host here is DEVELOPMENT, which has exactly
one. It now pins traffic to that cluster.

Both view types run SQL over a table, and the table had no column families, so
there was nothing for a query to read. Each now declares one.

Diagnosed from timing rather than from the API's own message: the agent logs
"Updating resource state to Failed" with an empty native ID and nothing else,
so a plugin-side create failure carries no status code or message into the job
log. Worth fixing on its own.
BigtableProvisioner.Create unmarshals properties and never called
base.UnwrapValues, which base.Create does twice. Any property whose value
arrived wrapped - which is what a reference to another resource produces - read
as empty, so a table declared alongside its instance failed with "instance is
required for nested resources" while the instance sat right next to it in the
same forma.

That is why bigtable's table schema typed instance as a plain String, and why
the type has no conformance case: with a literal string a table can only name
an instance that already exists, and a reference never worked.

Found by running the discovery phase purely to read the error: an out-of-band
create reports the plugin's own message, while the same failure inside an apply
reports nothing at all.
"instance is required for nested resources" is unfalsifiable from a CI log:
the property may be absent, present under another name, or present as a type
this code cannot read. A plugin-side create failure carries no other diagnostic
out of an apply, so that one line was the whole signal - and it sent me down
three wrong explanations in a row.

The error now names the value's Go type and the property keys the request
actually carried.
BigtableProvisioner embeds *base.BaseResource and overrode only Create, so it
inherited the raw BaseResource.Status - which reports success and nothing else.
UnifiedProvisioner wraps that with a Read for exactly this reason; a
hand-written provisioner has to do the same, or a resource has no properties
after an async create completes.

The symptom was a reference to a Bigtable instance never resolving. With no
properties on the instance there is no ".name" to read, so a table declared
alongside its instance reached the plugin with the reference still unresolved
and failed with "instance is required for nested resources".

This affects Instance, Cluster and Table - every Bigtable type that existed
before this batch.
appProfiles.create answers with the AppProfile itself, not an Operation, but
the package's single OperationConfig declares every Bigtable create async. base
therefore looked for an operation id that was never there, got "", and polled
BaseURL + "/" - which answers 404:

    failed to get operation status: HTTP 404 error
    (URL: https://bigtableadmin.googleapis.com/v2/)

Of the five collections, appProfiles and tables answer with the resource;
clusters, logicalViews and materializedViews answer with an Operation. Only the
app profile needed the synchronous config here - tables go through
BigtableProvisioner, which handles their response shape itself.
…p profiles

Some GCP deletes are refused without a force flag. Bigtable answers an
app-profile delete with a safety-check failure unless ignoreWarnings=true is
sent - and its own discovery document describes the parameter as "Required"
while flagging it optional, which is how it went unnoticed.

ResourceConfig already had UpdateQueryParams; DeleteQueryParams is the same
idea for the delete path. The app profile now sends ignoreWarnings on both,
since a routing change needs it too.

Create, Verify, Extract, Sync and Update all passed for the app profile before
this; Destroy was the last red cell.
App profiles and both view types live under an instance, and discovery lists
with no properties - so it can name no instance, the path builder falls back to
a project-level collection URL that addresses nothing, and the conformance
Discover step times out with "resource did not appear in inventory".

This is the fifth instance of the same shape in this plugin (Service Directory,
Spanner, Cloud SQL, DNS and now Bigtable): a nested resource that GCP offers no
wildcard for has to be found by walking its parents.

The app profile's CRUD is 8/8 as of the previous commit; this is what stands
between the three new types and a green discovery phase.
The materialized-view fixture failed at Create with

    Error in field 'display_name' : value must be between 4 and 30 characters
    in length, inclusive, but got length 33

which was my own doing: renaming the host instance from "Logical View" to
"Materialized View" pushed the display name over the cap.

The limit is now stated in the instance schema, so a fixture that breaks it
fails at eval instead of after provisioning an instance - the same treatment
Spanner's displayName got for the same reason.
LogicalView and MaterializedView are out. Both create fine some runs and fail
at Create in others with no message the apply path surfaces, and settling that
needs Bigtable SQL semantics I cannot test locally - each attempt costs a CI
cycle on a concurrency group the nightly also needs. AppProfile is verified
8/8 and 4/4, and the five fixes below stand on their own; holding them hostage
to two views was the wrong trade.

LogicalView is removed entirely. MaterializedView keeps its corrected schema
and goes back to being a recorded parity gap, so the repo is left better than
it was found: the module now describes the real API instead of demanding a
cluster the resource does not have and omitting the query it requires.
@naxty
naxty merged commit 8b4f034 into naxty/gcpDnsBatch 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