Python terraform providers schema JSON census — attribute and block inventory
Plans show intent. Schema shows the contract.
<!-- hal:authoritative:yaml -->
*Plans show intent. Schema shows the contract. Walk providers schema -json. Inventory attributes and blocks before you invent HCL.*
§I - Frame
09-17 walked terraform show -json to prove write-only secrets never landed in state. 09-14 counted import, move, and no-op actions on a saved plan. 09-05 counted create/update/delete/replace risk from show-json resource changes. Keep those roles.
Ops today names google_cloud_run_v2_service and an IAM member with roles/run.invoker. The PR sentence you need before inventing arguments is different: which attributes and nested blocks does the installed Google provider actually advertise for that type?
That sentence is today's census. Call terraform providers schema -json after terraform init. Parse. Filter to one provider (prefer registry.terraform.io/hashicorp/google). Print a markdown inventory for the Cloud Run service type and the IAM member type. Exit zero when Terraform succeeded and the types exist.
No terratest. No Go. Counter 19 is Python-around-TF.
§II - Language idiom: subprocess plus defensive JSON
Python for DevOps treats subprocess.run as the standard-library way to call CLI tools and capture stdout.
import json
import subprocess
import sys
def providers_schema() -> dict:
proc = subprocess.run(
["terraform", "providers", "schema", "-json"],
check=False,
capture_output=True,
text=True,
)
if proc.returncode != 0:
sys.stderr.write(proc.stderr)
sys.exit(proc.returncode or 1)
return json.loads(proc.stdout)
def provider_schemas(doc: dict) -> dict:
# Terraform versions nest under provider_schemas keyed by registry address.
return doc.get("provider_schemas") or {}
Treat schema defensively. Provider version skew changes exact attribute sets. Prefer printing what is present over hard-coding one Google provider version forever.
§III - Census shape for Ops types
Target types for tonight's pair:
google_cloud_run_v2_service(resource)google_cloud_run_v2_service_iam_member(resource), or the_iam_binding/_iam_policysiblings if your module style prefers them
For each type, report:
- attribute names (required vs optional when the schema marks it)
- nested block type names under
block_types - whether
roles/run.invokerappears as a value constraint (usually not; role is a free string attribute)
INTERESTING = {
"google_cloud_run_v2_service",
"google_cloud_run_v2_service_iam_member",
}
def inventory(doc: dict, provider_addr: str) -> list[dict]:
schemas = provider_schemas(doc).get(provider_addr, {})
resources = schemas.get("resource_schemas") or {}
rows = []
for rtype, body in resources.items():
if rtype not in INTERESTING:
continue
block = (body.get("block") or {})
attrs = sorted((block.get("attributes") or {}).keys())
nested = sorted((block.get("block_types") or {}).keys())
rows.append({"type": rtype, "attributes": attrs, "block_types": nested})
return rows
def main() -> None:
doc = providers_schema()
addrs = list(provider_schemas(doc))
google = [a for a in addrs if a.endswith("/hashicorp/google")]
addr = google[0] if google else (addrs[0] if addrs else "")
rows = inventory(doc, addr) if addr else []
print("# providers schema census")
print(f"- provider: `{addr or '(none)'}`")
print(f"- interesting types found: {len(rows)}")
for row in rows:
print(f"## `{row['type']}`")
print(f"- attributes ({len(row['attributes'])}): " + ", ".join(row["attributes"][:40]))
print(f"- block_types ({len(row['block_types'])}): " + ", ".join(row["block_types"][:40]))
missing = INTERESTING - {r["type"] for r in rows}
if missing:
print(f"- MISSING: {', '.join(sorted(missing))}")
sys.exit(2)
if __name__ == "__main__":
main()
Run after terraform init in a workspace that already requires the Google provider (Ops stack or a tiny stub required_providers root). Without init, schema JSON is empty or the CLI fails.
§III.b - How to read a schema row against Ops HCL
When Ops writes:
ingress = "INGRESS_TRAFFIC_ALL"
your census should show an ingress attribute (or the provider's current equivalent) on google_cloud_run_v2_service. If the attribute is missing from schema JSON, either init used the wrong provider address or the installed provider version predates the argument Ops wants. Fail closed. Do not invent HCL that schema cannot see.
When Ops writes:
role = "roles/run.invoker"
member = "serviceAccount:..."
the IAM member type should list role and member (and location/name/project as required by the provider). Schema will not enumerate every valid IAM role string. Seeing role as a free string attribute is success. Searching schema for the literal roles/run.invoker enum is usually a false expectation.
Optional enrichment (not required for exit zero): count total resource types and data source types under the Google provider and print the top-level totals. That number is a drift signal when someone bumps the provider constraint in required_providers. Lab 28 owns how to write the constraint; this census owns proving what the locked provider still exposes.
Keep Maghrib quiz stems on: which CLI emits schema JSON; why init must precede the census; why missing Ops types are a hard fail.
§IV - What this census is not
Do not parse plan JSON again (09-14). Do not walk state for secret absence (09-17). Do not count create/update/delete risk (09-05). Do not call terraform apply -json as the primary stream (valid later fire; not tonight). Do not pin provider versions in this script; Lab 28 owns constraint operators. Lab 06 owns alias and auth wiring; cite it, do not redo it.
§V - Report and done criteria
Print markdown a reviewer can skim:
# providers schema census
- provider: registry.terraform.io/hashicorp/google
- interesting types found: 2
## google_cloud_run_v2_service
- attributes (N): name, location, ingress, ...
- block_types (N): template, ...
## google_cloud_run_v2_service_iam_member
- attributes (N): role, member, name, location, project, ...
- block_types (0):
Success: both Ops types appear; attribute lists include ingress (or equivalent) on the service and role / member on the IAM member; exit zero. Missing type exits non-zero so CI fails closed when the provider version cannot see the resource Ops plans to declare.
§VI - Close
Ops declares Cloud Run and invoker IAM. Python proves the provider schema still advertises those types and names the knobs. Pair Cert for data-source read-only edges. Maghrib owns quiz.html later. No Go companion.
Related
- Ops: Cloud Run + run.invoker
- Prior: state-json secret absence (09-17)
- Cert: data sources and depends_on
- Bootcamp Lab 06