Hedronite · Dev Lesson · Polyglot-Dev / Python · Wed 2026-09-23

Python terraform providers schema JSON census — attribute and block inventory

Plans show intent. Schema shows the contract.

Lesson Class: Dev (Python-around-TF)
Ops Pair: Cloud Run v2 service + run.invoker IAM
Tooling: subprocess.run · terraform providers schema -json · type inventory
Grounding: Python for DevOps subprocess · Lab 06 · Lab 28 referenced
Schema JSON
Capture provider contracts via subprocess after init.
Filter types
Cloud Run service + IAM member rows only.
Fail closed
Missing Ops types exit non-zero.
Init first. Inventory the types. Exit zero only when Ops rows exist.

<!-- 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:

  1. google_cloud_run_v2_service (resource)
  2. google_cloud_run_v2_service_iam_member (resource), or the _iam_binding / _iam_policy siblings if your module style prefers them

For each type, report:

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