Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,23 @@ A small number of files are scaffolded once and then owned by you — those carr
no such header, and the generator will not touch them again. The
[generated boundary](docs/) documentation is the full list.

Two consequences worth knowing before opening a pull request:
Three consequences worth knowing before opening a pull request:

- **Emitted output must be deterministic.** No timestamps, no tool version, no
absolute paths, and no reliance on Go map iteration order in anything written
to disk. A single such value makes the drift check fail on a run that changed
nothing, which destroys its usefulness.
- **Never commit a real credential to probe evidence.** Cassettes are committed
by design. Redaction allowlists header values rather than denylisting them, and
a final pass fails the run outright if any known secret survives into the
output. Run `tfpluginframeworkgen probe redact --check` before pushing.
a final pass fails the recording outright — exit `7`, nothing written — if any
credential-shaped value survives into the output. There is nothing extra to run
before pushing; the check is part of every record run, and
`probe -mode verify` re-derives committed evidence offline.
- **Regenerate with the postcheck on.** `emit` finishes by compiling the output,
regenerating its registry docs and running `terraform fmt`
(`-skip-postcheck` exists for tight inner loops only). A PR that arrives with
stale `docs/` or unformatted fixtures is a PR whose final `emit` was skipped —
CI will say so, one gate later.

<!-- References -->

Expand Down
271 changes: 137 additions & 134 deletions README.md

Large diffs are not rendered by default.

39 changes: 37 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
# Security Policy

