Repository navigation
feat: the stackorder provider with the stackorder_outputs data source and ephemeral resource - #1
Merged
Merged
Conversation
Start the provider as an empty Plugin Framework provider served over protocol 6 at registry.terraform.io/stackorder/stackorder, so the client, configuration, data source and ephemeral resource can land as separate changes on a module that already builds, lints and tests. The module targets go 1.26.0 like the Stackorder module it will import and pins the framework, plugin-go and plugin-log releases it is built against; plugin-log stays indirect until the client logs through it. The unit test drives the provider through the protocol 6 server, the same path Terraform uses, and needs no Terraform binary. The protocol carries no provider type name, so the test reads it from Metadata. The lint setup mirrors Stackorder's so both repositories follow one style, and its gofmt step fails on files gofmt cannot parse. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The provider needs one call, GET /v1/repos/{owner}/{repo}/stacks/{key}/outputs,
so the client is small and owns the things that are easy to get wrong.
Inside GitHub Actions the server accepts each OIDC token only once, so every
attempt, retries included, mints a new token; an API key is used only when
the Actions token variables are absent, so an admin key never silently wins
in CI. The audience defaults to the server URL as configured, trimmed as the
stackorder CLI trims it, and when nothing names an audience the source is
refused up front instead of sending one the server would answer with a bare
401. An empty run or reader is left out of the query for API keys, which
read outside any run.
Network failures, a connection lost or timed out while the answer is read
included, and 429 and 5xx answers, from the server or from the token
endpoint, are retried up to three attempts with backoff, waiting longer when
a Retry-After header asks for up to a minute; a longer request ends the
call. Every other answer is final and returned as an *APIError carrying the
v1.Error code, so the data source can map each refusal to its own diagnostic
without retrying a refusal that cannot change. The answer is read whole,
capped at 32 MiB, four times the server's cap on an outputs block, before it
is decoded, so a cut connection is retried while a malformed body is final.
The retry loop is written out rather than taken from go-retryablehttp
because the token must be replaced on every attempt.
Neither HTTP client follows redirects: Go resends the Authorization header
to the same host even from https to http, and a followed redirect would
replay a single-use token. The server URL may carry a path prefix but no
user info, query or fragment, which would break the endpoint built from it,
and a stack key of . or .. is refused because proxies normalise dot
segments. The token sources are unexported, so no zero value can send an
empty key or panic, and every format verb prints a description without the
key or the request token. Tokens never appear in error messages.
The wire types come from github.com/stackorder/stackorder/api/v1. go.mod
requires v0.3.0, the newest tag, as a placeholder until the release that
carries StackOutputs; until then local builds use an untracked go.work that
includes the stackorder checkout, as the README explains. go mod tidy
ignores go.work, so the tidy target needs no flag; requiring v0.3.0 raises a
few indirect versions through minimal version selection. The golden
stack_outputs.json from api/v1 is copied into testdata to pin the wire
contract on this side.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The provider takes server_url, api_key, repository and oidc_audience, each optional with an environment fallback, so a Stackorder job needs no provider configuration at all: the Stackorder CLI exports the server URL, repository, run id and stack key to Terraform. Unknown values refuse to configure, since a fallback chosen at plan time could differ at apply. Credentials come from the client's token source, which owns the API key and audience fallbacks and prefers GitHub Actions ID tokens whenever the runner offers them. The client now exports that check so the provider can tell when an ID token will be used without re-reading the runner variables. With an ID token the server can only bind the read to a stack of a Stackorder run, so the provider refuses to configure without STACKORDER_RUN_ID and STACKORDER_STACK and says to use an API key outside such a job. Diagnostics carry the source of a bad value, never a key or token. The client's errors for a rejected server URL no longer repeat the URL. Redacted hides only a password that url.Parse finds in user info, so a password in a URL without a scheme, a password read as a port, or a key in the query reached the diagnostic. Tests drive Configure directly and over protocol 6, and check where requests go and with which credentials against local fakes of the server and the Actions token endpoint, including server URLs that carry a password or a key. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The data source and the ephemeral resource expose a stack's outputs as one dynamic object each, so every output, sent as its JSON value beside the type Terraform wrote to state, has to become a framework value of exactly that type: a tuple must not turn into a list, numbers keep the 512-bit precision Terraform reads them at, and nulls stay typed. Types are parsed here, including the ones dynamically typed values carry inside the value, and values are walked here too, so tftypes only reads strings, numbers and bools. Its own type parser, deprecated for third parties, keeps the optional attribute markers of object types, and tftypes panics when such a type later meets a value, which would crash the provider. Building tftypes values still recovers from a panic as a backstop. Every type is rebuilt as the type of its contents, as cty computes it, so dynamically typed positions resolve. A list, set or map of dynamic elements takes the type its elements share and turns its null elements into nulls of that type, as cty does; elements of different types are refused, since Terraform cannot build such a value. Scalars are read as Terraform reads JSON, so a number may arrive as a string and the reverse. No outputs give an empty object rather than null. Errors name the output and what is malformed but never quote the value, which may be sensitive, so neither tftypes nor framework error text is passed through for values. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A reading stack names a producer as path, path:instance or owner/repo//key and receives the outputs the server serves as two dynamic objects: values, sensitive as a whole because a provider cannot mark nested paths, and nonsensitive_values for arguments that refuse sensitive values. The repository of the stack is checked as owner/repo before any request, so a malformed key or repository setting is reported on stack and names the setting that holds it. Every refusal code of the server, the run state codes included, becomes its own diagnostic that says who can act on it: the producer, the server's operator or the reader's credentials. None repeats a token, a value or the request URL. An answer without a v1 error code, as from a proxy in front of the server, is reported by status and redirect host only, so its body never reaches Terraform's output. Withheld sensitive outputs raise a warning that names them and how the producer shares them. The provider data now records the server host and whether ID tokens authenticate, so diagnostics name the host only and give credential advice for the mode in use. Acceptance tests run terraform-plugin-testing against an httptest server that serves the client's golden fixture. plugin-testing cannot observe warnings, so the withheld warning is asserted through the protocol 6 server in the unit tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Values a data source reads land in the reader's plan file, which Stackorder uploads as a workflow artifact, and in state, so a shared sensitive output had no safe way in. The ephemeral resource reads the same outputs with the same stack, values and nonsensitive_values, minus id, and Terraform keeps its result out of both. It needs Terraform 1.10 or OpenTofu 1.11. Open goes through the data source's read helper, so producer resolution, the request, the conversion, every refusal diagnostic and the withheld warning are the same code; the type check of the provider data is shared too. There is no Renew or Close: the outputs are a copy of state values, not a lease, so nothing expires or needs releasing. The data source description, the withheld warning and the README now point at ephemeral "stackorder_outputs" for sensitive values, and the warning no longer assumes the reader is a data source. The data source's read and error tables now drive both kinds over protocol 6, so the ephemeral result is checked for the exact types, the missing id and every error mapping. The acceptance test passes the result to the echo provider and checks that a shared sensitive output reaches it, that the pre-apply plan holds neither the value nor any resource but echo.x, and that state holds only echo.x and the value nowhere else. Terraform never lists ephemeral resources in plan or state JSON, so the checks name what must be there rather than search for what must not. OpenTofu reports its own version, so the test skips below 1.11 there and below 1.10 on Terraform. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Terraform Registry renders a provider's documentation from docs/ at each release tag, so the pages must be in the repository before the first tag. tfplugindocs renders them from templates, the example files and the schema's descriptions, so attribute text lives only in the Go schema and the pages cannot drift from it. The overview page says what the provider is for, how a producer opts in with outputs.readers and outputs.sensitive, which credentials apply inside and outside Stackorder jobs and which Terraform and OpenTofu releases are needed, and links to the Stackorder documentation. The data source and ephemeral resource pages add how values are addressed and how Stackorder orders a reader after its producer. The generator is a tool dependency in go.mod, pinned to terraform-plugin-docs v0.25.0 and run with go tool from go:generate lines in main.go, after terraform fmt on the examples the pages embed. It reads the schema with Terraform 1.14.4, which it downloads, because a terraform older than 1.10 on PATH cannot export the ephemeral resource schema and leaves docs/ half deleted, and a pinned release keeps the pages identical on every machine. Template and page names leave out the stackorder_ prefix because the generator and the registry look them up that way. make docs-check regenerates the pages and fails when docs/ or examples/ differ from HEAD, staged changes included, or docs/ holds an untracked page. CI can run it once go.mod requires the Stackorder release with the api/v1 stack outputs types; until then the provider builds only in a Go workspace with a Stackorder checkout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Terraform and OpenTofu registries ingest a provider from its GitHub releases: one zip per platform holding a binary named terraform-provider-stackorder_v<version>, a SHA256SUMS file covering the zips and the registry manifest, and a binary detached GPG signature of that file. GoReleaser builds these assets in the layout and for the platforms of the HashiCorp scaffolding, which covers the registry's recommended list including linux/s390x. The manifest declares plugin protocol 6, which the Terraform Registry would otherwise record as protocol 5. A pre-release tag becomes a GitHub pre-release. The release workflow runs on v* tags in the release environment, so the signing key can be limited to tag runs, and takes the release notes from the tag's CHANGELOG section as the Stackorder release does, failing when that section is missing. It builds with the newest stable Go and without a restored cache: go-version-file would install go.mod's go 1.26.0 directive as the exact toolchain, and a cache written by other runs could reach the signed binaries. govulncheck stops a release whose code reaches a known vulnerability before the key is imported. Actions are pinned to full commit SHAs and GoReleaser to v2.18.2, and checkout keeps no git credentials because GoReleaser publishes through GITHUB_TOKEN. The workflow builds without the local go.work, so a tag can only be cut once go.mod requires the Stackorder release carrying the api/v1 outputs types; that change also has to raise grpc and x/net past their known vulnerabilities for the govulncheck step to pass. The changelog names Stackorder 0.4.0 as the planned minimum. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Run lint, unit tests, a docs check and builds on every push to main and every pull request. The acceptance tests run against Terraform 1.10, the first release with ephemeral resources, and the latest Terraform, and against OpenTofu 1.11, its first release with them, and the latest OpenTofu. For OpenTofu, setup-opentofu's provider_acceptance_tests input points terraform-plugin-testing at the tofu binary and at OpenTofu's registry host, which the reattached provider's address must carry. The docs job runs go generate, which formats examples/ and renders docs/ from them, and fails when either differs from the commit or when it creates pages that were never committed, which git diff alone does not report. Actions are pinned to commit SHAs and checkouts do not keep the token. The module compiles in CI only once go.mod requires a Stackorder release whose api/v1 carries StackOutputs; until then every job stops at compile time, and the build step's name says so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e test OpenTofu 1.11 lists an ephemeral resource in a plan's resource_changes and planned_values, with mode ephemeral and the action open, so the plan check reported ephemeral.stackorder_outputs.vpc as a change beside echo.x. The plan walkers now skip entries whose mode is ephemeral. The state check still rejects everything but echo.x, ephemeral entries included, so the test still proves the ephemeral resource never reaches state, and the canary check still covers the whole plan JSON. OpenTofu 1.11 also reports the open as a change in the plan terraform-plugin-testing makes after apply, so that plan is never empty there. The step sets ExpectNonEmptyPlan only when the CLI is OpenTofu older than 1.12, because plugin-testing v1.16.0 fails a step that sets it when the plan comes back empty, as it does on OpenTofu 1.12 and later and on Terraform. tofu version -json does not name the product, so the CLI counts as OpenTofu when its binary is named tofu or TF_ACC_PROVIDER_HOST is OpenTofu's registry, and only the version is read from that JSON. Both helpers have unit tests. Since ExpectNonEmptyPlan switches off the empty-plan check, both post-apply plans now require echo.x to be a no-op, which keeps catching drift on OpenTofu 1.11. The acceptance tests pass on Terraform 1.10.5, 1.14.4 and 1.16.5 and on OpenTofu 1.11.14, 1.12.6 and 1.13.1. go-version moves from an indirect to a direct requirement at the same version. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Stackorder scanner now reads stackorder_outputs blocks in every local module a stack reaches, not only in the stack's own directory, and resolves a module's var.<name> references from the literal arguments of the module block that calls it, one level up. The Ordering section of the data source and ephemeral resource pages still said a block inside a module is never inferred, which would send readers to depends_on for lookup modules the scanner now orders on its own. The section now states the rule as the scanner applies it: literal stacks in the stack's directory or its local modules, module variables set to literals by the calling module block, and depends_on for any other expression. It names every module the scanner leaves unread, registry, git and other remote modules as well as local sources outside the repository, so readers know a block there also needs depends_on. It keeps terraform_remote_state tied to the stack's own directory, since the scanner does not follow that data source into modules, and it says that depends_on does not silence the warning for a non-literal stack, so readers do not take the remaining warning for a missing dependency. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Stackorder v0.4.0 is the first release whose api/v1 package has the shared-outputs types the provider decodes. Requiring it replaces the v0.3.0 placeholder, so the provider builds and tests from the published module without a Go workspace that points at a local checkout. The CI build step and the README no longer describe that arrangement. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
govulncheck reports GO-2026-6061, GO-2026-6348 and GO-2026-6443 in google.golang.org/grpc v1.82.0, and GO-2026-6603, GO-2026-6610, GO-2026-6611, GO-2026-6612 and GO-2026-6617 in golang.org/x/net v0.57.0, each with call paths from the provider's code. grpc v1.83.1 fixes GO-2026-6348 but not GO-2026-6443, which the 1.83 line fixes in v1.83.2, so this requires grpc v1.83.2 and x/net v0.60.0. x/net v0.60.0 raises its own minimums, which tidy carries over: x/crypto v0.57.0, x/mod v0.41.0, x/sync v0.23.0, x/sys v0.48.0, x/text v0.42.0 and x/tools v0.49.0. govulncheck v1.8.0 on Go 1.27.2 then finds no vulnerabilities in the code or in any required module. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Stackorder 0.4.0 is now released and is the first CLI release that passes the server URL, the run and the repository to Terraform. The README, the provider page and the changelog named it only as the release after 0.3.0 or as a planned one, so they now name 0.4.0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The initial
stackorderprovider for Terraform and OpenTofu, atregistry.terraform.io/stackorder/stackorderon plugin protocol 6. It reads the outputs another stack shares through the Stackorder server (stackorder/stackorder#5, released in v0.4.0), so a consumer stack needs no access to the producer's state object. It replacesterraform_remote_statefor stacks that Stackorder runs.stackorder_outputsdata source.stacknames the producer aspath,path:instanceorowner/repo//key.valuesholds every output the producer serves and is sensitive;nonsensitive_valuesholds the ones not marked sensitive. A warning names the sensitive outputs the producer withholds. Each refusal of the server (outputs_disabled,outputs_not_shared,not_found,state_location_unknown,state_bucket_not_allowed,state_not_found,state_access_denied,state_encrypted,state_unsupported,state_too_large,state_unreadable) gets its own diagnostic saying what to check or change.stackorder_outputsephemeral resource. Samestack,valuesandnonsensitive_values, never written to the plan file or the state. Terraform 1.10 or OpenTofu 1.11 or later.server_url,api_key,repositoryandoidc_audienceare optional and fall back toSTACKORDER_SERVER_URL,STACKORDER_API_KEY,STACKORDER_REPOSITORY(thenGITHUB_REPOSITORY) andSTACKORDER_OIDC_AUDIENCE, which the Stackorder CLI 0.4.0 sets for the tool.STACKORDER_RUN_IDandSTACKORDER_STACK. Elsewhere, an API key. Where an ID token can be requested, a configured API key is ignored, so an admin key never replaces the job's own identity. Retries only on network errors and 5xx, with a fresh token each attempt.github.com/stackorder/stackorder/api/v1at v0.4.0; a golden fixture in that module pins the format.go tool tfplugindocs, checked in CI), golangci-lint, acceptance tests against an httptest fake on a matrix of Terraform 1.10.x and latest and OpenTofu 1.11.x and latest, GoReleaser archives with_SHA256SUMS, a GPG detached signature andterraform-registry-manifest.json(protocol 6.0) onv*tags.Before v0.1.0
GPG_PRIVATE_KEYandPASSPHRASEfor the release signing step.## [0.1.0]section inCHANGELOG.md; the release workflow takes the release notes from it.Verification
GOWORK=off:go build,go vet,go test,make lint(0 issues),make docs-check(regenerated docs unchanged),go mod tidyandgo mod verifyclean.TF_ACC=1) pass on Terraform 1.14.4 and OpenTofu 1.12.6 locally; earlier commits were also run on Terraform 1.10.5 and 1.16.5 and OpenTofu 1.11.14 and 1.13.1.govulncheck@v1.8.0under Go 1.27.2 (today's stable, which the release workflow installs): no vulnerabilities found, after moving grpc to v1.83.2 and x/net to v0.60.0.🤖 Generated with Claude Code