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.
This commit is contained in:
Josh Siviour
2026-10-26 14:34:13 +10:00
committed by GitHub
parent 0c440a6717
commit 7fc79d95bd
23 changed files with 66 additions and 82 deletions
+1 -1
View File
@@ -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"},
+1 -1
View File
@@ -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/<service> adapter belongs to;
+1 -2
View File
@@ -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.
+3 -3
View File
@@ -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/<domain> 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())
},
},
{
+1 -1
View File
@@ -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"
)
+36 -52
View File
@@ -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
+6 -5
View File
@@ -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/<svc>`, because a new-path `awsapi` cannot import a legacy handler) | Recorded debt with removal conditions above. |
@@ -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"
@@ -1,4 +1,4 @@
package lbagent
package elbv2
import (
"os"
@@ -1,4 +1,4 @@
package lbagent
package elbv2
import (
"bytes"
@@ -1,4 +1,4 @@
package lbagent
package elbv2
import (
"bufio"
@@ -1,4 +1,4 @@
package lbagent
package elbv2
import (
"fmt"
@@ -1,4 +1,4 @@
package lbagent
package elbv2
import (
"crypto/tls"
@@ -1,4 +1,4 @@
package lbagent
package elbv2
import (
"net"
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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"
)
+1 -1
View File
@@ -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.
@@ -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"
)
+1 -1
View File
@@ -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
@@ -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"
)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"
)
@@ -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"
)