Skip to content

feat: a listed resource declares the identity its list results carry - #100

Merged
ShocOne merged 1 commit into
mainfrom
feat/resource-identity-schema
Aug 14, 2026
Merged

feat: a listed resource declares the identity its list results carry#100
ShocOne merged 1 commit into
mainfrom
feat/resource-identity-schema

Conversation

@ShocOne

@ShocOne ShocOne commented Aug 14, 2026

Copy link
Copy Markdown
Member

Phase 2. #99 made list resources valid at schema-load time; this makes them work
when terraform actually calls them.

The defect

list/list_resource.go:135 NewListResult builds every result against two
schemas it reads off the request:

identity := &tfsdk.ResourceIdentity{
    Raw:    tftypes.NewValue(r.ResourceIdentitySchema.Type().TerraformType(ctx), nil),
    ...
resource := &tfsdk.Resource{
    Raw:    tftypes.NewValue(r.ResourceSchema.Type().TerraformType(ctx), nil),

The server sources the identity from resource.ResourceWithIdentity and returns
nil when the resource does not implement it (fwserver/server.go:807, "It's
valid for a resource to not have an identity"). No resource declared one, so
every list resource would panic on its first result.

It was invisible because the generated test fabricated its own identity schema
and passed no resource schema at all. One entity proved it — the only list
resource whose SDK call currently succeeds:

panic: runtime error: invalid memory address or nil pointer dereference
  terraform-plugin-framework@v1.19.0/list/list_resource.go:141
  .../organizations_settings_billing_budget/list.go:34

What changed

A resource the API can enumerate declares an identity: its addressing
attributes, then its id, all RequiredForImport.

func (r *OrganizationsSettingsBillingBudgetResource) IdentitySchema(...) {
    resp.IdentitySchema = identityschema.Schema{
        Attributes: map[string]identityschema.Attribute{
            "org": identityschema.StringAttribute{RequiredForImport: true},
            "id":  identityschema.StringAttribute{RequiredForImport: true},
        },
    }
}

Addressing is part of it because the framework requires an identity to name "at
most one remote object per provider" — an id alone does not where a parent
scopes it, since two repositories' hooks may share one.

The id is a string in the identity whatever the API keys its objects with.
An identity names an object and is compared for equality; it is not the state
value, and findIdentityNode already renders every scalar as a string.

Both halves render from one derivation (resourceIdentity), so the identity
a list streams and the identity its resource declares cannot drift:

type identityModel struct {
    Org types.String `tfsdk:"org"`
    ID  types.String `tfsdk:"id"`
}

The id comes from the element; every other attribute is addressing, which the
list block's configuration already supplied to make the call. A list block that
cannot supply one the identity names refuses, rather than streaming a partial
identity.

The generated test stands the resource up and reads both schemas off it,
instead of fabricating one — so it now exercises what terraform does.

Only listed resources declare an identity. Identity affects state format and
enables import-by-identity; widening it to every resource is a separate,
deliberate call.

Measured effect

Generated list-resource test failures, before → after:

pilot before after
github 23 20
jamfpro 39 5
thousandeyes 11 3

The panic is gone and organizations_settings_billing_budget passes end to end
— an identity round-trips through a real List call for the first time.

What remains is almost entirely Phase 3, the fixture defect:

parsing time "tfpfgen-test-created-at" as "2006-01-02T15:04:05Z07:00"

Refusals: github 834 → 835 (+1, a list block that cannot supply an identity
attribute). jamfpro and thousandeyes unchanged. provider verify — no drift on
all three (4384 / 3895 / 1798 files).

Gates

  • gofmt, repo_hygiene_gate.sh, golangci-lint (0 issues), go build/go vet — pass
  • go test ./... — pass
  • provider generate + provider verify on all three — no drift

🤖 Generated with Claude Code

NewListResult builds every result against the resource's identity and
resource schemas, so a list resource whose resource declares no identity
panics the moment terraform calls it. No resource declared one.

A resource the API can enumerate now does: its addressing attributes and
its id, all required for import. Addressing is part of it because the
framework requires an identity to name at most one remote object per
provider, and an id alone does not where a parent scopes it — two
repositories' hooks may share one.

The id is a string in the identity whatever the API keys its objects
with. An identity names an object and is compared for equality; it is
not the state value, and the accessor a list reaches its id through
already renders every scalar as a string.

Both halves render from one derivation, so the identity a list streams
and the identity the resource declares cannot drift. The generated test
stands the resource up and reads both schemas off it rather than
fabricating one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ShocOne
ShocOne merged commit 804ec15 into main Aug 14, 2026
2 checks passed
@ShocOne
ShocOne deleted the feat/resource-identity-schema branch August 14, 2026 19:48
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