If you discover a security issue in this repo, please submit it through the [GitHub Security Bug Bounty](<https://hackerone.com/github>)
## Reporting a vulnerability

Thanks for helping make GitHub safe for everyone.
Please report suspected vulnerabilities privately via
[GitHub private vulnerability reporting](https://github.com/deploymenttheory/terraform-plugin-framework-codegen/security/advisories/new)
on this repository. Do not open a public issue for anything you believe is a
security problem. You should receive an acknowledgement within a few days; please
allow a reasonable window for a fix before any public disclosure.

## What is security-sensitive in this repository

This project probes live APIs with real credentials and commits the resulting
transcripts, so its threat surface is specific and worth stating:

- **Bearer tokens.** Credentials are read from the environment only
(`TFPFGEN_PROBE_TOKEN`; the generated pilot provider uses
`THOUSANDEYES_BEARER_TOKEN`). No command accepts a token flag, and the sandbox
profile loader refuses a profile containing a credential-shaped value. Anything
that would move a token into a file, a flag, a log line or a committed artefact
is a vulnerability — report it.
- **Committed cassettes.** Probe transcripts under `probe-evidence/` are public
by design. Redaction allowlists rather than denylists, and a recording fails
outright (exit `7`, nothing written) if a credential-shaped value survives.
A redaction bypass — any way a secret can reach a committed cassette — is the
highest-severity bug this repository can have.
- **Sandbox profiles.** `.tfpluginframeworkgen/sandbox/` is gitignored because a
profile carries tenant identifiers. Committed artefacts must never contain
tenant names; opaque numeric identifiers are accepted.
- **Generated fixtures.** The fixture derivation refuses to invent values for
credential-shaped fields, so generated test configurations cannot contain
anything that looks like a secret. Secret-bearing attributes are generated as
sensitive or ephemeral and never written to state.

## Scope

The generated pilot provider (`pilot/thousandeyes/`) is not published to the
Terraform registry; issues in it are still welcome here, since its code is this
repository's output. Vulnerabilities in the ThousandEyes API or SDK belong with
their respective owners.
2 changes: 1 addition & 1 deletion cmd/tfpluginframeworkgen/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var commands = []command{
{
name: "ingest",
summary: "infer a provider blueprint from an OpenAPI snapshot",
usage: "ingest [-spec FILE] [-out DIR] [-only TAG] [-list]",
usage: "ingest [-spec-root DIR] [-snapshot NAME] [-only TAG] [-list]",
run: runIngest,
},
{
Expand Down
47 changes: 30 additions & 17 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,28 @@ Documents are added as the phase that needs them lands, rather than up front as
empty stubs — a stub describing an unbuilt design is worse than no document,
because it reads as settled.

The consequence is that **outstanding** means the pipeline it documents is built
and the document is owed, which is a different thing from a phase not having
arrived yet.

| Document | Contents | State |
|---|---|---|
| [`architecture.md`](architecture.md) | the pipeline stages, and where logic is allowed to live | **written** |
| [`cli.md`](cli.md) | command reference, flags and exit codes | **written** |
| [`blueprint.md`](blueprint.md) | the IR: every field, and what it exists to express | **written** |
| [`generated-boundary.md`](generated-boundary.md) | how generated and hand-written code stay apart, and the escape hatch | **written** |
| [`onboarding-a-new-api.md`](onboarding-a-new-api.md) | numbered runbook for taking on a new API end to end | **written** |
| [`interop.md`](interop.md) | reading and writing Provider Code Specification v0.1, and what it cannot carry | **written** |
| [`probing.md`](probing.md) | the probe catalogue, confidence levels, safety model and cleanup guarantees | **written** |
| `pilot-thousandeyes.md` | the pilot: what was generated, and the before/after against the existing provider | Phase 6 |
| `adr/` | decision records, so settled questions are not re-litigated in six months | ongoing |
| Document | Contents |
|---|---|
| [`architecture.md`](architecture.md) | the pipeline stages, and where logic is allowed to live |
| [`onboarding-a-new-api.md`](onboarding-a-new-api.md) | the numbered runbook for taking on a new API end to end |
| [`cli.md`](cli.md) | command reference, flags and exit codes |
| [`blueprint.md`](blueprint.md) | the IR: every field, and what it exists to express |
| [`probing.md`](probing.md) | the probe catalogue, confidence levels, safety model and cleanup guarantees |
| [`fixtures-and-rehearsal.md`](fixtures-and-rehearsal.md) | one fixture derivation, the rehearsal probe, and the fixpoint between them |
| [`generated-boundary.md`](generated-boundary.md) | how generated and hand-written code stay apart, and the escape hatch |
| [`gates.md`](gates.md) | every CI gate, what it proves, and its local reproduction |
| [`interop.md`](interop.md) | reading and writing Provider Code Specification v0.1, and what it cannot carry |
| [`findings/`](findings/) | investigation write-ups whose evidence shaped the toolkit — kept as history |
| [`examples/`](examples/) | starting points, e.g. the sandbox profile |

The pilot provider documents itself: [`pilot/thousandeyes/README.md`](../pilot/thousandeyes/README.md)
covers what it proves and how to run it, and its `docs/` directory is
tfplugindocs output for the Terraform registry.

## Decisions already taken

Recorded here until `adr/` exists, because they shape everything else and are the
questions most likely to be asked again.
Recorded here because they shape everything else and are the questions most
likely to be asked again.

1. **Provider layer only.** The toolkit generates the Terraform provider. SDKs
already exist and are generated elsewhere; this project binds to them.
Expand All @@ -39,6 +41,17 @@ questions most likely to be asked again.
Every transcript is committed, and facts are re-derived from it offline in CI.
4. **The pilot is a new provider, not a migration.** Generating onto a clean
slate keeps state-upgrade work off the toolkit's critical path.
5. **Acceptance is confirmation, not discovery.** Everything an acceptance test
would discover is discovered by the probe first — the rehearsal runs the same
lifecycles with the same values before any provider code exists. A red
acceptance run means the evidence is incomplete, and the fix starts with
`probe`.
6. **Generation finishes with the tools that gate it.** `emit` runs the same
compile, docs and formatting checks CI runs, at generation time. A gate that
fires after the commit is just a slower postcheck.
7. **`deny` gates experiments, not sends.** A denied field is never probed, but a
fixture value declared for it still goes into every body that needs it —
otherwise a denied-but-required field would sink every create.

## Conventions worth knowing before reading the code

Expand Down
56 changes: 38 additions & 18 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,30 @@ those boundaries are drawn where they are.
## The pipeline

```
OpenAPI snapshot ──ingest──┐
├──merge──► blueprint.json ──emit──► provider Go tree
live API ────────probe──────┤ + tests, mocks,
│ fixtures, docs
human overrides ────────────┘
upstream URL ──specs──► OpenAPI snapshot ──ingest──┐
├──merge──► blueprint.json ──emit──► provider Go tree
live API ──probe (catalogue + rehearsal fixpoint)───┤ │ + tests, fixtures,
pinned SDK ──bindings -facts-out (static facts)─────┤ │ examples, docs
│ postcheck
human curation (plans, hints, presence) ────────────┘ (build · docs · fmt)
```

Every arrow's output is a committed, reviewable artefact. That is deliberate: a
pipeline whose intermediate state lives only in memory can only be reviewed by
reading its output, and its output is thousands of lines of generated Go.
reading its output, and its output is thousands of lines of generated Go. And
every arrow has a drift gate in CI — see [gates.md](gates.md).

Every stage is built, and each is a subcommand of the one binary.

| Stage | Package | State |
|---|---|---|
| `specs` — fetch and pin an OpenAPI snapshot | `internal/specstore` | **half-built** — read, list and checksum-verify work; the fetch is a stub, so snapshots are pinned by hand |
| `ingest` — OpenAPI → blueprint | `internal/ingest/openapi` | **built** |
| `probe` — live API → behaviour facts | `internal/probe` | **built** (record and replay) |
| `merge` — fold facts into a blueprint | `internal/blueprint/merge` | **built** |
| `emit` — blueprint → provider | `internal/emit`, `internal/render`, `internal/templates` | **built** |
| `specs` — fetch and pin an OpenAPI snapshot | `internal/specstore` | **built** — the refresh loop re-fetches from the latest snapshot's recorded source |
| `ingest` — OpenAPI → blueprint + plan drafts | `internal/ingest/openapi` | **built** |
| `probe` — live API → behaviour facts | `internal/probe` | **built** (record, replay, verify, sweep; the rehearsal fixpoint) |
| `merge` — fold facts into a blueprint | `internal/blueprint/merge` | **built** (plus plan promotion) |
| `emit` — blueprint → provider, postchecked | `internal/emit`, `internal/render`, `internal/templates` | **built** |
| `verify` — fail on drift | `cmd/tfpluginframeworkgen/verify.go`, `internal/manifest` | **built** |
| `bindings` — type-check bindings against the SDK | `internal/sdkbind` | **built** |
| `bindings` — type-check bindings against the SDK; derive static facts | `internal/sdkbind` | **built** |
| `interop` — Provider Code Specification v0.1 | `internal/interop` | **built** (export; import writes drafts) |

## Where logic is allowed to live
Expand Down Expand Up @@ -61,21 +63,35 @@ declarations, which the file template merely lists.
cmd/tfpluginframeworkgen/ CLI. stdlib flag, one FlagSet per subcommand, no cobra.
internal/
blueprint/ the IR: types, validation, canonical JSON, layered load
render/ ALL logic. Blueprint -> finished strings.
render/ ALL emit logic. Blueprint -> finished strings.
templates/ embedded .tmpl. The emitted shape, as reviewable text.
emit/ plan, format, write. Owns gofumpt and overwrite refusal.
emit/ plan, format, write, postcheck. Owns gofumpt and overwrite refusal.
manifest/ what the last run produced, so orphans can be found
naming/ identifiers. One word-splitter, several joins.
sdkbind/ type-checks bindings against the SDK actually pinned
-- every kind that has one: resources, data sources,
list facets and actions. It walked resources alone until
actions landed, so three quarters went unchecked
list facets and actions. Also derives static facts
(zero-value unsendable) from the SDK's struct tags.
fixturespec/ ONE derivation of acceptance-fixture values, rendered
as HCL by render and as wire JSON by probe
ingest/openapi/ OpenAPI document -> draft blueprints + plan worksheets
specstore/ pinned OpenAPI snapshots: list, read, checksum, pin
probe/ the probe catalogue, gate, ledger, sweeper, budgets
cassette/ transcripts: record, redact, replay, freeze
interop/ codegen-spec v0.1 export and import
version/ the tool version, in exactly one place
```

`naming` and `blueprint` depend on nothing else in the repo. `render` depends on
`blueprint` and `naming`. `emit` depends on `render` and `templates`. Nothing
depends on `cmd`.
`blueprint`, `naming` and `fixturespec`. `emit` depends on `render` and
`templates`. Nothing depends on `cmd`.

Two behaviours live in `cmd` rather than in a stage package, on purpose. The
rehearsal fixpoint (`rehearse.go`) alternates `probe` and `merge` until fixture
derivation converges, and plan promotion (`promote.go`) copies plan values into
blueprint hints — both need packages from opposite ends of the pipeline, and the
probe must never import the package that interprets its output. The command layer
is the only place allowed to see both at once.

## Determinism

Expand Down Expand Up @@ -133,4 +149,8 @@ The generator fails loudly rather than emitting something approximate.
- [`blueprint.md`](blueprint.md) — the IR, field by field
- [`generated-boundary.md`](generated-boundary.md) — what is generated, what is
yours, and how the two are kept apart
- [`probing.md`](probing.md) — the probe catalogue, gate, ledger and budgets
- [`fixtures-and-rehearsal.md`](fixtures-and-rehearsal.md) — fixture derivation
and the rehearsal fixpoint
- [`gates.md`](gates.md) — every CI gate and its local reproduction
- [`cli.md`](cli.md) — command reference
Loading
Loading