diff --git a/docs/PACKAGE_BOUNDARY_MIGRATION.md b/docs/PACKAGE_BOUNDARY_MIGRATION.md index 93b90f044..faaeeb182 100644 --- a/docs/PACKAGE_BOUNDARY_MIGRATION.md +++ b/docs/PACKAGE_BOUNDARY_MIGRATION.md @@ -338,7 +338,7 @@ while the branch is in flight; they do not describe the umbrella repository's | `7bf6c10d7` | `spinifex/vpcd` | `spinifex/runtime/roles/vpcd` | The whole vpcd role moves unchanged to its ADR-0001 runtime-role target: all sixteen production and test files (`vpcd.go`, the lease reaper, the OVN controller watchdog, the host-EIP loop, lease datapath reconcile and their tests), with no split and no lifecycle-behaviour change. The package name stays `vpcd`. `operator/cli/service.go` and `runtime/service/{service,service_test}.go` update their import path. `docs/service-interfaces.yaml`'s `vpcd` service entry, `.github/CODEOWNERS` and the real-manifest case in `tests/e2e/select-suites/selector_test.go` follow the new path; that package's own synthetic `testManifest()` fixture keeps its illustrative `spinifex/vpcd` string by design, decoupled from the real manifest. The network invariant scan roots in `domains/network/invariants/{nat_mode,nats_subjects,names}_test.go` and the S3 exemption for `vpcd.go` follow the new path; injected `UplinkMode()` and `vpc-`-prefixed concat faults in moved files failed S3 and S4 respectively, confirming the scan still covers them, and were reverted by hand. `ingress/aws/invariants/deps_test.go` keeps `vpcd` in its `forbiddenRoots` as defence in depth: `runtime` already rejects an import of the new path, confirmed by an injected import of `runtime/roles/vpcd` into an `ingress/aws` file, and the retained entry still rejects an ingress import of a recreated legacy `vpcd` directly. The layering package drops `vpcd` from `legacyRoots`, `runtimeLegacy` and `frozenLegacyPackages` and adds it to `retiredLegacyRoots`, so `runtime/roles/vpcd` classifies as runtime automatically and a revived `spinifex/vpcd` directory fails the frozen-root guard, both confirmed by fault injection and reverted; the stale `operator/cli` → `vpcd` allowlist entry is deleted rather than repointed, because operator already imports other `runtime/roles/*` packages with no allowlist entry and no rule forbids it. The instance-state ratchet's allowlist cites no vpcd path and is unchanged. | | `a130f6b8e` | `spinifex/handlers/ecs/bus` | `contracts/ecs/v1` (`ecsv1`) | The ECS scheduler-agent wire vocabulary moves with no legacy shim: five subject builders and the message types (`RegisterInstance`, `Heartbeat`, `Assign`, `AssignContainer`, `StopDirective`, `TaskState`, `ContainerStatus`, `InstanceCapacity`, `PortMapping`, `SystemControl`, status constants). Subject strings, JSON fields and types are byte-identical; the old-agent `AssignContainer` compatibility test moves unchanged and new golden pins cover every message type that had none. A trace found only `service-reconcile` live on the bus (`handlers/ecs/instances.go` publishes, the scheduler leader subscribes); the agent reuses `Assign`, `StopDirective` and `TaskState` as payloads of the gateway's `PollAssignments` and `SubmitTaskStateChange` actions, and the README route table and type comments say so. `docs/service-interfaces.yaml` lists `ecs.bus.*` and the contract path under the daemon. The layering package drops the stale `agents/ecs/agent` → `handlers/ecs/bus` allowlist entry and the frozen-package entry. | | `263c58167` | `spinifex/admin` (`images_promote.go`: `SystemOwnerAlias`, `PromoteImageOpts`/`PromoteImageResult`, `PromoteSystemImage`, `GetAMIMetadata`, the private `readPromotedSnapshot`, and its test fixtures) | `spinifex/domains/ec2/image` | The cohesive image-promotion API moves to its existing domain owner with only the package line changed: no storage, key, ordering, error or promotion-behaviour change, and no name collisions. `daemon/daemon_handlers_image.go` and `gateway/spx/promote_image.go` drop their `admin` import and use `ec2image`; `operator/cli/admin.go` adds `ec2image` and keeps `legacyadmin` for the account identifiers and key generators. `admin` now holds only `accounts.go`. No new forbidden edge and no stale allowlist entry. | - +| (uncommitted; hash added at commit) | `spinifex/admin` (`DefaultAccountID`, `DefaultAccountName`) | `spinifex/foundation/aws/identifiers` | The fixed, platform-reserved bootstrap account identifiers join `GlobalAccountID`: the ID and name of the first human-facing account created during bootstrap, used by `daemon`, `gateway`, `runtime/roles/awsgw`, `accountteardown` and `operator/cli`. Function names and values are unchanged, and they stay fixed rather than configurable. Callers use the existing `awsidentifiers` alias; `daemon`, `gateway`, `runtime/roles/awsgw`, `accountteardown` and the other `operator/cli` files drop their `admin` import, and `operator/cli/admin.go` keeps `legacyadmin` only for the credential generators. The `TestDefaultAccountID` assertion moves with the function, plus a name pin and a check that the ID is valid and distinct from `GlobalAccountID`. `admin` now holds only the IAM credential generators. | The EC2 contract row is intentionally different from the directory moves: it creates a compatibility boundary. `ec2.cmd.*` retains its deployed one-token NATS subscription and permission shape, while callers use @@ -413,7 +413,7 @@ These residuals were recorded by individual slices and must be resolved once the - `handlers/iam/service_impl.go:30` imports `admin` for `GenerateAWSSecretKey` (line 641) only, a domain-to-operator edge; the `admin.AccountID`, `admin.AccountName` and `admin.EncryptedSecret` uses are fields of a local `*AdminBootstrapData` parameter, not the package. It clears with the remaining `admin` entry, once the key generators have an IAM-credentials owner. - `domains/ec2/instance/state/imds_readers.go` (dependency, IMDS readers) imports `daemon` for `LocalState`, `ReadLocalState`, `LocalStatePath`, `NewJetStreamManager` and `InstanceStateBucket`. The local-file reader and the instance-record loader's `JetStreamManager` open, which `vpcd/imds_instance_state.go` held directly, moved here unchanged and are now injected into `vpcd` through `guestmetadata.LocalStateReader` and `guestmetadata.RecordLoader`. The readers already live in `domains/ec2/instance/state`. Removal condition: the legacy dependencies they call are extracted from `daemon` — `LocalStatePath`, `ReadLocalState` and `LocalState`, the instance bucket constants, and `JetStreamManager` with its bucket initialisation (`InitKVBucket`) — leaving `domains/ec2/instance/state` with no `daemon` import. - Advisory: 27 production files across `domains/{acm,dns,ec2/*,network/host,network/ovn,ochre}` import `bootstrap/config` directly rather than through typed constructor inputs, which ADR-0001's `config` row expects. `bootstrap/preflight/preflight.go:15` imports `runtime/host/command` and `services.go:13` imports `operator/host/systemd`; both are expected bootstrap-to-runtime/operator edges for startup validation and are not domain-rule violations. -- `admin` (remaining): the operator-only code moved to `operator/admin`. What remains has non-operator consumers and awaits owners: `DefaultAccountID`/`DefaultAccountName` (IAM/account bootstrap), `GenerateAWSAccessKey`/`GenerateAWSSecretKey` (IAM credentials) and `PromoteSystemImage` (the existing `domains/ec2/image`). +- `admin` (remaining): the operator-only code moved to `operator/admin`. Image promotion moved to `domains/ec2/image` and the bootstrap account identifiers to `foundation/aws/identifiers`. What remains is `GenerateAWSAccessKey`/`GenerateAWSSecretKey` (consumed by `handlers/iam` and `operator/cli`), deferred behind the IAM credential boundary. - `domains/dns`: imports `runtime/compute/vm` (domain-to-runtime), `domains/network/reconcile` for the reconcile lock and `bootstrap/config` for node configuration. - ACM: the IAM AES-GCM helper import (ACM relocation residual). - `runtime/roles/awsgw`: the quota reconcile loop belongs to `domains/admission/quota`, and the EKS token-verify responder belongs with STS or EKS identity. @@ -666,7 +666,7 @@ series above; "needs decision" is open. | `utils` | — | Done and deleted. `ec2_subject.go` went to `contracts/ec2/v1`, the vpcd protocol to `contracts/network/v1` with the typed client in `domains/network/projection`, and the master-key loader to `domains/ec2/ebs/encryption`. | | `vpcd` | — | Done. The IMDS local-file reader and record-space opener moved to `domains/ec2/instance/state` and are injected through `guestmetadata.LocalStateReader`/`RecordLoader` (recorded above), so `vpcd` no longer imports `daemon` directly; its inter-process boundary now exists (`contracts/network/v1`, with `domains/network/subscribers` as the consumer and `domains/network/projection` as the producer client). The package moved unchanged to `runtime/roles/vpcd` (recorded below), and the legacy directory is removed. | | `daemon` | Reserved as `runtime/daemon`'s home only; not split or moved. | Deferred: Q-28's write-side consolidation is a prerequisite ADR-0001 names explicitly. | -| `admin` | `DefaultAccountID`/`DefaultAccountName` (consumed by `daemon`, `gateway`, `runtime/roles/awsgw`, `operator/cli`), `GenerateAWSAccessKey`/`GenerateAWSSecretKey` (`handlers/iam`, `operator/cli`). The operator use cases moved to `operator/admin` and image promotion to `domains/ec2/image`. | Deferred to their owners: IAM/account bootstrap and IAM credentials. | +| `admin` | `GenerateAWSAccessKey`/`GenerateAWSSecretKey` (`handlers/iam`, `operator/cli`). The operator use cases moved to `operator/admin`, image promotion to `domains/ec2/image`, and the bootstrap account identifiers to `foundation/aws/identifiers`. | Deferred to the IAM credential owner. | | `accountteardown` | Direct calls into `domains/ec2/awsapi/*` action adapters (recorded above). | Deferred: the ADR's `accountteardown` row replaces this with domain-owned cleanup capabilities. | | `lbagent` | The whole package; `handlers/elbv2` imports it as a domain-to-agent implementation edge (recorded above). | Deferred: Q-30 reserves `agents/elbv2` as its destination but forbids moving it as an isolated rename ahead of the common-agent design. | | `migrate` | Nothing at the old path. | Split, not an exact ADR row: ADR-0001 maps `migrate` to `foundation/state/migrate`, but the package held two responsibilities. The KV migration engine and its tests are in `foundation/state/migrate` (`8cd9cff75`); the config-file and object-store upgrade workflow moved to `operator/upgrade` (`919d5bfa6`) by owner decision. Needs an ADR-0001 map clarification before this row can be claimed as target conformance. | @@ -680,8 +680,8 @@ ADR-0001 completion is structural, so none of these is cleared by remediating se | Class | Paths | Status | |---|---|---| | Explicitly deferred by ADR-0001 | `daemon` (Q-28), `lbagent` (Q-30) | Closed for this refactor; the ADR names the prerequisite. | -| No missing boundary (candidate moves) | `handlers/ecs/bus` → `contracts/ecs/v1` (`a130f6b8e`); `admin/images_promote.go` → `domains/ec2/image` (`263c58167`) | Done. | -| One narrow missing boundary | `admin` account identifiers (owner decision); `handlers/sts` (an EKS projection or contract for `AccountBucketName`/`OIDCJWKSKey`, plus IAM reads); `gateway/tagging` (per-domain tagging capabilities, owner decided); `gateway/spx` (owner of operator-plane dispatch registration) | Each needs one decision or one capability, then a mechanical move. | +| No missing boundary (candidate moves) | `handlers/ecs/bus` → `contracts/ecs/v1` (`a130f6b8e`); `admin/images_promote.go` → `domains/ec2/image` (`263c58167`); `admin` bootstrap account identifiers → `foundation/aws/identifiers` (uncommitted) | Done. | +| One narrow missing boundary | `handlers/sts` (an EKS projection or contract for `AccountBucketName`/`OIDCJWKSKey`, plus IAM reads); `gateway/tagging` (per-domain tagging capabilities, owner decided); `gateway/spx` (owner of operator-plane dispatch registration) | Each needs one decision or one capability, then a mechanical move. | | Broader boundary or follows another package | `handlers/iam` and `handlers/iam/mock` (per-consumer IAM capabilities); `handlers/{ecs,eks,rds}` (ADR-0004 blocked-dependency lists); `handlers/elbv2` (data-plane boundary evidence); `accountteardown` (domain cleanup capabilities); `gateway` root (ADR-0002 auth and request-context phase); `gateway/{iam,sts,ecs,eks,rds,elbv2}` (each follows its `handlers/`, because a new-path `awsapi` cannot import a legacy handler); `gateway/bedrock` (the recorded Ochre split plan) | Recorded debt with removal conditions above. | Per-consumer IAM capabilities are the widest unblocker: they gate the `admin` key generators, `handlers/sts`, Ochre's IAM reads, the ELBv2 role path and the ECS, EKS and RDS ensurers. diff --git a/spinifex/accountteardown/engine_test.go b/spinifex/accountteardown/engine_test.go index 22387c9e3..9dd313e2e 100644 --- a/spinifex/accountteardown/engine_test.go +++ b/spinifex/accountteardown/engine_test.go @@ -13,7 +13,6 @@ import ( "testing" "time" - "github.com/mulgadc/spinifex/spinifex/admin" handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam" ) @@ -126,7 +125,7 @@ func request() Request { // system treats as undeletable. Two hard-coded lists that drift is exactly the // failure this guards against. func TestProtectedAccountsMatchTheRestOfTheSystem(t *testing.T) { - for _, id := range []string{awsidentifiers.GlobalAccountID, admin.DefaultAccountID()} { + for _, id := range []string{awsidentifiers.GlobalAccountID, awsidentifiers.DefaultAccountID()} { if _, protected := protectedAccountIDs[id]; !protected { t.Fatalf("account %s is not protected by teardown", id) } @@ -144,7 +143,7 @@ func TestTeardownRefusesTheSuperAdminAccount(t *testing.T) { engine := quietEngine(accounts) _, err := engine.Teardown(context.Background(), Request{ - AccountID: admin.DefaultAccountID(), AccountName: "spinifex", + AccountID: awsidentifiers.DefaultAccountID(), AccountName: "spinifex", }) if !errors.Is(err, ErrProtectedAccount) { t.Fatalf("expected ErrProtectedAccount, got %v", err) diff --git a/spinifex/accountteardown/types.go b/spinifex/accountteardown/types.go index a58668f26..dd38e6a03 100644 --- a/spinifex/accountteardown/types.go +++ b/spinifex/accountteardown/types.go @@ -197,7 +197,7 @@ var ErrProtectedAccount = errors.New("account is protected and cannot be deleted var ErrResourcesStuck = errors.New("teardown left resources behind") // protectedAccountIDs are the two accounts no teardown may ever remove. They -// mirror identifiers.GlobalAccountID and admin.DefaultAccountID; a test asserts the +// mirror identifiers.GlobalAccountID and awsidentifiers.DefaultAccountID; a test asserts the // two lists agree so a change to either is caught rather than diverging. var protectedAccountIDs = map[string]string{ "000000000000": "system", diff --git a/spinifex/admin/accounts.go b/spinifex/admin/accounts.go index d6f6a47ce..89f074457 100644 --- a/spinifex/admin/accounts.go +++ b/spinifex/admin/accounts.go @@ -1,6 +1,5 @@ -// Package admin holds what non-operator code still needs from the former admin -// package: default account identifiers and AWS credential generators, -// awaiting IAM owners. +// Package admin holds the AWS credential generators non-operator code still +// needs from the former admin package, awaiting an IAM credential owner. package admin import ( @@ -10,17 +9,6 @@ import ( "math/big" ) -// DefaultAccountID returns the default admin account ID (000000000001). -// This is the first human-facing account created during bootstrap. -func DefaultAccountID() string { - return "000000000001" -} - -// DefaultAccountName returns the default admin account name ("spinifex"). -func DefaultAccountName() string { - return "spinifex" -} - // GenerateAWSAccessKey generates an AWS-style access key (AKIA + 16 random alphanumeric chars). func GenerateAWSAccessKey() (string, error) { const prefix = "AKIA" diff --git a/spinifex/admin/admin_test.go b/spinifex/admin/admin_test.go index dbf8297a7..12727096a 100644 --- a/spinifex/admin/admin_test.go +++ b/spinifex/admin/admin_test.go @@ -44,9 +44,3 @@ func TestGenerateAWSSecretKey_Uniqueness(t *testing.T) { assert.NoError(t, err) assert.NotEqual(t, k1, k2) } - -func TestDefaultAccountID(t *testing.T) { - id := DefaultAccountID() - assert.Equal(t, "000000000001", id) - assert.Len(t, id, 12) -} diff --git a/spinifex/daemon/daemon.go b/spinifex/daemon/daemon.go index 63fd077ec..9e1c861d6 100644 --- a/spinifex/daemon/daemon.go +++ b/spinifex/daemon/daemon.go @@ -41,7 +41,6 @@ import ( "github.com/mulgadc/spinifex/contracts/ec2/v1" operatorv1 "github.com/mulgadc/spinifex/contracts/operator/v1" viperblocklegacyv1 "github.com/mulgadc/spinifex/contracts/viperblockd/legacy/v1" - "github.com/mulgadc/spinifex/spinifex/admin" "github.com/mulgadc/spinifex/spinifex/bootstrap/config" "github.com/mulgadc/spinifex/spinifex/bootstrap/preflight" acmdomain "github.com/mulgadc/spinifex/spinifex/domains/acm" @@ -2213,11 +2212,11 @@ func (d *Daemon) startCluster() error { // (matches AWS: every account has a default VPC with IGW + default SG) if d.vpcService != nil { failedDefaultVPCs := map[string]struct{}{} - for _, accountID := range []string{awsidentifiers.GlobalAccountID, admin.DefaultAccountID()} { + for _, accountID := range []string{awsidentifiers.GlobalAccountID, awsidentifiers.DefaultAccountID()} { // Pass bootstrap IDs for the admin account so EnsureDefaultVPC uses // the same IDs that admin init wrote to [bootstrap] in spinifex.toml. var opts []ec2vpc.BootstrapIDs - if accountID == admin.DefaultAccountID() && d.clusterConfig != nil && d.clusterConfig.Bootstrap.VpcId != "" { + if accountID == awsidentifiers.DefaultAccountID() && d.clusterConfig != nil && d.clusterConfig.Bootstrap.VpcId != "" { opts = append(opts, ec2vpc.BootstrapIDs{ VpcId: d.clusterConfig.Bootstrap.VpcId, SubnetId: d.clusterConfig.Bootstrap.SubnetId, diff --git a/spinifex/daemon/daemon_handlers_vpc.go b/spinifex/daemon/daemon_handlers_vpc.go index 0873f2a66..7ee254e43 100644 --- a/spinifex/daemon/daemon_handlers_vpc.go +++ b/spinifex/daemon/daemon_handlers_vpc.go @@ -9,7 +9,6 @@ import ( "time" "github.com/aws/aws-sdk-go/service/ec2" - "github.com/mulgadc/spinifex/spinifex/admin" "github.com/nats-io/nats.go" ) @@ -108,7 +107,7 @@ func (d *Daemon) handleEnsureDefaultVpc(msg *nats.Msg) string { // in skipAccounts are not touched — used to avoid attaching infrastructure to // a half-built VPC when EnsureDefaultVPC failed earlier in startup. func (d *Daemon) ensureDefaultVPCInfrastructure(skipAccounts map[string]struct{}) { - for _, accountID := range []string{awsidentifiers.GlobalAccountID, admin.DefaultAccountID()} { + for _, accountID := range []string{awsidentifiers.GlobalAccountID, awsidentifiers.DefaultAccountID()} { if _, skip := skipAccounts[accountID]; skip { continue } diff --git a/spinifex/daemon/daemon_handlers_vpc_test.go b/spinifex/daemon/daemon_handlers_vpc_test.go index d7b2db515..96b5922db 100644 --- a/spinifex/daemon/daemon_handlers_vpc_test.go +++ b/spinifex/daemon/daemon_handlers_vpc_test.go @@ -10,7 +10,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/mulgadc/spinifex/internal/testkit" - "github.com/mulgadc/spinifex/spinifex/admin" ec2igw "github.com/mulgadc/spinifex/spinifex/domains/ec2/igw" ec2routetable "github.com/mulgadc/spinifex/spinifex/domains/ec2/routetable" ec2vpc "github.com/mulgadc/spinifex/spinifex/domains/ec2/vpc" @@ -261,7 +260,7 @@ func TestHandleEnsureDefaultVpc_ErrorEnvelopes(t *testing.T) { func TestEnsureDefaultVPCInfrastructure_SkipsHalfBuiltAccounts(t *testing.T) { daemon, _ := createDefaultVPCTestDaemon(t) - adminAccount := admin.DefaultAccountID() + adminAccount := awsidentifiers.DefaultAccountID() for _, accountID := range []string{awsidentifiers.GlobalAccountID, adminAccount} { _, err := daemon.vpcService.EnsureDefaultVPC(accountID) require.NoError(t, err) diff --git a/spinifex/foundation/aws/identifiers/account.go b/spinifex/foundation/aws/identifiers/account.go index 63a130af5..d836a4165 100644 --- a/spinifex/foundation/aws/identifiers/account.go +++ b/spinifex/foundation/aws/identifiers/account.go @@ -3,6 +3,17 @@ package identifiers // GlobalAccountID is the root/system account ID. const GlobalAccountID = "000000000000" +// DefaultAccountID returns the fixed, platform-reserved ID of the first +// human-facing account created during bootstrap (000000000001). +func DefaultAccountID() string { + return "000000000001" +} + +// DefaultAccountName returns the fixed name of the bootstrap account ("spinifex"). +func DefaultAccountName() string { + return "spinifex" +} + // IsAccountID checks if a string is a valid 12-digit AWS account ID. func IsAccountID(s string) bool { if len(s) != 12 { diff --git a/spinifex/foundation/aws/identifiers/account_test.go b/spinifex/foundation/aws/identifiers/account_test.go index bcb3fb07f..f5b0f88dd 100644 --- a/spinifex/foundation/aws/identifiers/account_test.go +++ b/spinifex/foundation/aws/identifiers/account_test.go @@ -36,3 +36,22 @@ func TestGlobalAccountID(t *testing.T) { t.Error("GlobalAccountID should be a valid account ID") } } + +func TestDefaultAccountID(t *testing.T) { + id := DefaultAccountID() + if id != "000000000001" { + t.Errorf("DefaultAccountID() = %q, want %q", id, "000000000001") + } + if !IsAccountID(id) { + t.Errorf("DefaultAccountID() = %q is not a valid account ID", id) + } + if id == GlobalAccountID { + t.Error("DefaultAccountID() must differ from GlobalAccountID") + } +} + +func TestDefaultAccountName(t *testing.T) { + if got := DefaultAccountName(); got != "spinifex" { + t.Errorf("DefaultAccountName() = %q, want %q", got, "spinifex") + } +} diff --git a/spinifex/gateway/admin.go b/spinifex/gateway/admin.go index 350f55b12..f3fe048be 100644 --- a/spinifex/gateway/admin.go +++ b/spinifex/gateway/admin.go @@ -12,8 +12,8 @@ import ( "uuid" "github.com/go-chi/chi/v5" - "github.com/mulgadc/spinifex/spinifex/admin" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" + awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers" ingresshttp "github.com/mulgadc/spinifex/spinifex/ingress/http" ) @@ -177,7 +177,7 @@ func (gw *GatewayConfig) authorizeAdmin(r *http.Request, method string) error { if service != "spinifex" { return deny("credential scope is not spinifex") } - if accountID != admin.DefaultAccountID() { + if accountID != awsidentifiers.DefaultAccountID() { return deny("not the super-admin account") } // Long-lived IAM users only. This excludes assumed-role sessions outright, diff --git a/spinifex/gateway/admin_test.go b/spinifex/gateway/admin_test.go index 4283ab7e6..0d030da92 100644 --- a/spinifex/gateway/admin_test.go +++ b/spinifex/gateway/admin_test.go @@ -14,8 +14,8 @@ import ( "testing" "github.com/go-chi/chi/v5" - "github.com/mulgadc/spinifex/spinifex/admin" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" + awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers" handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -78,7 +78,7 @@ func TestAuthorizeAdminGates(t *testing.T) { { name: "permitted signup principal", service: "spinifex", - accountID: admin.DefaultAccountID(), + accountID: awsidentifiers.DefaultAccountID(), identity: "signup", principalType: principalTypeUser, policy: createAccountPolicy(), @@ -86,7 +86,7 @@ func TestAuthorizeAdminGates(t *testing.T) { { name: "credential scope is not spinifex", service: "ec2", - accountID: admin.DefaultAccountID(), + accountID: awsidentifiers.DefaultAccountID(), identity: "signup", principalType: principalTypeUser, policy: createAccountPolicy(), @@ -104,7 +104,7 @@ func TestAuthorizeAdminGates(t *testing.T) { { name: "assumed-role session", service: "spinifex", - accountID: admin.DefaultAccountID(), + accountID: awsidentifiers.DefaultAccountID(), identity: "signup", principalType: principalTypeAssumedRole, policy: createAccountPolicy(), @@ -113,7 +113,7 @@ func TestAuthorizeAdminGates(t *testing.T) { { name: "no identity", service: "spinifex", - accountID: admin.DefaultAccountID(), + accountID: awsidentifiers.DefaultAccountID(), identity: "", principalType: principalTypeUser, policy: createAccountPolicy(), @@ -122,7 +122,7 @@ func TestAuthorizeAdminGates(t *testing.T) { { name: "policy does not grant CreateAccount", service: "spinifex", - accountID: admin.DefaultAccountID(), + accountID: awsidentifiers.DefaultAccountID(), identity: "signup", principalType: principalTypeUser, policy: &policyMockIAMService{}, @@ -256,7 +256,7 @@ func authorizedAdminRequestForMethod(gw *GatewayConfig, method, body string) *ht ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx) ctx = context.WithValue(ctx, ctxService, "spinifex") - ctx = context.WithValue(ctx, ctxAccountID, admin.DefaultAccountID()) + ctx = context.WithValue(ctx, ctxAccountID, awsidentifiers.DefaultAccountID()) ctx = context.WithValue(ctx, ctxIdentity, "admin") ctx = context.WithValue(ctx, ctxPrincipalType, principalTypeUser) diff --git a/spinifex/gateway/spinifex.go b/spinifex/gateway/spinifex.go index a1cdf1f22..e308efd20 100644 --- a/spinifex/gateway/spinifex.go +++ b/spinifex/gateway/spinifex.go @@ -6,8 +6,8 @@ import ( "log/slog" "net/http" - "github.com/mulgadc/spinifex/spinifex/admin" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" + awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers" gateway_bedrock "github.com/mulgadc/spinifex/spinifex/gateway/bedrock" gateway_spx "github.com/mulgadc/spinifex/spinifex/gateway/spx" ) @@ -50,7 +50,7 @@ func (gw *GatewayConfig) Spinifex_Request(w http.ResponseWriter, r *http.Request return errors.New(awserrors.ErrorServerInternal) } - if spinifexAdminActions[action] && accountID != admin.DefaultAccountID() { + if spinifexAdminActions[action] && accountID != awsidentifiers.DefaultAccountID() { slog.Info("Spinifex_Request: non-admin access denied", "action", action, "accountID", accountID) return errors.New(awserrors.ErrorAccessDenied) } diff --git a/spinifex/gateway/spinifex_modelaccess_test.go b/spinifex/gateway/spinifex_modelaccess_test.go index 031692ac0..c5ab3c7d2 100644 --- a/spinifex/gateway/spinifex_modelaccess_test.go +++ b/spinifex/gateway/spinifex_modelaccess_test.go @@ -9,8 +9,8 @@ import ( "testing" "github.com/mulgadc/spinifex/internal/testkit" - "github.com/mulgadc/spinifex/spinifex/admin" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" + awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers" gateway_bedrock "github.com/mulgadc/spinifex/spinifex/gateway/bedrock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -58,7 +58,7 @@ func accessGateway(t *testing.T) *GatewayConfig { // revoke takes it away again. func TestSpinifex_ModelAccess_GrantListRevoke(t *testing.T) { gw := accessGateway(t) - adminAccount := admin.DefaultAccountID() + adminAccount := awsidentifiers.DefaultAccountID() w := spinifexAccessRequest(t, gw, "ListModelAccess", adminAccount, url.Values{"AccountId": {"000000000002"}}) require.Equal(t, http.StatusOK, w.Code) @@ -97,7 +97,7 @@ func TestSpinifex_ModelAccess_NonAdminDenied(t *testing.T) { func TestSpinifex_ModelAccess_MissingParameters(t *testing.T) { gw := accessGateway(t) - adminAccount := admin.DefaultAccountID() + adminAccount := awsidentifiers.DefaultAccountID() cases := []struct { action string @@ -119,7 +119,7 @@ func TestSpinifex_ModelAccess_MissingParameters(t *testing.T) { // a grant store: the admin actions refuse rather than panicking on nil. func TestSpinifex_ModelAccess_NoStoreIsServerError(t *testing.T) { gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()} - adminAccount := admin.DefaultAccountID() + adminAccount := awsidentifiers.DefaultAccountID() for _, action := range []string{"GrantModelAccess", "RevokeModelAccess", "ListModelAccess"} { w := spinifexAccessRequest(t, gw, action, adminAccount, diff --git a/spinifex/gateway/spinifex_test.go b/spinifex/gateway/spinifex_test.go index ff80cbd60..8b7e2b4c6 100644 --- a/spinifex/gateway/spinifex_test.go +++ b/spinifex/gateway/spinifex_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "github.com/mulgadc/spinifex/spinifex/admin" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" + awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -41,7 +41,7 @@ func TestSpinifex_GetVersion_Admin(t *testing.T) { Commit: "ae1deb5", IAMService: allowAllIAMService(), } - w := spinifexRequest(t, gw, "GetVersion", admin.DefaultAccountID(), "admin") + w := spinifexRequest(t, gw, "GetVersion", awsidentifiers.DefaultAccountID(), "admin") assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), `"version":"v0.5.0-43-gae1deb5"`) assert.Contains(t, w.Body.String(), `"license":"open-source"`) @@ -75,7 +75,7 @@ func TestSpinifex_GetVMs_NonAdmin_Denied(t *testing.T) { func TestSpinifex_InvalidAction(t *testing.T) { gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()} - w := spinifexRequest(t, gw, "DoesNotExist", admin.DefaultAccountID(), "admin") + w := spinifexRequest(t, gw, "DoesNotExist", awsidentifiers.DefaultAccountID(), "admin") require.Equal(t, http.StatusForbidden, w.Code) assert.Contains(t, w.Body.String(), awserrors.ErrorInvalidAction) } @@ -84,7 +84,7 @@ func TestSpinifex_MissingAction(t *testing.T) { gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()} req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("")) ctx := context.WithValue(req.Context(), ctxService, "spinifex") - ctx = context.WithValue(ctx, ctxAccountID, admin.DefaultAccountID()) + ctx = context.WithValue(ctx, ctxAccountID, awsidentifiers.DefaultAccountID()) ctx = context.WithValue(ctx, ctxIdentity, "admin") req = req.WithContext(ctx) @@ -115,7 +115,7 @@ func TestSpinifex_GetNodes_NoNATS(t *testing.T) { NATSConn: nil, IAMService: allowAllIAMService(), } - w := spinifexRequest(t, gw, "GetNodes", admin.DefaultAccountID(), "admin") + w := spinifexRequest(t, gw, "GetNodes", awsidentifiers.DefaultAccountID(), "admin") require.Equal(t, http.StatusForbidden, w.Code) assert.Contains(t, w.Body.String(), awserrors.ErrorServerInternal) } diff --git a/spinifex/operator/cli/admin.go b/spinifex/operator/cli/admin.go index 2fd9bf238..b5e2b7239 100644 --- a/spinifex/operator/cli/admin.go +++ b/spinifex/operator/cli/admin.go @@ -1687,7 +1687,7 @@ func runAdminInit(cmd *cobra.Command, args []string) { // which persists them for the life of the node. They are recoverable from // bootstrap.json and ~/.aws/credentials. fmt.Printf("\n🔑 Generated admin credentials (written to ~/.aws/credentials)\n") - fmt.Printf(" Account: %s (%s)\n", legacyadmin.DefaultAccountName(), legacyadmin.DefaultAccountID()) + fmt.Printf(" Account: %s (%s)\n", awsidentifiers.DefaultAccountName(), awsidentifiers.DefaultAccountID()) fmt.Printf(" AWS Profile: spinifex\n") } @@ -1763,7 +1763,7 @@ func runAdminInit(cmd *cobra.Command, args []string) { networkConfig.ExternalMode = externalMode networkConfig.Pools = externalPools networkConfig.PoolDNSServers = dnsServers - networkConfig.BootstrapAccountId = legacyadmin.DefaultAccountID() + networkConfig.BootstrapAccountId = awsidentifiers.DefaultAccountID() networkConfig.BootstrapVpcId = bootstrapVpcId networkConfig.BootstrapSubnetId = bootstrapSubnetId networkConfig.BootstrapIgwId = bootstrapIgwId @@ -1883,7 +1883,7 @@ func runAdminInit(cmd *cobra.Command, args []string) { Pools: externalPools, OperatorEmail: email, - BootstrapAccountId: legacyadmin.DefaultAccountID(), + BootstrapAccountId: awsidentifiers.DefaultAccountID(), BootstrapVpcId: bootstrapVpcId, BootstrapSubnetId: bootstrapSubnetId, BootstrapIgwId: bootstrapIgwId, @@ -3476,8 +3476,8 @@ func writeBootstrapFilesWithAdmin(configDir, bootstrapDir string, masterKey []by EncryptedSecret: encryptedSecret, AccountID: accountID, Admin: &handlers_iam.AdminBootstrapData{ - AccountID: legacyadmin.DefaultAccountID(), - AccountName: legacyadmin.DefaultAccountName(), + AccountID: awsidentifiers.DefaultAccountID(), + AccountName: awsidentifiers.DefaultAccountName(), UserName: "admin", AccessKeyID: adminAccessKey, EncryptedSecret: adminEncryptedSecret, diff --git a/spinifex/operator/cli/admin_acm.go b/spinifex/operator/cli/admin_acm.go index a95ded811..c5d6919fd 100644 --- a/spinifex/operator/cli/admin_acm.go +++ b/spinifex/operator/cli/admin_acm.go @@ -9,8 +9,8 @@ import ( "strings" "time" - legacyadmin "github.com/mulgadc/spinifex/spinifex/admin" acmdomain "github.com/mulgadc/spinifex/spinifex/domains/acm" + awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers" "github.com/mulgadc/spinifex/spinifex/operator/admin" "github.com/spf13/cobra" ) @@ -73,7 +73,7 @@ func runCertForceRenew(cmd *cobra.Command, _ []string) { certArn, _ := cmd.Flags().GetString("arn") accountID, _ := cmd.Flags().GetString("account") if accountID == "" { - accountID = legacyadmin.DefaultAccountID() + accountID = awsidentifiers.DefaultAccountID() } _, nc, err := loadConfigAndConnect() diff --git a/spinifex/operator/cli/admin_eks.go b/spinifex/operator/cli/admin_eks.go index cd2e5ffd6..9b9ddcba9 100644 --- a/spinifex/operator/cli/admin_eks.go +++ b/spinifex/operator/cli/admin_eks.go @@ -6,7 +6,7 @@ import ( "os" "time" - "github.com/mulgadc/spinifex/spinifex/admin" + awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers" handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks" "github.com/spf13/cobra" ) @@ -46,7 +46,7 @@ func runAdminEksRestoreSnapshot(cmd *cobra.Command, _ []string) { snapshot, _ := cmd.Flags().GetString("snapshot") accountID, _ := cmd.Flags().GetString("account") if accountID == "" { - accountID = admin.DefaultAccountID() + accountID = awsidentifiers.DefaultAccountID() } _, nc, err := loadConfigAndConnect() diff --git a/spinifex/operator/cli/admin_principal.go b/spinifex/operator/cli/admin_principal.go index e3d383e04..66f110131 100644 --- a/spinifex/operator/cli/admin_principal.go +++ b/spinifex/operator/cli/admin_principal.go @@ -10,7 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/iam" - "github.com/mulgadc/spinifex/spinifex/admin" + awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers" "github.com/mulgadc/spinifex/spinifex/gateway" handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam" "github.com/spf13/cobra" @@ -46,7 +46,7 @@ func runAdminPrincipalCreate(cmd *cobra.Command, args []string) error { } defer cleanup() - accountID := admin.DefaultAccountID() + accountID := awsidentifiers.DefaultAccountID() if _, err := svc.CreateUser(accountID, &iam.CreateUserInput{ UserName: aws.String(userName), @@ -119,7 +119,7 @@ func runAdminPrincipalList(_ *cobra.Command, _ []string) error { } defer cleanup() - accountID := admin.DefaultAccountID() + accountID := awsidentifiers.DefaultAccountID() users, err := svc.ListUsers(accountID, &iam.ListUsersInput{}) if err != nil { return fmt.Errorf("list users: %w", err) @@ -249,7 +249,7 @@ func runAdminPrincipalRevoke(_ *cobra.Command, args []string) error { } defer cleanup() - accountID := admin.DefaultAccountID() + accountID := awsidentifiers.DefaultAccountID() if _, err := svc.GetUser(accountID, &iam.GetUserInput{UserName: aws.String(userName)}); err != nil { return fmt.Errorf("no principal %q in account %s", userName, accountID) } @@ -316,7 +316,7 @@ func runAdminPrincipalDelete(_ *cobra.Command, args []string) error { } defer cleanup() - accountID := admin.DefaultAccountID() + accountID := awsidentifiers.DefaultAccountID() if _, err := svc.GetUser(accountID, &iam.GetUserInput{UserName: aws.String(userName)}); err != nil { return fmt.Errorf("no principal %q in account %s", userName, accountID) } @@ -479,7 +479,7 @@ func runAdminPrincipalAudit(_ *cobra.Command, _ []string) error { } defer cleanup() - accountID := admin.DefaultAccountID() + accountID := awsidentifiers.DefaultAccountID() roles, err := svc.ListRoles(accountID, &iam.ListRolesInput{}) if err != nil { return fmt.Errorf("list roles: %w", err) diff --git a/spinifex/runtime/roles/awsgw/awsgw.go b/spinifex/runtime/roles/awsgw/awsgw.go index bb6e9e689..e74d23213 100644 --- a/spinifex/runtime/roles/awsgw/awsgw.go +++ b/spinifex/runtime/roles/awsgw/awsgw.go @@ -23,7 +23,6 @@ import ( "github.com/mulgadc/bluebottle/pkg/masterkey" "github.com/mulgadc/bluebottle/pkg/ratelimit" "github.com/mulgadc/bluebottle/pkg/tlsconfig" - "github.com/mulgadc/spinifex/spinifex/admin" "github.com/mulgadc/spinifex/spinifex/bootstrap/config" "github.com/mulgadc/spinifex/spinifex/daemon" acmawsapi "github.com/mulgadc/spinifex/spinifex/domains/acm/awsapi" @@ -372,12 +371,12 @@ func launchService(config *config.ClusterConfig) error { // start. Tenant accounts are unaffected and still begin with no access. // Best-effort: the gateway must serve even if this fails, and because the // marker is written only on success, the next start retries. - if seeded, err := bedrockAccess.SeedAccountGrants(context.Background(), admin.DefaultAccountID(), gateway_bedrock.CatalogModelIDs()); err != nil { + if seeded, err := bedrockAccess.SeedAccountGrants(context.Background(), awsidentifiers.DefaultAccountID(), gateway_bedrock.CatalogModelIDs()); err != nil { slog.Warn("Bedrock model access: seeding admin grants failed, will retry on next start", - "accountID", admin.DefaultAccountID(), "err", err) + "accountID", awsidentifiers.DefaultAccountID(), "err", err) } else if seeded { slog.Info("Bedrock model access: seeded admin account with the model catalog", - "accountID", admin.DefaultAccountID(), "models", len(gateway_bedrock.CatalogModelIDs())) + "accountID", awsidentifiers.DefaultAccountID(), "models", len(gateway_bedrock.CatalogModelIDs())) } // Bedrock self-host endpoints: OCHRE_VLLM_ENDPOINTS pins a modelId=baseURL