From 2dec7b0de78110c0e1f3cfe80709e7796bf84d9c Mon Sep 17 00:00:00 2001 From: joshsiv-mulga Date: Thu, 8 Oct 2026 11:53:52 +1000 Subject: [PATCH] docs: record EKS access entry lifecycle contract --- docs/PACKAGE_BOUNDARY_MIGRATION.md | 1 + .../eks-access-entry-lifecycle.md | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 docs/package-boundary/eks-access-entry-lifecycle.md diff --git a/docs/PACKAGE_BOUNDARY_MIGRATION.md b/docs/PACKAGE_BOUNDARY_MIGRATION.md index c1e21f55f..aa58679ac 100644 --- a/docs/PACKAGE_BOUNDARY_MIGRATION.md +++ b/docs/PACKAGE_BOUNDARY_MIGRATION.md @@ -292,6 +292,7 @@ while the branch is in flight; they do not describe the umbrella repository's | `17dc29251` | `spinifex/handlers/rds` (`DBSubnetGroupRecord`, `DBSubnetGroupSubnet`, `DBSubnetGroupKey`, `DBSubnetGroupsPrefix`, `getDBSubnetGroup`, `resolveGroupSubnets` and the create/modify/delete bodies) | `spinifex/domains/rds/subnetgroup` (`Owner`, `Record`, `Subnet`, `Key`, `Prefix`) | First ADR-0004 resource owner. The handler keeps AWS validation, projection and paging; delete reaches instances through the owner-declared `Dependants`, and instance placement reaches the group through the instance-declared `subnetGroupPlacement`. Stored shape unchanged; `docs/service-interfaces.yaml` lists the package with the RDS daemon. No conformance claimed; gaps in the lifecycle contract. | | `1cb868e68` | — | — | ECS task definition characterization before extraction (ADR-0003 S1, ADR-0004): stored keys and field names, every reference form and its not-found fault, repeatable deregister, an inactive revision still launching, bare-family launch resolution, a service pinning its resolved revision. Present contract and gaps, including the bare-family `iam:PassRole` re-resolution security gap, in `docs/package-boundary/ecs-task-definition-lifecycle.md`; no conformance claimed. | | `544b0c6a0` | `spinifex/handlers/ecs` (`TaskDefRecord`, `ContainerDef`, `TaskDefRevKey`, `TaskDefLatestRevKey`, `ParseTaskDefRef`, `TaskDefARN`, `TaskDefRefARN`, revision register/deregister/resolve/list) | `spinifex/domains/ecs/taskdefinition` (`Owner`, `Record`, `Container`, `PortMapping`, `SystemControl`, `ParseRef`, `ARN`, `RefARN`, keys) | ADR-0004 owner extraction (ADR-0003 S1 contract in `docs/package-boundary/ecs-task-definition-lifecycle.md`). Stored keys and JSON field names unchanged; the owner declares its own persisted `PortMapping`/`SystemControl` so it does not import `handlers/ecs/bus`. Launches use the launch-side `taskDefinitionResolver`; gateway authz and all `iam:PassRole` checks unchanged. `docs/service-interfaces.yaml` now lists `spinifex/handlers/ecs` and the new package under `spinifex_daemon`. Proves ownership and dependency direction only; no conformance claimed. | +| `b36d70b22` | — | — | EKS access entry characterization before extraction (ADR-0003 S1, ADR-0004): persisted and nested field names, pre-existing JSON decodes, actions ignore cluster status, associate without an entry is not found, disassociating an unbound policy is a no-op, an empty update keeps username and groups, cluster delete sweeps only that cluster's entries, token review needs no cluster record. Present contract and gaps in `docs/package-boundary/eks-access-entry-lifecycle.md`; no conformance claimed. | The EC2 contract row is intentionally different from the directory moves: it creates a compatibility boundary. `ec2.cmd.*` retains its deployed one-token NATS diff --git a/docs/package-boundary/eks-access-entry-lifecycle.md b/docs/package-boundary/eks-access-entry-lifecycle.md new file mode 100644 index 000000000..de902436a --- /dev/null +++ b/docs/package-boundary/eks-access-entry-lifecycle.md @@ -0,0 +1,39 @@ +# EKS access entry: lifecycle contract + +Present contract under ADR-0003 S1, recorded before the resource moves. +It describes the code as it stands, so it claims no conformance; the gaps below are observed behaviour, not targets. +Paths are relative to `spinifex/` unless they start with `tests/`. + +## Scope + +An access entry has no realization of its own. +Create, update, delete and policy association complete synchronously within the request as writes to the account's EKS bucket; the token webhook reads the record when it authenticates a `get-token` bearer. + +## Contract + +| Item | Present behaviour | Evidence | +|---|---|---| +| Identity | Cluster name plus principal ARN. Key and ARN suffix use `sha256hex(principalARN)` (`handlers/eks/store.go` `PrincipalARNHash`). ARN `arn:aws:eks:{region}:{account}:access-entry/{cluster}/{hash}` (`foundation/aws/arn/eks.go`); the AWS ARN shape differs and is not verified. | `handlers/eks/service_impl_test.go` `TestAccessEntry_CreateDescribeListDelete`, `handlers/eks/store_test.go`, `gateway/eks/authz_test.go` `TestResourceARNsAccessEntryMatchesHandler` | +| Scope | Account and Region, within one cluster: keys live under `clusters/{cluster}/access-entries/` in the `eks-account-{account}` bucket. | `handlers/eks/access_lifecycle_test.go` `TestAccessEntryPersistedLayout_TopLevelAndNestedFieldNames`, `TestAccessEntryRecord_DecodesHandWrittenJSON`, `handlers/eks/create_cluster_test.go` `TestCreateCluster_SeedsCreatorAdminAccessEntry` | +| Durable owner and record | `handlers/eks/access_entry.go` writes `AccessEntryRecord` with nested `AssociatedAccessPolicy` and `AccessScope` (`handlers/eks/types.go`). The cluster launch also writes the creator-admin entry, and `DeleteClusterPrefix` (`handlers/eks/cluster_state.go`) erases entries with the cluster. The gateway process reads records directly through `ResolveTokenReview` (`handlers/eks/token_review.go`). | `handlers/eks/access_lifecycle_test.go` `TestAccessEntryPersistedLayout_TopLevelAndNestedFieldNames`, `TestAccessEntryRecord_DecodesHandWrittenJSON`, `handlers/eks/create_cluster_test.go` `TestCreateCluster_SeedsCreatorAdminAccessEntry` | +| Desired state | Principal, Kubernetes username and groups, type (`STANDARD` only), tags (set at create only) and cluster-scoped access policy associations. | `handlers/eks/service_impl_test.go` `TestAccessEntry_RejectsNodeType`, `TestAccessPolicy_AssociateRejectsUnsupportedPolicyAndScope`, `TestAccessPolicy_AssociateRejectsNamespaceScope`, `handlers/eks/access_lifecycle_test.go` `TestAccessEntry_UpdateWithNoFieldsKeepsUsernameAndGroups` | +| Generation | None. Update, associate and disassociate retry against the KV revision; create and delete do not. | | +| Observed state | Not tracked; the record is the only state. | | +| Readiness | Usable by token review as soon as the write returns. | `handlers/eks/token_review_test.go` `TestResolveTokenReview_AuthenticatesViaVerifyAndKV`, `TestResolveTokenReview_NoAccessEntryDenies`, `handlers/eks/service_impl_test.go` `TestAccessPolicy_DisassociateRemovesProjectedGroup` | +| Idempotency | `clientRequestToken` is ignored. Update always advances `modifiedAt`, even when nothing changes. A duplicate create returns `ResourceInUseException`; a repeated delete returns `ResourceNotFoundException`; disassociating an unbound policy succeeds without change. Internal NATS redelivery of create returns `ResourceInUseException` to the redelivered request. | `handlers/eks/service_impl_test.go` `TestAccessEntry_CreateDescribeListDelete`, `TestAccessEntry_DescribeDeleteMissingIsNotFound`, `handlers/eks/access_lifecycle_test.go` `TestAccessPolicy_DisassociateUnassociatedLeavesRecordUnchanged`, `TestAccessPolicy_AssociateWithNoEntryIsNotFound` | +| Dependencies | Every action requires only that the cluster's meta record exists (`acctKVForCluster`); cluster status is not read, so actions succeed on a `CREATING`, `FAILED` or `DELETING` cluster. Token review does not check the cluster at all. The principal is not checked against IAM. | `handlers/eks/service_impl_test.go` `TestAccessEntry_UnknownClusterIsNotFound`, `handlers/eks/access_lifecycle_test.go` `TestAccessEntryActionsIgnoreClusterLifecycleStatus`, `TestResolveTokenReview_AuthenticatesWithoutClusterMeta` | +| Authorization | Gateway authz derives the access-entry ARN with `PrincipalARNHash` (`gateway/eks/authz.go`). No `iam:PassRole`. The STS step of token review is the existing `eks.VerifyToken` NATS contract served by awsgw. | `gateway/eks/authz_test.go` `TestResourceARNsAccessEntryMatchesHandler` | +| Deletion ordering | Delete is get-then-delete, not conditional. Cluster delete erases entries in the prefix purge only after all infrastructure teardown succeeds; on failure they survive while the cluster stays `DELETING`. | `handlers/eks/access_lifecycle_test.go` `TestDeleteClusterPrefix_SweepsAccessEntriesScopedToCluster` | +| Restart and interruption | The prefix purge deletes keys in listing order; an interruption after the meta key is gone leaves entries that a new cluster of the same name inherits. Reclaiming a `FAILED` cluster and a failed launch purge with the meta kept, so the recreated cluster inherits the failed attempt's entries. | none | +| End-to-end | `tests/e2e/eks/eks_test.go` `AccessEntry` and `GetToken`; `tests/integration/cross_tenant_test.go`. No Terraform apply/destroy evidence for `aws_eks_access_entry` or `aws_eks_access_policy_association`. | | + +## Gaps + +1. Create is get-then-put with no exclusive create, so concurrent creates for one principal both succeed and the later write wins. +2. The creator-admin seed at cluster launch is an unconditional put after the cluster meta is final, so it replaces an entry for the same principal created while the cluster was `CREATING`, including its policy associations. +3. Access actions do not check cluster status (see dependencies row), and opening the bucket creates it for an account that has none. +4. Entries can outlive their cluster (interrupted prefix purge) or carry over to a recreated cluster (reclaim and failed launch keep the meta and do not purge entries separately). +5. Token review authenticates against an entry without checking the cluster exists or its status. +6. `ResolveTokenReview` opens the EKS bucket and reads the record in the gateway process, outside the owner. +7. List ignores `maxResults` and `nextToken`; tagging an access-entry ARN returns `NotImplemented`. +8. No ADR-0003 S5 failure or recovery evidence.