From 7fc79d95bdee890841d7533fffa2b60f859996d6 Mon Sep 17 00:00:00 2001 From: Josh Siviour Date: Mon, 26 Oct 2026 14:34:13 +1000 Subject: [PATCH] refactor: relocate lbagent to agents/elbv2 (#1268) Mechanical move permitted by the ADR-0001 lbagent amendment. The package becomes elbv2 under agents/; importers alias it lbagent. APIs, on-guest paths, NATS subjects and behaviour are unchanged. Q-30's data-plane boundary is recorded as deferred debt in the migration record. Also brings docs/DESIGN.md up to date with the current code: gateway dispatch and registrations, EC2 handler and NATS request signatures, RunInstances placement, daemon subscription form, volume mount subject, DescribeInstances scatter-gather, and the package layout. --- architecture/layering/allowlist_test.go | 2 +- architecture/layering/layers_test.go | 2 +- architecture/layering/legacy_test.go | 3 +- architecture/layering/rules_test.go | 6 +- cmd/lb-agent/main.go | 2 +- docs/DESIGN.md | 88 ++++++++----------- docs/PACKAGE_BOUNDARY_MIGRATION.md | 11 +-- spinifex/{lbagent => agents/elbv2}/agent.go | 4 +- .../elbv2}/agent_certs_test.go | 2 +- .../{lbagent => agents/elbv2}/agent_test.go | 2 +- spinifex/{lbagent => agents/elbv2}/health.go | 2 +- .../{lbagent => agents/elbv2}/health_test.go | 2 +- .../{lbagent => agents/elbv2}/healthcheck.go | 2 +- .../elbv2}/healthcheck_test.go | 2 +- spinifex/handlers/elbv2/agent_types.go | 2 +- spinifex/handlers/elbv2/agent_types_test.go | 2 +- spinifex/handlers/elbv2/haproxy.go | 2 +- spinifex/handlers/elbv2/haproxy_certs_test.go | 2 +- spinifex/handlers/elbv2/health_checker.go | 2 +- .../handlers/elbv2/health_checker_test.go | 2 +- spinifex/handlers/elbv2/nginx.go | 2 +- spinifex/handlers/elbv2/nginx_test.go | 2 +- .../elbv2/service_impl_listener_certs_test.go | 2 +- 23 files changed, 66 insertions(+), 82 deletions(-) rename spinifex/{lbagent => agents/elbv2}/agent.go (99%) rename spinifex/{lbagent => agents/elbv2}/agent_certs_test.go (99%) rename spinifex/{lbagent => agents/elbv2}/agent_test.go (99%) rename spinifex/{lbagent => agents/elbv2}/health.go (99%) rename spinifex/{lbagent => agents/elbv2}/health_test.go (98%) rename spinifex/{lbagent => agents/elbv2}/healthcheck.go (99%) rename spinifex/{lbagent => agents/elbv2}/healthcheck_test.go (99%) diff --git a/architecture/layering/allowlist_test.go b/architecture/layering/allowlist_test.go index 0d5b59754..a8ac0f5f0 100644 --- a/architecture/layering/allowlist_test.go +++ b/architecture/layering/allowlist_test.go @@ -146,7 +146,7 @@ var allowlist = []allowance{ {From: "handlers/elbv2", To: "domains/ec2/vpc", Debt: "handlers/elbv2 (cross-domain)", Reason: "legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"}, {From: "handlers/elbv2", To: "domains/network/topology", Debt: "handlers/elbv2 (cross-domain)", Reason: "legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"}, {From: "handlers/elbv2", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "system-instance role ensurer; needs an IAM capability"}, - {From: "handlers/elbv2", To: "lbagent", Debt: "handlers/elbv2/{agent_types,haproxy,health_checker,nginx}.go import lbagent", Reason: "health-report and cert/PID-path vocabulary; needs a guest/controller contract"}, + {From: "handlers/elbv2", To: "agents/elbv2", Debt: "handlers/elbv2/{agent_types,haproxy,health_checker,nginx}.go import lbagent", Reason: "health-report and cert/PID-path vocabulary; needs a guest/controller contract"}, {From: "domains/rds/instance", To: "domains/dns", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"}, {From: "domains/rds/instance", To: "domains/ec2/instance", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"}, diff --git a/architecture/layering/layers_test.go b/architecture/layering/layers_test.go index 29fab1958..e877beca8 100644 --- a/architecture/layering/layers_test.go +++ b/architecture/layering/layers_test.go @@ -61,7 +61,7 @@ func (c class) isLegacy(roots ...string) bool { // legacyRoots are pre-ADR-0001 application roots still standing. var legacyRoots = map[string]bool{ "handlers": true, "gateway": true, - "accountteardown": true, "lbagent": true, + "accountteardown": true, } // gatewayDomains names the domain each gateway/ adapter belongs to; diff --git a/architecture/layering/legacy_test.go b/architecture/layering/legacy_test.go index 15a22e04e..d28dc07c2 100644 --- a/architecture/layering/legacy_test.go +++ b/architecture/layering/legacy_test.go @@ -17,12 +17,11 @@ var frozenLegacyPackages = []string{ "gateway/spx", "handlers/ecs", "handlers/elbv2", - "lbagent", } // retiredLegacyRoots were legacy roots whose every package has moved to its // ADR-0001 target; recreating one would reopen a closed migration row. -var retiredLegacyRoots = []string{"admin", "daemon", "services", "utils", "vpcd"} +var retiredLegacyRoots = []string{"admin", "daemon", "lbagent", "services", "utils", "vpcd"} // TestADR0001_LegacyRootsFrozen: the current-to-target map gives every legacy // root a destination, so none may gain a package and no retired root may return. diff --git a/architecture/layering/rules_test.go b/architecture/layering/rules_test.go index d6a3d0e66..8863954b5 100644 --- a/architecture/layering/rules_test.go +++ b/architecture/layering/rules_test.go @@ -121,7 +121,7 @@ var rules = []rule{ return false } switch { - case t.layer == layerAgents || t.isLegacy("lbagent"): + case t.layer == layerAgents: return true case t.layer == layerIngress: return f.kind != domainAWSAPI && f.kind != legacyAdapter @@ -137,9 +137,9 @@ var rules = []rule{ id: "Agents_ImportNoDomainOrLegacyImplementation", clause: "area table: agents/ own in-guest and service-role behaviour, not tenant API authority or another domain's private state", hint: "move the shared wire vocabulary behind a guest/controller contract under contracts/", - subject: func(f class, _ string) bool { return f.layer == layerAgents || f.isLegacy("lbagent") }, + subject: func(f class, _ string) bool { return f.layer == layerAgents }, forbids: func(f, t class, _ string) bool { - return (f.layer == layerAgents || f.isLegacy("lbagent")) && (t.domainShaped() || t.isLegacy()) + return f.layer == layerAgents && (t.domainShaped() || t.isLegacy()) }, }, { diff --git a/cmd/lb-agent/main.go b/cmd/lb-agent/main.go index 1c277b468..1799ab320 100644 --- a/cmd/lb-agent/main.go +++ b/cmd/lb-agent/main.go @@ -19,7 +19,7 @@ import ( "syscall" "github.com/mulgadc/bluebottle/pkg/otelsetup" - "github.com/mulgadc/spinifex/spinifex/lbagent" + lbagent "github.com/mulgadc/spinifex/spinifex/agents/elbv2" _ "github.com/mulgadc/bluebottle/pkg/fipsboot" ) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index e8ff01512..f566f58fe 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -50,14 +50,12 @@ Request routing (`spinifex/gateway/gateway.go`): 1. **Authentication**: SigV4 middleware validates AWS credentials and resolves the account ID 2. **Throttling**: Per-account+action token bucket rejects bursts post-auth 3. **Service Detection**: Extracts service name from the Authorization header -4. **Action Dispatch**: Routes to service-specific handler. A service registered through the ingress dispatch registry (`ingress/aws/dispatch`, wired by `runtime/roles/awsgw`) is dispatched by its registration; currently that is ECR's control plane, registered from `domains/ecr/awsapi`. Every other service uses the legacy switch: +4. **Action Dispatch**: Routes to service-specific handler. A service registered through the ingress dispatch registry (`ingress/aws/dispatch`, wired by `runtime/roles/awsgw`) is dispatched by its registration; currently those are the ECR and ACM control planes, registered from `domains/ecr/awsapi` and `domains/acm/awsapi`. Every other service uses the legacy switch: ```go switch svc { case "ec2": err = gw.EC2_Request(w, r) -case "account": - err = gw.Account_Request(w, r) case "iam": err = gw.IAM_Request(w, r) case "sts": @@ -66,10 +64,13 @@ case "elasticloadbalancing": err = gw.ELBv2_Request(w, r) case "eks": err = gw.EKS_Request(w, r) +case "bedrock": + err = gw.Bedrock_Request(w, r) +// ... bedrock-runtime, bedrock-agent, bedrock-agent-runtime case "ecs": err = gw.ECS_Request(w, r) -case "acm": - err = gw.ACM_Request(w, r) +case "rds": + err = gw.RDS_Request(w, r) case "tagging": err = gw.Tagging_Request(w, r) case "spinifex": @@ -79,14 +80,15 @@ case "spinifex": ### 3. EC2 Handler -The EC2 handler (`spinifex/gateway/ec2.go`) parses the `Action` parameter and delegates to specific handlers via a generic `ec2Handler` wrapper. The account ID resolved by SigV4 auth is threaded into every handler so requests are scoped per IAM principal: +The EC2 handler (`spinifex/gateway/ec2.go`) parses the `Action` parameter and delegates to specific handlers via the generic `ec2Handler` and `ec2HandlerWithReq` wrappers. The account ID resolved by SigV4 auth is threaded into every handler so requests are scoped per IAM principal: ```go -"RunInstances": ec2Handler(func(input *ec2.RunInstancesInput, gw *GatewayConfig, accountID string) (any, error) { - return ec2instanceapi.RunInstances(input, gw.NATSConn, accountID) +"RunInstances": ec2HandlerWithReq(func(ctx context.Context, input *ec2.RunInstancesInput, gw *GatewayConfig, accountID string, r *http.Request) (any, error) { + // ... PassRole and launch-quota checks, then the vCPU quota charge + return ec2instanceapi.RunInstances(ctx, input, gw.NATSConn, gw.IAMService, accountID, passRoleCheck, launchQuotaCheck, gw.ExpectedNodes) }), -"DescribeInstances": ec2Handler(func(input *ec2.DescribeInstancesInput, gw *GatewayConfig, accountID string) (any, error) { - return ec2instanceapi.DescribeInstances(input, gw.NATSConn, gw.DiscoverActiveNodes(), accountID) +"DescribeInstances": ec2Handler(func(ctx context.Context, input *ec2.DescribeInstancesInput, gw *GatewayConfig, accountID string) (any, error) { + return ec2instanceapi.DescribeInstancesChecked(ctx, input, gw.NATSConn, 0, gw.NodeIDs, accountID) }), // ... + volumes, snapshots, VPCs, subnets, route tables, IGWs, NAT gateways, // security groups, network interfaces, elastic IPs, placement groups, key pairs, @@ -98,13 +100,13 @@ The EC2 handler (`spinifex/gateway/ec2.go`) parses the `Action` parameter and de The gateway communicates with daemons via NATS request/response. Most calls go through `natsmsg.NATSRequest`, which marshals the input, attaches the account ID as a NATS header, and unmarshals the typed response: ```go -func (s *NATSInstanceService) RunInstances(input *ec2.RunInstancesInput, accountID string) (*ec2.Reservation, error) { +func (s *NATSInstanceService) RunInstances(ctx context.Context, input *ec2.RunInstancesInput, accountID string) (*ec2.Reservation, error) { topic := fmt.Sprintf("ec2.RunInstances.%s", aws.StringValue(input.InstanceType)) - return natsmsg.NATSRequest[ec2.Reservation](s.natsConn, topic, input, 5*time.Minute, accountID) + return natsmsg.NATSRequest[ec2.Reservation](ctx, s.natsConn, topic, input, 5*time.Minute, accountID) } ``` -`RunInstances` uses a per-instance-type subject so NATS only delivers the request to a node with spare capacity for that type — no application-level reject-and-retry. +The gateway's `RunInstances` places instances itself (`domains/ec2/awsapi/instance/placement.go`): it gathers capacity from every node on `spinifex.node.status`, spreads the launch across eligible nodes, and sends each node its share on the node-targeted subject `ec2.RunInstances.{type}.{nodeId}`, rolling back on partial failure. ### Cross-Process Contract Ownership @@ -128,10 +130,10 @@ Daemons (`spinifex/runtime/daemon/daemon.go`) subscribe to NATS topics and handl ```go // Queue group → load-balanced (one daemon handles each request) -{"ec2.CreateKeyPair", d.handleEC2CreateKeyPair, "spinifex-workers"}, +{"ec2.CreateKeyPair", handleNATSRequest(d.node, d.keyService.CreateKeyPair), "spinifex-workers"}, // No queue group → fan-out (every daemon responds) -{"ec2.DescribeInstanceTypes", d.handleEC2DescribeInstanceTypes, ""}, +{"ec2.DescribeInstanceTypes", handleNATSRequest(d.node, d.instanceService.DescribeInstanceTypes), ""}, ``` `ec2.RunInstances` is dynamic: `ResourceManager` subscribes to `ec2.RunInstances.{type}` only while the node has capacity for that type, and unsubscribes when full. The node-targeted subject `ec2.RunInstances.{type}.{nodeId}` stays subscribed even at full capacity — the launch-time `allocate()` enforces limits there. Per-instance commands flow over `ec2.cmd.{instanceID}`, subscribed only by the owning node. @@ -144,20 +146,17 @@ When a daemon handles `RunInstances`: 1. **Resource Check**: Validates CPU/memory availability via `ResourceManager` 2. **Volume Generation**: Creates boot and EFI volumes via Viperblock -3. **Volume Mount**: Sends `ebs.mount` request to Viperblock, receives NBD URI +3. **Volume Mount**: Sends a mount request to the colocated Viperblock worker on `ebs.{node}.mount`, receives NBD URI 4. **QEMU Launch**: Builds and executes QEMU command with NBD-backed disks 5. **QMP Monitoring**: Establishes QEMU Machine Protocol connection for VM management 6. **Command subscription**: Subscribes to `ec2.cmd.{instanceID}` for subsequent commands on this VM ```go -// Volume mounting via NATS -msg, err := d.natsConn.Request("ebs.mount", ebsMountRequest, 10*time.Second) +// Volume mounting via NATS (runtime/daemon/vm_adapters.go) +reply, err := ebsRequestWithTrace(ctx, a.nc, instance.AccountID, viperblocklegacyv1.MountSubject(a.node), ebsMountRequest, 30*time.Second) -// QEMU launch with NBD storage -cmd := exec.Command("qemu-system-x86_64", - "-drive", fmt.Sprintf("file=nbd:%s,format=raw", nbdURI), - // ... additional QEMU args -) +// QEMU launch with NBD-backed drives in args (runtime/compute/vm/vm.go) +cmd := exec.Command(qemuArchitecture, args...) // qemu-system-x86_64 or qemu-system-aarch64 ``` ## Key Components @@ -199,33 +198,17 @@ type ResourceManager struct { ### Multi-Node Aggregation -For operations that need data from all nodes (like `DescribeInstances`), the gateway uses inbox-based fan-out (`spinifex/domains/ec2/awsapi/instance/DescribeInstances.go`): +For operations that need data from all nodes (like `DescribeInstances`), the gateway uses the scatter-gather helper `natsmsg.Gather` (`spinifex/foundation/messaging/nats/nats.go`), called from `spinifex/domains/ec2/awsapi/instance/DescribeInstances.go`: ```go -func DescribeInstances(...) { - // Create unique inbox for collecting responses - inbox := nats.NewInbox() - sub, _ := natsConn.SubscribeSync(inbox) - - // Publish to all nodes (no queue group = all daemons respond). - // Account ID is propagated as a NATS header so each daemon scopes its response. - pubMsg := nats.NewMsg("ec2.DescribeInstances") - pubMsg.Reply = inbox - pubMsg.Data = jsonData - pubMsg.Header.Set(natsmsg.AccountIDHeader, accountID) - _ = natsConn.PublishMsg(pubMsg) - - // Collect responses, returning early once expectedNodes have replied - // or the 3s timeout is hit. - for { - msg, err := sub.NextMsg(3 * time.Second) - allReservations = append(allReservations, nodeOutput.Reservations...) - } - - return allReservations -} +// Publish to every node (no queue group = all daemons respond) and collect +// the replies. The account ID travels as a NATS header so each daemon scopes +// its response. +frames, sum, err := natsmsg.Gather(ctx, natsConn, "ec2.DescribeInstances", jsonData, gatherOpts) ``` +Given the configured node IDs, `Gather` checks each reply's `X-Node-ID` header against them, and `DescribeInstancesChecked` fails closed rather than returning a partial listing when a node is missing, duplicated or unidentified. + ## Storage Integration ### Viperblock (EBS) @@ -240,6 +223,7 @@ type EBSRequest struct { Boot bool // Boot volume flag EFI bool // EFI boot volume NBDURI string // NBD URI returned from mount + // ... + DeviceName, DeleteOnTermination, hotplug port and host block-device fields } ``` @@ -298,6 +282,7 @@ type Config struct { Predastore PredastoreConfig // S3 endpoint + service credentials Viperblock ViperblockConfig VPCD VPCDConfig // OVN/OVS endpoints + // ... + EBS, Northstar, RDS, ACM, Bedrock, OchreVector } ``` @@ -320,15 +305,14 @@ spinifex/ │ ├── ingress/ # AWS protocol ingress: query/REST parsing, dispatch registry, rate limiting │ ├── runtime/ │ │ ├── roles/ # Long-running service roles (awsgw, spinifex, vpcd, nats, predastore, ...) +│ │ ├── daemon/ # Per-node daemon and its NATS subscription table │ │ └── compute/ # VM lifecycle, QMP, NBD, GPU │ ├── bootstrap/ # Cluster and node configuration structures, preflight checks │ ├── operator/ # spx CLI commands, admin use cases, installer, upgrade -│ ├── agents/ # In-guest agent libraries (ecs, eks, rds) +│ ├── agents/ # In-guest agent libraries (ecs, eks, elbv2, rds) │ ├── gateway/ # Legacy root: HTTP route assembly, SigV4 auth, legacy per-service dispatch -│ ├── handlers/ # Legacy root: ecs, eks, elbv2, iam, rds, sts service implementations -│ ├── daemon/ # Legacy root: per-node daemon and its NATS subscription table -│ ├── accountteardown/ # Legacy root: account deletion cleanup -│ └── lbagent/ # Legacy root: load-balancer agent +│ ├── handlers/ # Legacy root: ecs and elbv2 service implementations +│ └── accountteardown/ # Legacy root: account deletion cleanup ├── internal/ # In-guest SigV4 clients, AWS Smithy models and test kits ├── architecture/ # Executable architecture rules (package layering) ├── build/ # systemd units, node helper scripts, microVM build @@ -338,7 +322,7 @@ spinifex/ └── e2e/ # Live-cluster suites (see docs/testing/e2e-coverage.md) ``` -The five legacy roots predate ADR-0001 and are frozen: they may shrink but not gain packages. +The three remaining legacy roots predate ADR-0001 and are frozen: they may shrink but not gain packages. [`PACKAGE_BOUNDARY_MIGRATION.md`](PACKAGE_BOUNDARY_MIGRATION.md) records what remains in each and what blocks its move. ## Development diff --git a/docs/PACKAGE_BOUNDARY_MIGRATION.md b/docs/PACKAGE_BOUNDARY_MIGRATION.md index 604be2c1b..3248a0ca3 100644 --- a/docs/PACKAGE_BOUNDARY_MIGRATION.md +++ b/docs/PACKAGE_BOUNDARY_MIGRATION.md @@ -346,7 +346,8 @@ while the branch is in flight; they do not describe the umbrella repository's | `spinifex#1264` | `spinifex/handlers/eks` | `spinifex/domains/eks/cluster` (`cluster`, imported as `ekscluster`) | A mechanical relocation of the remaining EKS implementation, nodegroups included; APIs, NATS subjects, KV buckets and behaviour are unchanged and only the package name changed from `handlers_eks`. The frozen-legacy entry is removed, the allowlist entries naming `handlers/eks` now name `domains/eks/cluster`, and the `operator/cli` entry is removed because an operator importing a domain package no longer breaks the legacy rule. Nodegroup service extraction is deferred. | | `spinifex#1265` | `spinifex/gateway/{iam,sts,eks,rds}` | `spinifex/domains/{iam,sts,eks,rds}/awsapi` (`awsapi`, imported as `iamawsapi`, `stsawsapi`, `eksawsapi`, `rdsawsapi`) | A mechanical relocation of the IAM, STS, EKS and RDS AWS action adapters to their ADR-0001 domain homes; APIs, NATS subjects, KV buckets and behaviour are unchanged. The root `gateway` dispatch still imports each package, recorded as `gateway` (service registration) debt. `gateway/bedrock` stays: ADR-0001 moves only its action adapters, and the package also holds the Ochre domain core, stores and inference engine. | | `spinifex#1266` | `spinifex/gateway/bedrock` | `spinifex/domains/ochre` (merged into package `ochre`) | A mechanical whole-package relocation: the Ochre domain core, stores, inference engine and Bedrock action layer join the existing `domains/ochre` package with their package-private code and tests intact. No identifier, API, NATS subject, KV bucket, metric, error string or behaviour changes; `domains/ochre` no longer imports a legacy package. Separating the action adapters into `domains/ochre/awsapi` is deferred, recorded as `domains/ochre` (action adapters) debt. | -| — | `spinifex/daemon` | `spinifex/runtime/daemon` | A mechanical whole-package relocation under the 2026-10-26 ADR-0001 amendment: the per-node daemon keeps package name `daemon`, its files, tests and current dependencies. No identifier, API, NATS subject, KV bucket, metric, error string or behaviour changes. The interim daemon → gateway → handlers guardrail now matches `runtime/daemon`. Q-28's ownership and deployment refactor is deferred, recorded as `runtime/daemon` (Q-28 ownership and deployment) debt. | +| `spinifex#1267` | `spinifex/daemon` | `spinifex/runtime/daemon` | A mechanical whole-package relocation under the 2026-10-26 ADR-0001 amendment: the per-node daemon keeps package name `daemon`, its files, tests and current dependencies. No identifier, API, NATS subject, KV bucket, metric, error string or behaviour changes. The interim daemon → gateway → handlers guardrail now matches `runtime/daemon`. Q-28's ownership and deployment refactor is deferred, recorded as `runtime/daemon` (Q-28 ownership and deployment) debt. | +| — | `spinifex/lbagent` | `spinifex/agents/elbv2` (package `elbv2`) | A mechanical whole-package relocation under the 2026-10-26 ADR-0001 amendment: the load-balancer agent keeps its files, tests and current dependencies, and importers alias it `lbagent` so call sites are unchanged. No identifier, API, NATS subject, file path on the guest, metric, error string or behaviour changes; `cmd/lb-agent` stays the thin main. Q-30's data-plane boundary is deferred, recorded as `agents/elbv2` (Q-30 data-plane boundary) debt. | 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 @@ -420,7 +421,8 @@ These residuals were recorded by individual slices and must be resolved once the - The public `/keys` endpoint (root `gateway`, `oidc_discovery.go`) serves malformed stored JWKS bytes with HTTP 200. Removal condition: validate before serving, or record it as accepted. - The public discovery and `/keys` endpoints return 404 for KV read errors, including timeouts; only a bucket-open failure returns 500. Removal condition: distinguish dependency failure from not-found. - The root `gateway` package imports `domains/eks/cluster` directly (`gateway/oidc_discovery.go`: `AccountBucketName`, `GetClusterMeta`). The layering rules don't see it because the root gateway has no domain classification, so it has no allowlist entry. Removal condition: the public OIDC discovery and JWKS endpoints consume an EKS-owned resolver injected at composition, the same pattern as STS, leaving the root gateway with no `domains/eks/cluster` import. -- `handlers/elbv2/{agent_types,haproxy,health_checker,nginx}.go` import `lbagent` (`HealthReport`, `ServerStatus`, `CertDir`, `NginxPIDPath`, `NginxCertDir`), a domain reading an agent's implementation package rather than a declared wire contract. Removal condition: the health-report and cert/PID-path vocabulary moves behind a guest/controller contract the domain and the agent both consume. +- `handlers/elbv2/{agent_types,haproxy,health_checker,nginx}.go` import `agents/elbv2` (`HealthReport`, `ServerStatus`, `CertDir`, `NginxPIDPath`, `NginxCertDir`), a domain reading an agent's implementation package rather than a declared wire contract. Removal condition: the health-report and cert/PID-path vocabulary moves behind a guest/controller contract the domain and the agent both consume. +- `agents/elbv2` (Q-30 data-plane boundary): the agent moved whole, keeping its current dependencies and the `handlers/elbv2` import above. Q-30's data-plane boundary and the common agent layout remain deferred by ADR-0001 and are not paid by the move. - `domains/ec2/instance/state/imds_readers.go` (dependency, IMDS readers) imports `runtime/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. - Operator bootstrap credential generation: `operator/cli/admin.go` mints the bootstrap admin and Northstar access keys and secrets directly with `awscredentials` and writes them into config, rather than asking an IAM bootstrap capability to issue them. `awscredentials` only formats the values. Removal condition: a real IAM bootstrap capability issues these credentials and `operator/cli` calls it. @@ -678,18 +680,17 @@ series above; "needs decision" is open. | `daemon` | `runtime/daemon` (whole-package move, above) | Done. Q-28's ownership and deployment refactor remains deferred debt; the amended ADR-0001 row makes it not a relocation prerequisite. | | `admin` | — | Done and deleted. The operator use cases moved to `operator/admin`, image promotion to `domains/ec2/image`, the bootstrap account identifiers to `foundation/aws/identifiers`, and the credential-format generators to `foundation/aws/credentials` (recorded above). Operator bootstrap's direct credential generation stays recorded as debt. | | `accountteardown` | The whole package, a frozen legacy root. Its direct cleanup calls into `domains/ec2/awsapi/*` action adapters, other domain and handler services, the object-store provider, raw NATS subjects and a raw KV bucket stay as recorded debt. | Assessed for relocation and not moved. ADR-0001's `accountteardown` row requires conversion to domain-owned cleanup capabilities before moving, and ADR-0008 does not authorise a mechanical rename. It remains a legacy root until its direct cleanup calls are converted to domain-owned capabilities; its destination after that conversion is `operator/accountlifecycle`. | -| `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. | +| `lbagent` | `agents/elbv2` (whole-package move, above) | Done. Q-30's data-plane boundary remains deferred debt; the amended ADR-0001 row makes it not a relocation prerequisite. The `handlers/elbv2` import stays recorded debt. | | `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. | ### Remaining legacy roots by blocker class -Four legacy roots remain: `accountteardown`, `gateway`, `handlers` and `lbagent`. +Three legacy roots remain: `accountteardown`, `gateway` and `handlers`. The rows above give each path's blocker; this summary groups them so a deferral is not mistaken for unfinished mechanical work. ADR-0001 completion is structural, so none of these is cleared by remediating service lifecycle behaviour. | Class | Paths | Status | |---|---|---| -| Explicitly deferred by ADR-0001 | `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`); `admin` bootstrap account identifiers → `foundation/aws/identifiers` (`3fdaa54d9`); `admin` credential-format generators → `foundation/aws/credentials` `0368e861a`, retiring `admin` | Done. | | One narrow missing boundary | Done: `handlers/sts` EKS JWKS lookup → `handlers/eks` `ClusterJWKSResolver` (`254d898a3`); `gateway/tagging` → `domains/tagging` with AWSGW-composed listers (`0d830e901`, `f3545e3ea`, `aedd0b2c4`). Open: `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/{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/{ecs,elbv2}` (each follows its `handlers/`, because a new-path `awsapi` cannot import a legacy handler) | Recorded debt with removal conditions above. | diff --git a/spinifex/lbagent/agent.go b/spinifex/agents/elbv2/agent.go similarity index 99% rename from spinifex/lbagent/agent.go rename to spinifex/agents/elbv2/agent.go index 5b8250406..d747fe606 100644 --- a/spinifex/lbagent/agent.go +++ b/spinifex/agents/elbv2/agent.go @@ -1,7 +1,7 @@ -// Package lbagent implements the LB config agent that runs inside load balancer VMs. +// Package elbv2 implements the LB config agent that runs inside load balancer VMs. // It polls the AWS gateway for config updates and reports health via heartbeats. // All communication uses SigV4-signed HTTP requests to the gateway. -package lbagent +package elbv2 import ( "bytes" diff --git a/spinifex/lbagent/agent_certs_test.go b/spinifex/agents/elbv2/agent_certs_test.go similarity index 99% rename from spinifex/lbagent/agent_certs_test.go rename to spinifex/agents/elbv2/agent_certs_test.go index 02afae66b..b87b3b439 100644 --- a/spinifex/lbagent/agent_certs_test.go +++ b/spinifex/agents/elbv2/agent_certs_test.go @@ -1,4 +1,4 @@ -package lbagent +package elbv2 import ( "os" diff --git a/spinifex/lbagent/agent_test.go b/spinifex/agents/elbv2/agent_test.go similarity index 99% rename from spinifex/lbagent/agent_test.go rename to spinifex/agents/elbv2/agent_test.go index 62b9212ae..98e2bdb8c 100644 --- a/spinifex/lbagent/agent_test.go +++ b/spinifex/agents/elbv2/agent_test.go @@ -1,4 +1,4 @@ -package lbagent +package elbv2 import ( "bytes" diff --git a/spinifex/lbagent/health.go b/spinifex/agents/elbv2/health.go similarity index 99% rename from spinifex/lbagent/health.go rename to spinifex/agents/elbv2/health.go index 7510a7954..fe0019ec3 100644 --- a/spinifex/lbagent/health.go +++ b/spinifex/agents/elbv2/health.go @@ -1,4 +1,4 @@ -package lbagent +package elbv2 import ( "bufio" diff --git a/spinifex/lbagent/health_test.go b/spinifex/agents/elbv2/health_test.go similarity index 98% rename from spinifex/lbagent/health_test.go rename to spinifex/agents/elbv2/health_test.go index 1abcd1d97..2c25be53a 100644 --- a/spinifex/lbagent/health_test.go +++ b/spinifex/agents/elbv2/health_test.go @@ -1,4 +1,4 @@ -package lbagent +package elbv2 import ( "fmt" diff --git a/spinifex/lbagent/healthcheck.go b/spinifex/agents/elbv2/healthcheck.go similarity index 99% rename from spinifex/lbagent/healthcheck.go rename to spinifex/agents/elbv2/healthcheck.go index 4bd752283..0cedd43c1 100644 --- a/spinifex/lbagent/healthcheck.go +++ b/spinifex/agents/elbv2/healthcheck.go @@ -1,4 +1,4 @@ -package lbagent +package elbv2 import ( "crypto/tls" diff --git a/spinifex/lbagent/healthcheck_test.go b/spinifex/agents/elbv2/healthcheck_test.go similarity index 99% rename from spinifex/lbagent/healthcheck_test.go rename to spinifex/agents/elbv2/healthcheck_test.go index 2a237be06..83070b787 100644 --- a/spinifex/lbagent/healthcheck_test.go +++ b/spinifex/agents/elbv2/healthcheck_test.go @@ -1,4 +1,4 @@ -package lbagent +package elbv2 import ( "net" diff --git a/spinifex/handlers/elbv2/agent_types.go b/spinifex/handlers/elbv2/agent_types.go index 7b0f7e9f2..7c356937b 100644 --- a/spinifex/handlers/elbv2/agent_types.go +++ b/spinifex/handlers/elbv2/agent_types.go @@ -2,7 +2,7 @@ package handlers_elbv2 import ( "github.com/aws/aws-sdk-go/aws" - "github.com/mulgadc/spinifex/spinifex/lbagent" + lbagent "github.com/mulgadc/spinifex/spinifex/agents/elbv2" ) // LBAgentHeartbeatInput is sent by the LB agent on each heartbeat tick, diff --git a/spinifex/handlers/elbv2/agent_types_test.go b/spinifex/handlers/elbv2/agent_types_test.go index 58edc0ffa..01381d681 100644 --- a/spinifex/handlers/elbv2/agent_types_test.go +++ b/spinifex/handlers/elbv2/agent_types_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/mulgadc/spinifex/spinifex/lbagent" + lbagent "github.com/mulgadc/spinifex/spinifex/agents/elbv2" "github.com/stretchr/testify/assert" ) diff --git a/spinifex/handlers/elbv2/haproxy.go b/spinifex/handlers/elbv2/haproxy.go index f5c11dad1..e5e892e47 100644 --- a/spinifex/handlers/elbv2/haproxy.go +++ b/spinifex/handlers/elbv2/haproxy.go @@ -9,7 +9,7 @@ import ( "strings" "text/template" - "github.com/mulgadc/spinifex/spinifex/lbagent" + lbagent "github.com/mulgadc/spinifex/spinifex/agents/elbv2" ) // All interpolated fields are regex-validated at the input boundary. diff --git a/spinifex/handlers/elbv2/haproxy_certs_test.go b/spinifex/handlers/elbv2/haproxy_certs_test.go index 85d3a36e2..97cd47d13 100644 --- a/spinifex/handlers/elbv2/haproxy_certs_test.go +++ b/spinifex/handlers/elbv2/haproxy_certs_test.go @@ -14,11 +14,11 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/mulgadc/spinifex/internal/testkit" + lbagent "github.com/mulgadc/spinifex/spinifex/agents/elbv2" acmdomain "github.com/mulgadc/spinifex/spinifex/domains/acm" iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" awsxml "github.com/mulgadc/spinifex/spinifex/foundation/aws/xml" - "github.com/mulgadc/spinifex/spinifex/lbagent" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/spinifex/handlers/elbv2/health_checker.go b/spinifex/handlers/elbv2/health_checker.go index 75394590c..a1edcc0db 100644 --- a/spinifex/handlers/elbv2/health_checker.go +++ b/spinifex/handlers/elbv2/health_checker.go @@ -7,7 +7,7 @@ import ( "log/slog" "sync" - "github.com/mulgadc/spinifex/spinifex/lbagent" + lbagent "github.com/mulgadc/spinifex/spinifex/agents/elbv2" ) // healthChecker processes target health reports from LB agents pushed via diff --git a/spinifex/handlers/elbv2/health_checker_test.go b/spinifex/handlers/elbv2/health_checker_test.go index bb7a3b4b9..5c019cd12 100644 --- a/spinifex/handlers/elbv2/health_checker_test.go +++ b/spinifex/handlers/elbv2/health_checker_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/mulgadc/spinifex/internal/testkit" - "github.com/mulgadc/spinifex/spinifex/lbagent" + lbagent "github.com/mulgadc/spinifex/spinifex/agents/elbv2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/spinifex/handlers/elbv2/nginx.go b/spinifex/handlers/elbv2/nginx.go index 305c2ab06..ffc332c7f 100644 --- a/spinifex/handlers/elbv2/nginx.go +++ b/spinifex/handlers/elbv2/nginx.go @@ -10,7 +10,7 @@ import ( "strings" "text/template" - "github.com/mulgadc/spinifex/spinifex/lbagent" + lbagent "github.com/mulgadc/spinifex/spinifex/agents/elbv2" ) // maxDNSNameLen is the longest legal DNS name, so a map key cannot be padded out diff --git a/spinifex/handlers/elbv2/nginx_test.go b/spinifex/handlers/elbv2/nginx_test.go index 3e51786f2..ed3d1e073 100644 --- a/spinifex/handlers/elbv2/nginx_test.go +++ b/spinifex/handlers/elbv2/nginx_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/mulgadc/spinifex/spinifex/lbagent" + lbagent "github.com/mulgadc/spinifex/spinifex/agents/elbv2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/spinifex/handlers/elbv2/service_impl_listener_certs_test.go b/spinifex/handlers/elbv2/service_impl_listener_certs_test.go index 3a1dde190..cc6328d95 100644 --- a/spinifex/handlers/elbv2/service_impl_listener_certs_test.go +++ b/spinifex/handlers/elbv2/service_impl_listener_certs_test.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elbv2" + lbagent "github.com/mulgadc/spinifex/spinifex/agents/elbv2" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - "github.com/mulgadc/spinifex/spinifex/lbagent" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" )