Compare commits

..

4 Commits

Author SHA1 Message Date
Josh Siviour a6b6bb73a9 fix: restore ADR-0007 invariant scan coverage for relocated domains (#1272)
The IAM, STS, RDS, EKS and ELBv2 relocations moved their packages out of
spinifex/handlers, which the S4, S6 and S7 network invariant scans still
walked, so those packages silently dropped out of the scans. Re-add their
new homes alongside domains/ecs. The checked rules are unchanged.
2026-10-26 16:35:15 +10:00
Josh Siviour 744a73ef1e refactor: relocate handlers/ecs to domains/ecs (#1271)
* refactor: relocate handlers/ecs to domains/ecs

Mechanical move per the ADR-0001 handlers/ecs row (files already
organised by resource). Package renamed handlers_ecs -> ecs; importers
alias ecsdomain. APIs, NATS subjects, storage and behaviour unchanged.
handlers is now a retired legacy root; ECS allowlist debt retargeted to
domains/ecs and R5 holds it to the interim daemon-import ban.

* docs: cite spinifex#1271 in the ECS relocation row

* fix: point network invariant scans at domains/ecs

The ADR-0007 S4, S6 and S7 scans walked spinifex/handlers, which no
longer exists after the ECS move. Scan domains/ecs, the same code.
2026-10-26 16:09:59 +10:00
Josh Siviour 2df87981f2 refactor: relocate handlers/elbv2 to domains/elbv2 (#1269)
* refactor: relocate handlers/elbv2 to domains/elbv2

Mechanical move per the amended ADR-0001 handlers/elbv2 row. Package
renamed handlers_elbv2 -> elbv2; importers alias elbv2domain. APIs,
NATS subjects, storage and behaviour unchanged. Layering debt entries
retargeted; Q-30 data-plane boundary stays deferred debt.

* docs: cite spinifex#1269 in the ELBv2 relocation row

* docs: match citation style in the ELBv2 relocation row
2026-10-26 15:23:17 +10:00
Josh Siviour 7fc79d95bd 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.
2026-10-26 14:34:13 +10:00
188 changed files with 660 additions and 657 deletions
+13 -13
View File
@@ -23,7 +23,7 @@ var allowlist = []allowance{
{From: "accountteardown", To: "domains/ec2/awsapi/volume", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
{From: "accountteardown", To: "domains/ec2/awsapi/vpc", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
{From: "agents/ecs/agent", To: "handlers/ecs", Debt: "agents/ecs/agent", Reason: "assignment/state wire types from the ECS implementation; needs a guest/controller contract"},
{From: "agents/ecs/agent", To: "domains/ecs", Debt: "agents/ecs/agent", Reason: "assignment/state wire types from the ECS implementation; needs a guest/controller contract"},
{From: "agents/eks/tokenwebhook", To: "domains/eks/cluster", Debt: "agents/eks/tokenwebhook", Reason: "WebhookTokenReviewResult from the EKS implementation; needs an EKS contract or projection"},
@@ -125,11 +125,11 @@ var allowlist = []allowance{
{From: "gateway", To: "domains/iam/awsapi", Debt: "gateway (service registration)", Reason: "IAM, STS, EKS and RDS dispatch stays in gateway until the ingress registration seam"},
{From: "gateway", To: "domains/rds/awsapi", Debt: "gateway (service registration)", Reason: "IAM, STS, EKS and RDS dispatch stays in gateway until the ingress registration seam"},
{From: "gateway", To: "domains/sts/awsapi", Debt: "gateway (service registration)", Reason: "IAM, STS, EKS and RDS dispatch stays in gateway until the ingress registration seam"},
{From: "handlers/ecs", To: "domains/ec2/ebs/metadata", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "handlers/ecs", To: "domains/ec2/eip", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "handlers/ecs", To: "domains/ec2/instancetypes", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "handlers/ecs", To: "handlers/elbv2", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "handlers/ecs", To: "domains/iam", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "domains/ecs", To: "domains/ec2/ebs/metadata", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "domains/ecs", To: "domains/ec2/eip", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "domains/ecs", To: "domains/ec2/instancetypes", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "domains/ecs", To: "domains/elbv2", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "domains/ecs", To: "domains/iam", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "domains/eks/cluster", To: "domains/dns", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
{From: "domains/eks/cluster", To: "domains/ec2/ebs/metadata", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
@@ -140,13 +140,13 @@ var allowlist = []allowance{
{From: "domains/eks/cluster", To: "domains/iam", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
{From: "domains/eks/cluster", To: "runtime/compute/vm", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
{From: "handlers/elbv2", To: "domains/acm", 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/dns", 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/ec2/systeminstance", 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/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: "domains/elbv2", To: "domains/acm", Debt: "domains/elbv2 (cross-domain)", Reason: "ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From: "domains/elbv2", To: "domains/dns", Debt: "domains/elbv2 (cross-domain)", Reason: "ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From: "domains/elbv2", To: "domains/ec2/systeminstance", Debt: "domains/elbv2 (cross-domain)", Reason: "ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From: "domains/elbv2", To: "domains/ec2/vpc", Debt: "domains/elbv2 (cross-domain)", Reason: "ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From: "domains/elbv2", To: "domains/network/topology", Debt: "domains/elbv2 (cross-domain)", Reason: "ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From: "domains/elbv2", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "system-instance role ensurer; needs an IAM capability"},
{From: "domains/elbv2", To: "agents/elbv2", Debt: "domains/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"},
+12 -8
View File
@@ -193,19 +193,23 @@ func TestADR0001_LayeringCheckerDetectsFaults(t *testing.T) {
}
// TestADR0001_InterimDaemonDirectionCoversRuntimeDaemon: the daemon left its
// legacy root, so the interim guardrail must still reject handlers and gateway
// importing it at runtime/daemon.
// legacy root, so gateway importing runtime/daemon must still be rejected, and
// the former handlers packages are held to it by R5 at their domain homes.
func TestADR0001_InterimDaemonDirectionCoversRuntimeDaemon(t *testing.T) {
r := ruleByID("Interim_DaemonGatewayHandlersDirection")
daemon, _ := classify("spinifex/runtime/daemon")
for _, from := range []string{"spinifex/gateway", "spinifex/handlers/ecs"} {
f, _ := classify(from)
if !r.subject(f, from) || !r.forbids(f, daemon, from) {
t.Errorf("%s -> runtime/daemon is not rejected", from)
checks := []struct{ rule, from string }{
{"Interim_DaemonGatewayHandlersDirection", "spinifex/gateway"},
{"R5_DomainImportsNoRuntimeOrOperator", "spinifex/domains/ecs"},
}
for _, c := range checks {
r := ruleByID(c.rule)
f, _ := classify(c.from)
if !r.subject(f, c.from) || !r.forbids(f, daemon, c.from) {
t.Errorf("%s -> runtime/daemon is not rejected by %s", c.from, c.rule)
}
}
other, _ := classify("spinifex/runtime/host/process")
if gw, _ := classify("spinifex/gateway"); r.forbids(gw, other, "spinifex/gateway") {
if gw, _ := classify("spinifex/gateway"); ruleByID("Interim_DaemonGatewayHandlersDirection").forbids(gw, other, "spinifex/gateway") {
t.Error("gateway -> runtime/host/process is rejected, but it is not the daemon")
}
}
+2 -2
View File
@@ -60,8 +60,8 @@ 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,
"gateway": true,
"accountteardown": true,
}
// gatewayDomains names the domain each gateway/<service> adapter belongs to;
+1 -4
View File
@@ -15,14 +15,11 @@ var frozenLegacyPackages = []string{
"gateway/ecs",
"gateway/elbv2",
"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", "handlers", "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"
)
+35 -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,13 @@ 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
│ └── 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 +321,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 two 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
+20 -16
View File
@@ -346,7 +346,10 @@ 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#1268` | `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. |
| `spinifex#1269` | `spinifex/handlers/elbv2` | `spinifex/domains/elbv2` (package `elbv2`) | A mechanical whole-package relocation under the 2026-10-26 ADR-0001 amendment: the ELBv2 control plane keeps its files, tests and current dependencies, and importers alias it `elbv2domain`. No identifier, API, NATS subject, KV bucket, metric, error string or behaviour changes. Its allowlist entries are now sourced from `domains/elbv2`. Q-30's data-plane boundary stays deferred, recorded as `agents/elbv2` (Q-30 data-plane boundary) debt. |
| `spinifex#1271` | `spinifex/handlers/ecs` | `spinifex/domains/ecs` (package `ecs`) | A mechanical whole-package relocation: ADR-0001's `handlers/ecs` row targets `domains/ecs`, the files are already organised by resource, and resource packages are introduced only where callers need a compiler boundary. The ECS control plane keeps its files, tests and current dependencies alongside the already-extracted `domains/ecs/taskdefinition`, and importers alias it `ecsdomain`. No identifier, API, NATS subject, KV bucket, metric, error string or behaviour changes. Its allowlist entries are now sourced from `domains/ecs`. `handlers` has no package left and is a retired legacy root; R5 holds `domains/ecs` to the interim rule's ban on importing the daemon. |
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,14 +423,15 @@ 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.
- `domains/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 `domains/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.
- `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.
- `runtime/roles/awsgw` → `handlers/elbv2` (inherited from `gateway/tagging`): this is a new physical import in AWSGW that inherits the former `gateway/tagging` dependency, because the `elbv2Lister` adapter builds its client with `handlers_elbv2.NewNATSELBv2Service` and calls the `ELBv2Service` interface. It is permitted temporarily because AWSGW is the composition root, which may wire legacy packages, so the layering test needs no allowlist row. Removal condition: it must clear when ELBv2 exposes a domain-owned boundary (its NATS client or contract under its domain owner) and the adapter consumes that instead.
- `runtime/roles/awsgw` → `domains/elbv2` (inherited from `gateway/tagging`): this is a new physical import in AWSGW that inherits the former `gateway/tagging` dependency, because the `elbv2Lister` adapter builds its client with `elbv2domain.NewNATSELBv2Service` and calls the `ELBv2Service` interface. AWSGW is the composition root, so the layering test needs no allowlist row. Removal condition: it must clear when ELBv2 exposes a domain-owned boundary (its NATS client or contract under its domain owner) and the adapter consumes that instead.
- `runtime/roles/vpcd` (placement): the role still mixes process/role supervision with network-controller responsibilities. `lease_reaper.go` (orphaned external-pool DHCP lease sweep), `ovn_controller_watchdog.go` (OVN controller liveness and restart), `host_eip_loop.go` (host-EIP reconcile loop) and `lease_datapath_reconcile.go` (lease datapath reconcile) are periodic network-control loops, not role wiring; `vpcd.go` itself also carries bridge-mode detection and verification, chassis-list parsing, external-CIDR resolution, DHCP manager startup and lease/IP-change handling, and gateway/host-EIP allocator and neighbour flush/prime construction alongside `Service.Start`. The future placement question is whether these belong in `domains/network`. Removal condition: an accepted decision on network-controller placement, and extraction of those loops and helpers behind the role.
- ECR control-plane registration: `awsapi.Deps.NATS` is a raw `*nats.Conn`, not a narrow capability. Every NATS-backed `Handler` passes it to `domains/ecr.NewNATSMetaStore`, which calls `natsmsg.NATSRequest`, so narrowing it means changing the exported `Handler` type, every handler and the meta store; it must not be broadened further meanwhile.
- ACM control-plane registration: `domains/acm/awsapi.Deps.NATS` (`registration.go`) is likewise a raw `*nats.Conn` carried by every `Handler`, for the same reason; it must not be broadened further meanwhile.
@@ -444,7 +448,7 @@ These residuals were recorded by individual slices and must be resolved once the
- `domains/ec2/instance`: imports `runtime/compute/vm` and `runtime/compute/gpu` (domain-to-runtime) and `domains/dns` (cross-domain implementation import).
- `domains/ec2/instance/state` (dependency): the vpcd IMDS local reader imports `runtime/compute/vm` to return `vm.VM`. Removal condition: that type moves into a domain-owned package that `runtime/compute/vm` consumes (runtime to domain is allowed).
- `agents/eks/tokenwebhook`: imports `domains/eks/cluster` for `WebhookTokenReviewResult`, an agent depending on a domain implementation. Temporary: the eventual EKS contract or projection for the token-review reply needs its own design, and no contract is invented to complete the move.
- `agents/ecs/agent`: takes its wire types from `contracts/ecs/v1`, and still imports `handlers/ecs` for the poll-assignment and GPU-report inputs and outputs and the instance-type and availability-zone attribute names. Temporary agent-to-domain debt; no contract is created during the relocation.
- `agents/ecs/agent`: takes its wire types from `contracts/ecs/v1`, and still imports `domains/ecs` for the poll-assignment and GPU-report inputs and outputs and the instance-type and availability-zone attribute names. Temporary agent-to-domain debt; no contract is created during the relocation.
- `agents/rds/agent` (wire contract): imports `domains/rds/instance` for the command, reply, parameter, health and bootstrap types exchanged with the control plane. Temporary agent-to-domain debt; no contract is created during the relocation. The earlier `LookupEngine` dependency on the RDS domain's engine registry has since cleared: `controlPlaneRulesFrom` and both engine constructors now take the agent-declared `EngineCatalog` (see the narrowed entry below), with zero production uses of `LookupEngine` remaining in `agents/rds/agent`.
- `operator/installer/firstboot` (security): `Config.InstallCallback` is rendered into `spinifex-firstboot.sh`, which is world-readable (0755). The field is currently unused (`fireInstallCallback` reads `SPINIFEX_INSTALL_CALLBACK` from the environment instead), but any future wiring must not place secrets or tokenised callback URLs in the generated script; sensitive callback credentials should reach first boot through a protected channel such as a 0600 file or a credential store.
- `operator/cli` (known security risk): `spx admin join` contacts the formation server with `InsecureSkipVerify`, sends the join token as a bearer credential and receives the cluster CA certificate and key, master key, Viperblock key and shared credentials, so an on-path attacker during formation can capture the token and the cluster root secrets or substitute a CA. This predates the move; lint surfaced it when the tree left `cmd/`. It is accepted for now and carries a line-level gosec annotation stating it is not a false positive. Removal condition: authenticate formation TLS with a pinned key or certificate delivered with the join material, or an out-of-band CA, with tests proving a mismatched server certificate fails before credentials are sent.
@@ -464,17 +468,17 @@ These residuals were recorded by individual slices and must be resolved once the
Enforcement: the owner decided the layering test is blocking now, with a ratchet. Its allowlist mirrors this debt list exactly, each edge carrying its owner and removal condition; any new unrecorded forbidden edge fails local preflight and CI, and an allowlisted edge that no longer exists must be removed from the list.
- `domains/rds/instance` → `domains/rds/subnetgroup` (legacy edges): `instanceReferences` binds `Dependants` by scanning instance records, and `tags.go` writes the group record through its generic tag registry. The first clears when the RDS instance gains its own owner and answers `Dependants` from it. The second clears when tagging invokes a subnet-group-owned tag/untag capability rather than writing the record; moving the tagging adapter to `domains/rds/awsapi` alone does not clear it.
- `domains/rds/instance` → `domains/rds/parametergroup` (legacy edges): `parameterGroupReferences` binds `Dependants` by scanning instance records; this clears when the RDS instance gains its own owner and answers `Dependants` from it. `tags.go` writes the meta record through its generic tag registry; this clears when tagging invokes a parameter-group-owned tag mutation capability, and moving the tagging adapter to `domains/rds/awsapi` alone does not clear it. The Modify handler still calls `propagateParameterGroup` after the owner's write; propagation is instance-side realization and stays legacy until it is designed as asynchronous realization (group generation, instance staleness, retry). `parameterGroupValues` returns the persisted `Record` and overrides to instance code; when the instance owner is extracted, replace them with a consumer-owned resolved-parameters projection.
- `handlers/ecs` → `domains/ecs/taskdefinition` (legacy edges): `tags.go` reads and writes the revision record through its generic tag registry; this clears when ECS tagging invokes an owner-provided tag mutation capability, and moving `tags.go` to `domains/ecs/awsapi` alone does not clear it. `taskDefinitionResolver` returns the persisted `Record` to task and service launch code; when the task and service owners are extracted, replace the record crossing that boundary with a consumer-owned resolved task-definition (launch-spec) projection. Moving packages has not made the record private.
- `domains/ecs/taskdefinition` (deliberate duplication, not debt): private `getJSON`, `putJSON` and `keysWithPrefix` copy the raw-KV helpers in `handlers/ecs` so the extraction keeps ECS's get/put/list error and recovery semantics. Revisit only if a later, tested state abstraction proves it preserves those semantics; do not migrate ECS to `kvstore` merely to remove them.
- `domains/ecs` → `domains/ecs/taskdefinition` (owner bypass): `tags.go` reads and writes the revision record through its generic tag registry; this clears when ECS tagging invokes an owner-provided tag mutation capability, and moving `tags.go` to `domains/ecs/awsapi` alone does not clear it. `taskDefinitionResolver` returns the persisted `Record` to task and service launch code; when the task and service owners are extracted, replace the record crossing that boundary with a consumer-owned resolved task-definition (launch-spec) projection. Moving packages has not made the record private.
- `domains/ecs/taskdefinition` (deliberate duplication, not debt): private `getJSON`, `putJSON` and `keysWithPrefix` copy the raw-KV helpers in `domains/ecs` so the extraction keeps ECS's get/put/list error and recovery semantics. Revisit only if a later, tested state abstraction proves it preserves those semantics; do not migrate ECS to `kvstore` merely to remove them.
- `domains/eks/cluster` → `domains/eks/access` (legacy edges): `ResolveTokenReview` opens the EKS bucket in the gateway process and reads records through the exported `access.Get`; this clears when token review asks the owner across a contract rather than opening the bucket itself. `DeleteClusterPrefix` erases access-entry keys as part of its blanket cluster sweep; this clears when cluster delete invokes an owner-provided removal of the cluster's entries. The owner returns the persisted `Record` to the AWS adapter and token review; replace it with consumer-owned projections when those consumers are extracted.
- `domains/eks/cluster` → `domains/eks/addon` (legacy edges): `DeleteClusterPrefix` erases add-on records and manifests in its blanket cluster sweep; this clears when cluster delete invokes an add-on-owned teardown capability. `stageGPUDeviceAddon` is a temporary node-group-to-add-on orchestration edge: best-effort, errors logged and swallowed, never undone on node-group delete; it clears when the node-group owner is extracted and the dependency is designed. `ClusterReconciler` hosts the add-on status subscription; it clears when add-on delivery observation has its own host under the lifecycle correction. Cluster existence is checked in the handler, not through an owner-declared capability; it moves into the owner when cluster admission is defined (today status is ignored). Guest wire types (`AddonStatusReport`, `AddonDeliveryPhase`, `AddonStatusSubject`, `StagedAddonManifest`, `InternalAddonsResponse`) are owned by `contracts/eks/v1`, which `domains/eks/cluster`, `gateway/eks` and `agents/eks/gatewayfetch` import directly.
- `domains/eks/cluster` and `domains/eks/awsapi` → `domains/eks/addon` (further edges): the AWS adapter reads the persisted `Record` through `addon.Get`/`addon.List`; this clears when the owner returns a view and the add-on adapter moves to `domains/eks/awsapi`. The guest route reaches the owner through the `handlers_eks` NATS RPC (`ListStagedAddonManifests`); this clears with per-resource commands behind EKS registration. The add-on ARN is derived in both the owner and `domains/eks/awsapi/authz.go`; this clears when authorization obtains the ARN from the owner, which an incarnation suffix would require. The owner's `Manifest`/`Report` beside the `contracts/eks/v1` types is deliberate duplication, not debt.
- `agents/rds/agent` (dependency, narrowed): engine metadata now arrives through the agent-declared `EngineCatalog`, but the agent still imports `domains/rds/instance` for the command protocol, wire and record types (`Command*`, `Parameter`, `EngineHealth*`, `BootstrapMode*`) and API output types. This clears when those move behind a guest/controller wire contract.
- `domains/rds/awsapi` → `domains/rds/instance` (`InstanceSizing`): the gateway obtains its `Sizing` through the RDS handler package's adapter rather than its own composition root. This clears when the gateway's RDS composition moves to `runtime/roles/awsgw` and builds `Sizing` there.
- `domains/admission/quota` → `domains/ec2/instancetypes`: `reconcile.go`, `records.go` and `vcpu.go` read `DefaultVCPUs` to size instances without a recorded vCPU count. This clears when quota receives vCPU sizing through a consumer-owned capability or an EC2 projection.
- IAM implementation consumers in STS, Ochre and ELBv2: `domains/sts` imports `domains/iam` across five files (`IAMService`, `SessionRevoker`, `EncryptSecret`/`DecryptSecret`, `AccessKeyStatusActive`, `ValidateTrustPolicyDocument`, `PolicyEffectAllow`/`PolicyEffectDeny`, `IAMAccountBucketName`, `OIDCProviderARN`, `OIDCProviderKey`), not only `oidc_jwks.go`; `domains/sts/awsapi/GetCallerIdentity.go` takes `IAMService`; `domains/ochre/credentials.go` uses `EncryptSecret`/`DecryptSecret`; and `handlers/elbv2/{lb_role,service_impl}.go` use `EnsureSystemInstanceProfile` and `SystemInstanceRoleEnsurer`. Each clears when its consumer receives a narrow IAM capability, or a shared crypto boundary for the secret helpers, rather than importing the IAM implementation.
- `handlers/elbv2` (cross-domain): `service_impl.go` imports `domains/acm` (`Store`, `NewStore`), `domains/dns` (`ELBChanges`, `ELBName`, `Change`, `ActionUpsert`, `ResolveBaseDomain`), `domains/ec2/vpc` (`VPCServiceImpl`) and `domains/network/topology` (`Port`), and `launcher.go` re-exports `domains/ec2/systeminstance` launch types as aliases. These clear when ELBv2 is extracted and each edge becomes a consumer-owned capability, which ADR-0001 sequences after the ELBv2 data-plane boundary is settled.
- `docs/service-interfaces.yaml` (ECS coverage gap, closed): the traced `ecs.bus.*` family and the ECS AWS-API action subjects are both listed. The action subjects are declared as `ecs.*`, the same wildcard convention as `rds.*`: `handlers/ecs/service_nats.go` publishes them from the gateway and `daemon/daemon.go` subscribes to them. Declaring `ecs.bus.*` alone had made `ecs` a managed prefix, so `manifest-lint` failed CI on all 34 undeclared action subjects until this was added.
- IAM implementation consumers in STS, Ochre and ELBv2: `domains/sts` imports `domains/iam` across five files (`IAMService`, `SessionRevoker`, `EncryptSecret`/`DecryptSecret`, `AccessKeyStatusActive`, `ValidateTrustPolicyDocument`, `PolicyEffectAllow`/`PolicyEffectDeny`, `IAMAccountBucketName`, `OIDCProviderARN`, `OIDCProviderKey`), not only `oidc_jwks.go`; `domains/sts/awsapi/GetCallerIdentity.go` takes `IAMService`; `domains/ochre/credentials.go` uses `EncryptSecret`/`DecryptSecret`; and `domains/elbv2/{lb_role,service_impl}.go` use `EnsureSystemInstanceProfile` and `SystemInstanceRoleEnsurer`. Each clears when its consumer receives a narrow IAM capability, or a shared crypto boundary for the secret helpers, rather than importing the IAM implementation.
- `domains/elbv2` (cross-domain): `service_impl.go` imports `domains/acm` (`Store`, `NewStore`), `domains/dns` (`ELBChanges`, `ELBName`, `Change`, `ActionUpsert`, `ResolveBaseDomain`), `domains/ec2/vpc` (`VPCServiceImpl`) and `domains/network/topology` (`Port`), and `launcher.go` re-exports `domains/ec2/systeminstance` launch types as aliases. These clear when each edge becomes a consumer-owned capability.
- `docs/service-interfaces.yaml` (ECS coverage gap, closed): the traced `ecs.bus.*` family and the ECS AWS-API action subjects are both listed. The action subjects are declared as `ecs.*`, the same wildcard convention as `rds.*`: `domains/ecs/service_nats.go` publishes them from the gateway and `runtime/daemon/daemon.go` subscribes to them. Declaring `ecs.bus.*` alone had made `ecs` a managed prefix, so `manifest-lint` failed CI on all 34 undeclared action subjects until this was added.
- `contracts/ecs/v1` (dead routes): four of five bus subjects have no producer. The scheduler leader still subscribes to `instance-register`, `instance-heartbeat` and `task-state`, which can never fire, and `assign` has neither producer nor consumer; only `service-reconcile` is live. Removal condition: an owner decision to retire these routes (a successor contract under the compatibility rules) or to adopt them in the agent.
The layering test enforcing these rules (`architecture/layering`, a module-root repository governance test, not Spinifex product code; run by `make test`, `test-cover` and `test-race`) is blocking, with a ratchet: the debt recorded in this section is narrowly allow-listed by owner and removal condition, and any new, unrecorded forbidden edge fails local preflight and CI. The allow-list mirrors the edges in this section that break an import rule; same-domain legacy edges, placement residuals and security notes are not import-rule violations and have no allow-list entry.
@@ -661,15 +665,16 @@ series above; "needs decision" is open.
|---|---|---|
| `handlers/iam` | — | Done. The implementation moved to `domains/iam` (package `iam`) in `spinifex#1249` with no API, NATS subject, storage or behaviour change. ADR-0001's row says a shared IAM contract is not inferred from current KV readers, and the move adds none. The cross-domain readers (the EC2 instance and spot-instance AWS adapters, `domains/sts`, `domains/sts/awsapi`, `domains/ochre`, the ELBv2 system-instance role path and ECR/Ochre signing-key crypto) still import the implementation and each still needs an IAM capability; that is recorded dependency-rule debt, with its allowlist entries now targeting `domains/iam`, not a prerequisite to relocating IAM's implementation. |
| `handlers/sts` | — | Done. The implementation moved to `domains/sts` (package `sts`) in `spinifex#1250` with no API, NATS subject, storage or behaviour change. ADR-0001's condition, cutting the EKS dependency, was already met through the injected `ClusterJWKSResolver`. `domains/sts` still imports `domains/iam` directly; per-consumer IAM capabilities remain dependency-rule debt, with its allowlist entry now sourced from `domains/sts`. |
| `handlers/ecs` | Task, service and capacity resources (`taskdefinition` already extracted to `domains/ecs/taskdefinition`). | Deferred: ADR-0004 blocked-dependency list — `iam:PassRole` in the gateway adapter and EC2/ELBv2/EIP adapters called as internal APIs. |
| `handlers/ecs` | — | Done. The ECS control plane moved to `domains/ecs` (package `ecs`) with no API, NATS subject, storage or behaviour change, alongside the already-extracted `domains/ecs/taskdefinition`. The ADR-0004 blocked-dependency list (`iam:PassRole` in the gateway adapter and EC2/ELBv2/EIP adapters called as internal APIs) remains dependency-rule debt, with its allowlist entries now sourced from `domains/ecs`. |
| `handlers/eks` | — | Done for the relocation: the remaining EKS implementation moved to `domains/eks/cluster` (package `cluster`) in `spinifex#1264` with no API, NATS subject, storage or behaviour change, alongside the already-extracted `access` and `addon` packages. Deferred follow-on: nodegroup service extraction. The nodegroup operations are methods on the shared `EKSServiceImpl` and use its private state, so moving them to their own resource package needs a nodegroup owner and interfaces, which is decomposition work, not a move. The ADR-0004 blocked-dependency imports (IAM role/instance-profile creation, Bluebottle `auth`, the NATS CA, and EKS identity: token review, OIDC) remain dependency-rule debt, with their allowlist entries now sourced from `domains/eks/cluster`. |
| `handlers/rds` | — | Done. The remaining RDS instance resource moved to `domains/rds/instance` (package `instance`) in `spinifex#1251` with no API, NATS subject, storage or behaviour change, fulfilling ADR-0001's organise-by-resource condition alongside the already-extracted subnet group, parameter group and engine catalogue. The ADR-0004 blocked-dependency imports (quota admission, the duplicated expected-node fan-out, the IAM ensurer and the Bluebottle master key) remain dependency-rule debt, with their allowlist entries now sourced from `domains/rds/instance`. |
| `handlers/elbv2` | Everything; not yet touched. | Deferred: ADR-0001 sequences this after the ELBv2 data-plane boundary is settled. Needs decision: what evidence establishes that boundary is settled. |
| `handlers/elbv2` | — | Done. The implementation moved to `domains/elbv2` (package `elbv2`) with no API, NATS subject, storage or behaviour change. Q-30's data-plane boundary remains deferred debt; the amended ADR-0001 row makes it not a relocation prerequisite. Its cross-domain, IAM and `agents/elbv2` imports remain dependency-rule debt, with their allowlist entries now sourced from `domains/elbv2`. |
| `handlers/iam/mock` | — | Done. Moved with the implementation to `domains/iam/mock`. |
| `gateway` (root) | Generic HTTP route assembly, SigV4/registry authentication, the policy gate, request-audit middleware, request-context keys, and the legacy per-service dispatch switch for every service not yet registered. | Deferred: the ADR-0002 phases above (auth/IAM and request-context boundary design) gate moving the audit, context-key and legacy-dispatch mechanics. |
| `gateway/bedrock` | — | Done. The whole package moved into `domains/ochre` in `spinifex#1266` with its package-private core and tests intact and no behaviour change. Separating the action adapters into `domains/ochre/awsapi` remains deferred, recorded as `domains/ochre` (action adapters) debt above. |
| `gateway/{iam,sts,eks,rds}` | — | Done. The action adapters moved to `domains/{iam,sts,eks,rds}/awsapi` in `spinifex#1265` with no API, NATS subject, storage or behaviour change. ADR-0001 does not make ADR-0002 registration a prerequisite for relocating a domain's adapters, as the EC2 adapter moves already showed; the root `gateway` dispatch that still imports them is recorded `gateway` (service registration) debt. |
| `gateway/{ecs,elbv2}` | Each service's AWS action adapters and dispatch, not yet registered through ADR-0002. | Deferred: each follows its `handlers/<svc>`, because a new-path `awsapi` cannot import a legacy handler. |
| `gateway/ecs` | The ECS AWS action adapters and dispatch, not yet registered through ADR-0002. | Not moved. Its handler is now `domains/ecs`, so the follows-its-handler blocker no longer applies. |
| `gateway/elbv2` | The ELBv2 AWS action adapters and dispatch, not yet registered through ADR-0002. | Not moved. Its handler is now `domains/elbv2`, so the follows-its-handler blocker no longer applies. |
| `gateway/spx` | `promote_image.go` and `spx.go` dispatch (`storage.go` already consumes `contracts/operator/v1`). | Needs decision: which package owns operator-plane action dispatch registration — an `ingress/aws`-style seam for the operator plane, or `gateway` stays its home. |
| `gateway/tagging` | — | Done. `domains/tagging` owns the read-only cross-resource `GetResources` aggregation and its API semantics (`0d830e901`, `aedd0b2c4`); AWSGW composes the owner listers, the EC2 tag index and ELBv2 (`f3545e3ea`); the root `gateway` keeps only the legacy dispatch in `gateway/tagging.go`. The package is removed, and its two allowlist rows and frozen-legacy entry are gone. Owner tag mutation capabilities remain future direction, so the RDS and ECS tag-writing edges above stay open. |
| `services/viperblockd` | — | Done. The adapter is `providers/ebs/viperblock` (with the read-only `dirty` reader), the shared vocabulary is `contracts/viperblockd/legacy/v1`, and the role is `runtime/roles/viperblockd`. `services/` is empty. |
@@ -678,21 +683,20 @@ 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 `domains/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`.
Two legacy roots remain: `accountteardown` and `gateway`.
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. |
| Broader boundary or follows another package | `accountteardown` (domain cleanup capabilities); `gateway` root (ADR-0002 auth and request-context phase) | Recorded debt with removal conditions above. |
Per-consumer IAM capabilities are the widest unblocker: they gate STS's IAM reads, Ochre's IAM reads, the ELBv2 role path and the ECS, EKS and RDS ensurers.
+5 -5
View File
@@ -74,10 +74,10 @@ services:
- spinifex/domains/ec2/volume
- spinifex/domains/ec2/volumestate
- spinifex/domains/ec2/vpc
- spinifex/handlers/elbv2
- spinifex/domains/elbv2
- spinifex/domains/acm
- spinifex/domains/iam
- spinifex/handlers/ecs
- spinifex/domains/ecs
- spinifex/domains/rds/instance
- spinifex/domains/rds/subnetgroup
- spinifex/domains/rds/parametergroup
@@ -136,7 +136,7 @@ services:
- "spinifex.cluster.shutdown.infra"
# Operator-plane Predastore topology contract (contracts/operator/v1).
- "spinifex.storage.config"
# ELBv2 control plane (handlers/elbv2/service_nats.go topic map).
# ELBv2 control plane (domains/elbv2/service_nats.go topic map).
- "elbv2.CreateLoadBalancer"
- "elbv2.DeleteLoadBalancer"
- "elbv2.DescribeLoadBalancers"
@@ -163,7 +163,7 @@ services:
# per-subject list would be a hand-mirror of that file with nothing
# checking it.
- "rds.*"
# ECS control plane (handlers/ecs/service_nats.go, registered in
# ECS control plane (domains/ecs/service_nats.go, registered in
# daemon.go). One wildcard, as for RDS: every action subject is a literal
# in those two files, so a per-subject list would only mirror them.
- "ecs.*"
@@ -403,7 +403,7 @@ suites:
e2e-lb:
path: tests/e2e/lb
covers: [awsgw, spinifex_daemon, vpcd, viperblockd, predastore]
# Exercises full ELBv2 control plane (handlers/elbv2) + data plane
# Exercises full ELBv2 control plane (domains/elbv2) + data plane
# (LB agent on the system VM). ELBv2 is part of spinifex_daemon in
# this manifest; revisit if it splits into a standalone service.
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecs"
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
"github.com/nats-io/nats.go"
)
@@ -18,7 +18,7 @@ import (
// leaves an INACTIVE record that still lists, so a stage waiting for services
// to disappear would never drain.
func ECSReapers(nc *nats.Conn) []Reaper {
return []Reaper{&ecsClusterReaper{svc: handlers_ecs.NewNATSECSService(nc)}}
return []Reaper{&ecsClusterReaper{svc: ecsdomain.NewNATSECSService(nc)}}
}
// ecsClusterReaper removes the account's ECS clusters.
@@ -27,7 +27,7 @@ func ECSReapers(nc *nats.Conn) []Reaper {
// so the instance reaper takes them — but only after this one has stopped the
// tasks riding on them, which is why this is registered first.
type ecsClusterReaper struct {
svc handlers_ecs.ECSService
svc ecsdomain.ECSService
}
func (r *ecsClusterReaper) Kind() string { return "ecs-cluster" }
+2 -2
View File
@@ -13,7 +13,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/mulgadc/spinifex/internal/testkit"
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -94,7 +94,7 @@ func (c *fakeECS) request(t *testing.T, subject string, into any) {
}
func newECSReaper(nc *nats.Conn) *ecsClusterReaper {
return &ecsClusterReaper{svc: handlers_ecs.NewNATSECSService(nc)}
return &ecsClusterReaper{svc: ecsdomain.NewNATSECSService(nc)}
}
// The listing is keyed by cluster name, which is the identity ECS takes back
+4 -4
View File
@@ -5,7 +5,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/elbv2"
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
elbv2domain "github.com/mulgadc/spinifex/spinifex/domains/elbv2"
"github.com/nats-io/nats.go"
)
@@ -15,7 +15,7 @@ import (
// groups, so it has to go before either can be deleted. Target groups follow
// their balancers because a group still referenced by a listener refuses.
func ELBv2Reapers(nc *nats.Conn) []Reaper {
svc := handlers_elbv2.NewNATSELBv2Service(nc)
svc := elbv2domain.NewNATSELBv2Service(nc)
return []Reaper{
&loadBalancerReaper{svc: svc},
&targetGroupReaper{svc: svc},
@@ -23,7 +23,7 @@ func ELBv2Reapers(nc *nats.Conn) []Reaper {
}
type loadBalancerReaper struct {
svc handlers_elbv2.ELBv2Service
svc elbv2domain.ELBv2Service
}
func (r *loadBalancerReaper) Kind() string { return "load-balancer" }
@@ -58,7 +58,7 @@ func (r *loadBalancerReaper) Delete(ctx context.Context, accountID string, resou
}
type targetGroupReaper struct {
svc handlers_elbv2.ELBv2Service
svc elbv2domain.ELBv2Service
}
func (r *targetGroupReaper) Kind() string { return "target-group" }
@@ -10,7 +10,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/elbv2"
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
elbv2domain "github.com/mulgadc/spinifex/spinifex/domains/elbv2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -18,7 +18,7 @@ import (
// The embedded interface is nil, so a reaper reaching for anything beyond the
// four methods below panics rather than passing.
type fakeELBv2 struct {
handlers_elbv2.ELBv2Service
elbv2domain.ELBv2Service
loadBalancers []*elbv2.LoadBalancer
targetGroups []*elbv2.TargetGroup
+6 -6
View File
@@ -12,7 +12,7 @@ import (
ecsv1 "github.com/mulgadc/spinifex/contracts/ecs/v1"
"github.com/mulgadc/spinifex/internal/ecsgw"
"github.com/mulgadc/spinifex/spinifex/agents/ecs/credentials"
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
)
// controlPlane is the agent's channel to the AWS gateway. It replaces the direct
@@ -31,7 +31,7 @@ type controlPlane interface {
PollAssignments(cluster, instance string, ackAssigns, ackStops []string) ([]ecsv1.Assign, []ecsv1.StopDirective, error)
// ReportTaskGPU reports the local ledger's pinned device UUIDs for a task's
// containers, so the control plane can populate DescribeTasks gpuIds.
ReportTaskGPU(cluster, task string, containers []handlers_ecs.ContainerGPUReport) error
ReportTaskGPU(cluster, task string, containers []ecsdomain.ContainerGPUReport) error
}
// gatewayControlPlane implements controlPlane over the SigV4 ECS gateway client.
@@ -125,7 +125,7 @@ func (g *gatewayControlPlane) SubmitTaskState(st ecsv1.TaskState) error {
}
func (g *gatewayControlPlane) PollAssignments(cluster, instance string, ackAssigns, ackStops []string) ([]ecsv1.Assign, []ecsv1.StopDirective, error) {
in := &handlers_ecs.PollAssignmentsInput{
in := &ecsdomain.PollAssignmentsInput{
Cluster: cluster,
ContainerInstance: instance,
AckTaskIDs: ackAssigns,
@@ -139,15 +139,15 @@ func (g *gatewayControlPlane) PollAssignments(cluster, instance string, ackAssig
if err != nil {
return nil, nil, fmt.Errorf("poll: %w", err)
}
var out handlers_ecs.PollAssignmentsOutput
var out ecsdomain.PollAssignmentsOutput
if err := json.Unmarshal(resp, &out); err != nil {
return nil, nil, fmt.Errorf("decode poll: %w", err)
}
return out.Assignments, out.Stops, nil
}
func (g *gatewayControlPlane) ReportTaskGPU(cluster, task string, containers []handlers_ecs.ContainerGPUReport) error {
in := &handlers_ecs.ReportTaskGPUInput{Cluster: cluster, Task: task, Containers: containers}
func (g *gatewayControlPlane) ReportTaskGPU(cluster, task string, containers []ecsdomain.ContainerGPUReport) error {
in := &ecsdomain.ReportTaskGPUInput{Cluster: cluster, Task: task, Containers: containers}
body, err := json.Marshal(in)
if err != nil {
return fmt.Errorf("marshal report-task-gpu: %w", err)
@@ -10,7 +10,7 @@ import (
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/mulgadc/spinifex/spinifex/agents/ecs/credentials"
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
)
// fixedCredsProvider is a static credentials.CredentialsProvider for tests
@@ -101,7 +101,7 @@ func TestGatewayControlPlane_Register_NoGPUOmitsResource(t *testing.T) {
}
// ReportTaskGPU posts the per-container device UUIDs under the ReportTaskGPU
// action, decodable as the handlers_ecs internal shape.
// action, decodable as the domains/ecs internal shape.
func TestGatewayControlPlane_ReportTaskGPU(t *testing.T) {
var gotTarget string
var body []byte
@@ -113,7 +113,7 @@ func TestGatewayControlPlane_ReportTaskGPU(t *testing.T) {
defer srv.Close()
cp := newTestGatewayControlPlane(t, srv)
err := cp.ReportTaskGPU("web", "t-1", []handlers_ecs.ContainerGPUReport{
err := cp.ReportTaskGPU("web", "t-1", []ecsdomain.ContainerGPUReport{
{Name: "trainer", GPUIDs: []string{"GPU-aaa"}},
})
if err != nil {
@@ -123,7 +123,7 @@ func TestGatewayControlPlane_ReportTaskGPU(t *testing.T) {
t.Fatal("no X-Amz-Target header sent")
}
var in handlers_ecs.ReportTaskGPUInput
var in ecsdomain.ReportTaskGPUInput
if err := json.Unmarshal(body, &in); err != nil {
t.Fatalf("decode body: %v", err)
}
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecs"
ecsv1 "github.com/mulgadc/spinifex/contracts/ecs/v1"
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
)
// identity is the container instance's stable identity, assembled at boot from
@@ -34,8 +34,8 @@ func instanceAttributes(id identity) []*ecs.Attribute {
}
attrs = append(attrs, &ecs.Attribute{Name: aws.String(name), Value: aws.String(value)})
}
add(handlers_ecs.AttrInstanceType, id.InstanceType)
add(handlers_ecs.AttrAvailabilityZone, id.AZ)
add(ecsdomain.AttrInstanceType, id.InstanceType)
add(ecsdomain.AttrAvailabilityZone, id.AZ)
return attrs
}
@@ -9,7 +9,7 @@ import (
"time"
ecsv1 "github.com/mulgadc/spinifex/contracts/ecs/v1"
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
)
// fakeCP is a test double for the gateway control-plane. It records registers,
@@ -37,7 +37,7 @@ type fakeCP struct {
type gpuReport struct {
cluster string
task string
containers []handlers_ecs.ContainerGPUReport
containers []ecsdomain.ContainerGPUReport
}
var _ controlPlane = (*fakeCP)(nil)
@@ -93,7 +93,7 @@ func (f *fakeCP) PollAssignments(_, _ string, ackAssigns, ackStops []string) ([]
return out, stops, nil
}
func (f *fakeCP) ReportTaskGPU(cluster, task string, containers []handlers_ecs.ContainerGPUReport) error {
func (f *fakeCP) ReportTaskGPU(cluster, task string, containers []ecsdomain.ContainerGPUReport) error {
f.mu.Lock()
defer f.mu.Unlock()
f.gpuReports = append(f.gpuReports, gpuReport{cluster: cluster, task: task, containers: containers})
+3 -3
View File
@@ -11,7 +11,7 @@ import (
ecsv1 "github.com/mulgadc/spinifex/contracts/ecs/v1"
ctrruntime "github.com/mulgadc/spinifex/spinifex/agents/ecs/runtime"
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
)
// defaultStopTimeout is applied when a container carries no explicit
@@ -413,10 +413,10 @@ func (a *Agent) reportTaskState(as *ecsv1.Assign, status, reason string, contain
// container in the report that has them, so DescribeTasks can surface gpuIds.
// A no-op when nothing was pinned (the common non-GPU-task case).
func (a *Agent) reportTaskGPUs(taskID string, containers []ecsv1.ContainerStatus) {
reports := make([]handlers_ecs.ContainerGPUReport, 0, len(containers))
reports := make([]ecsdomain.ContainerGPUReport, 0, len(containers))
for _, c := range containers {
if len(c.GPUIDs) > 0 {
reports = append(reports, handlers_ecs.ContainerGPUReport{Name: c.Name, GPUIDs: c.GPUIDs})
reports = append(reports, ecsdomain.ContainerGPUReport{Name: c.Name, GPUIDs: c.GPUIDs})
}
}
if len(reports) == 0 {
@@ -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"
@@ -57,7 +57,7 @@ const (
registrationGrace = 60 * time.Second
)
// Data-plane engine names (duplicated from handlers/elbv2 to avoid an import cycle).
// Data-plane engine names (duplicated from domains/elbv2 to avoid an import cycle).
const (
EngineHAProxy = "haproxy"
EngineNginx = "nginx"
@@ -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
@@ -166,7 +166,7 @@ func WithRenewalClock(now func() time.Time) WorkerOption {
// NewWorker builds a renewal worker over svc. holderID identifies this node
// in the per-certificate lease (the daemon passes its node ID, mirroring
// handlers_ecs.NewScheduler's holder identity).
// ecs.NewScheduler's holder identity).
func NewWorker(svc *ACMServiceImpl, holderID string, opts ...WorkerOption) *Worker {
w := &Worker{
svc: svc,
+2 -2
View File
@@ -82,7 +82,7 @@ type ACMServiceImpl struct {
// SAN) by deriveValidationMode. Wired by the daemon post-construction
// (mirroring CertMaterialUpdated below) to domains/dns.HostsZone. A func
// field stands in for a direct import of domains/dns, the same pattern
// CertMaterialUpdated uses for handlers/elbv2, so this package and its
// CertMaterialUpdated uses for domains/elbv2, so this package and its
// tests stay decoupled from domains/dns's S3/northstar-config
// dependencies. Nil means "northstar hosts nothing" — the safe default for
// tests and any deployment that never wires it, so an unwired service can
@@ -91,7 +91,7 @@ type ACMServiceImpl struct {
// CertMaterialUpdated, when set, is invoked after new certificate material
// is written under an existing ARN so the caller can re-render every load
// balancer that references it (see handlers/elbv2.UpdateStoredConfigForCert).
// balancer that references it (see domains/elbv2.UpdateStoredConfigForCert).
// Wired by the daemon post-construction to avoid an elbv2 -> acm import
// cycle; nil-safe — skipped in tests and whenever ELBv2 isn't wired up.
// Fan-out errors are logged, never propagated: a rendering problem on one
+2 -2
View File
@@ -106,7 +106,7 @@ type CertRecord struct {
ImportedAt time.Time `json:"imported_at"`
Tags map[string]string `json:"tags,omitempty"`
// InUseBy is the set of load balancer ARNs whose listeners currently
// reference this certificate. Maintained by handlers/elbv2 as listeners are
// reference this certificate. Maintained by domains/elbv2 as listeners are
// created, modified and deleted; also surfaces as the public
// CertificateDetail.InUseBy field.
InUseBy []string `json:"in_use_by,omitempty"`
@@ -443,7 +443,7 @@ const maxInUseByCASAttempts = 50
// No-op if the certificate does not exist or already lists resourceArn.
//
// InUseBy is the sole mechanism by which a re-imported certificate reaches
// the data plane (see UpdateStoredConfigForCert in handlers/elbv2), so a lost
// the data plane (see UpdateStoredConfigForCert in domains/elbv2), so a lost
// update here is not a cosmetic race: it silently drops a load balancer from
// fan-out, and that load balancer's certificate expires in HAProxy while ACM
// still reports it as renewed. Two listeners on different load balancers can
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"github.com/mulgadc/spinifex/spinifex/domains/ecs/taskdefinition"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"github.com/mulgadc/spinifex/spinifex/domains/ecs/taskdefinition"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"fmt"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -2,7 +2,7 @@
// the package before a task reaches the API surface.
//
//test:in-package — placeTask and InstanceRecord's slot arithmetic are
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import "context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,24 +1,24 @@
package handlers_ecs_test
package ecs_test
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecs"
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// listInstanceARNs lists a cluster's container instances at one status, or at
// every status when the filter is empty.
func listInstanceARNs(t *testing.T, svc *handlers_ecs.Service, cluster, status string) []string {
func listInstanceARNs(t *testing.T, svc *ecsdomain.Service, cluster, status string) []string {
t.Helper()
in := &ecs.ListContainerInstancesInput{Cluster: aws.String(cluster)}
if status != "" {
in.Status = aws.String(status)
}
out, err := svc.ListContainerInstances(t.Context(), in, handlers_ecs.TestAccountID)
out, err := svc.ListContainerInstances(t.Context(), in, ecsdomain.TestAccountID)
require.NoError(t, err)
return aws.StringValueSlice(out.ContainerInstanceArns)
}
@@ -27,24 +27,24 @@ func listInstanceARNs(t *testing.T, svc *handlers_ecs.Service, cluster, status s
// find one that can take work, and a cluster carrying a drained node reads as
// if every node were live.
func TestListContainerInstances_StatusSelectsOnlyThatStatus(t *testing.T) {
svc, _ := handlers_ecs.NewTestService(t)
_, err := svc.CreateCluster(t.Context(), &ecs.CreateClusterInput{ClusterName: aws.String("web")}, handlers_ecs.TestAccountID)
svc, _ := ecsdomain.NewTestService(t)
_, err := svc.CreateCluster(t.Context(), &ecs.CreateClusterInput{ClusterName: aws.String("web")}, ecsdomain.TestAccountID)
require.NoError(t, err)
handlers_ecs.RegisterInstance(t, svc, "web", "i-live", 1024, 2048)
handlers_ecs.RegisterInstance(t, svc, "web", "i-drained", 1024, 2048)
ecsdomain.RegisterInstance(t, svc, "web", "i-live", 1024, 2048)
ecsdomain.RegisterInstance(t, svc, "web", "i-drained", 1024, 2048)
kv, err := svc.Bucket(t.Context(), handlers_ecs.TestAccountID)
kv, err := svc.Bucket(t.Context(), ecsdomain.TestAccountID)
require.NoError(t, err)
drained, err := svc.UpsertInstance(t.Context(), kv, handlers_ecs.TestAccountID, "web", "i-drained", func(r *handlers_ecs.InstanceRecord) {
r.Status = handlers_ecs.InstanceStatusDraining
drained, err := svc.UpsertInstance(t.Context(), kv, ecsdomain.TestAccountID, "web", "i-drained", func(r *ecsdomain.InstanceRecord) {
r.Status = ecsdomain.InstanceStatusDraining
})
require.NoError(t, err)
active := listInstanceARNs(t, svc, "web", handlers_ecs.InstanceStatusActive)
active := listInstanceARNs(t, svc, "web", ecsdomain.InstanceStatusActive)
assert.Len(t, active, 1)
assert.NotContains(t, active, drained.ARN)
assert.Equal(t, []string{drained.ARN}, listInstanceARNs(t, svc, "web", handlers_ecs.InstanceStatusDraining))
assert.Equal(t, []string{drained.ARN}, listInstanceARNs(t, svc, "web", ecsdomain.InstanceStatusDraining))
assert.Len(t, listInstanceARNs(t, svc, "web", ""), 2)
}
@@ -52,10 +52,10 @@ func TestListContainerInstances_StatusSelectsOnlyThatStatus(t *testing.T) {
// so asking for one is a valid question with an empty answer rather than an
// error.
func TestListContainerInstances_TransitionalStatusMatchesNothing(t *testing.T) {
svc, _ := handlers_ecs.NewTestService(t)
_, err := svc.CreateCluster(t.Context(), &ecs.CreateClusterInput{ClusterName: aws.String("web")}, handlers_ecs.TestAccountID)
svc, _ := ecsdomain.NewTestService(t)
_, err := svc.CreateCluster(t.Context(), &ecs.CreateClusterInput{ClusterName: aws.String("web")}, ecsdomain.TestAccountID)
require.NoError(t, err)
handlers_ecs.RegisterInstance(t, svc, "web", "i-live", 1024, 2048)
ecsdomain.RegisterInstance(t, svc, "web", "i-live", 1024, 2048)
for _, status := range []string{"REGISTERING", "DEREGISTERING", "REGISTRATION_FAILED"} {
assert.Empty(t, listInstanceARNs(t, svc, "web", status), status)
@@ -63,42 +63,42 @@ func TestListContainerInstances_TransitionalStatusMatchesNothing(t *testing.T) {
}
func TestListContainerInstances_UnknownStatusIsRefused(t *testing.T) {
svc, _ := handlers_ecs.NewTestService(t)
_, err := svc.CreateCluster(t.Context(), &ecs.CreateClusterInput{ClusterName: aws.String("web")}, handlers_ecs.TestAccountID)
svc, _ := ecsdomain.NewTestService(t)
_, err := svc.CreateCluster(t.Context(), &ecs.CreateClusterInput{ClusterName: aws.String("web")}, ecsdomain.TestAccountID)
require.NoError(t, err)
_, err = svc.ListContainerInstances(t.Context(), &ecs.ListContainerInstancesInput{
Cluster: aws.String("web"),
Status: aws.String("INACTIVE"),
}, handlers_ecs.TestAccountID)
}, ecsdomain.TestAccountID)
require.Error(t, err)
}
// A filtered list and a describe of what it returned have to agree, since the
// status the filter selects on is the one describe reports.
func TestListContainerInstances_FilterAgreesWithDescribe(t *testing.T) {
svc, _ := handlers_ecs.NewTestService(t)
_, err := svc.CreateCluster(t.Context(), &ecs.CreateClusterInput{ClusterName: aws.String("web")}, handlers_ecs.TestAccountID)
svc, _ := ecsdomain.NewTestService(t)
_, err := svc.CreateCluster(t.Context(), &ecs.CreateClusterInput{ClusterName: aws.String("web")}, ecsdomain.TestAccountID)
require.NoError(t, err)
handlers_ecs.RegisterInstance(t, svc, "web", "i-live", 1024, 2048)
handlers_ecs.RegisterInstance(t, svc, "web", "i-drained", 1024, 2048)
ecsdomain.RegisterInstance(t, svc, "web", "i-live", 1024, 2048)
ecsdomain.RegisterInstance(t, svc, "web", "i-drained", 1024, 2048)
kv, err := svc.Bucket(t.Context(), handlers_ecs.TestAccountID)
kv, err := svc.Bucket(t.Context(), ecsdomain.TestAccountID)
require.NoError(t, err)
_, err = svc.UpsertInstance(t.Context(), kv, handlers_ecs.TestAccountID, "web", "i-drained", func(r *handlers_ecs.InstanceRecord) {
r.Status = handlers_ecs.InstanceStatusDraining
_, err = svc.UpsertInstance(t.Context(), kv, ecsdomain.TestAccountID, "web", "i-drained", func(r *ecsdomain.InstanceRecord) {
r.Status = ecsdomain.InstanceStatusDraining
})
require.NoError(t, err)
arns := listInstanceARNs(t, svc, "web", handlers_ecs.InstanceStatusActive)
arns := listInstanceARNs(t, svc, "web", ecsdomain.InstanceStatusActive)
require.NotEmpty(t, arns)
out, err := svc.DescribeContainerInstances(t.Context(), &ecs.DescribeContainerInstancesInput{
Cluster: aws.String("web"),
ContainerInstances: aws.StringSlice(arns),
}, handlers_ecs.TestAccountID)
}, ecsdomain.TestAccountID)
require.NoError(t, err)
require.Len(t, out.ContainerInstances, len(arns))
for _, ci := range out.ContainerInstances {
assert.Equal(t, handlers_ecs.InstanceStatusActive, aws.StringValue(ci.Status))
assert.Equal(t, ecsdomain.InstanceStatusActive, aws.StringValue(ci.Status))
}
}
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"fmt"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"errors"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"errors"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"testing"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"fmt"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
//test:in-package — these decode persisted records straight into the unexported
// record types and call their unexported toAWS projection, which is the whole
@@ -2,7 +2,7 @@
//convergeIfDue) and the trigger is an unexported channel, so an external test
//package can reach neither.
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -2,7 +2,7 @@
// unexported, and the pass is only reachable through the scheduler's own state.
//
//test:in-package — the converge pass has no exported entry point
package handlers_ecs
package ecs
import (
"context"
@@ -1,7 +1,7 @@
// Package handlers_ecs is the ECS control plane: the ECSService contract, its
// Package ecs is the ECS control plane: the ECSService contract, its
// daemon-side JetStream KV implementation and NATS client, and the leader-elected
// scheduler that converges services and drains silent container instances.
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"bytes"
@@ -16,8 +16,8 @@ import (
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/aws/aws-sdk-go/service/elbv2"
ec2eip "github.com/mulgadc/spinifex/spinifex/domains/ec2/eip"
elbv2domain "github.com/mulgadc/spinifex/spinifex/domains/elbv2"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
@@ -43,7 +43,7 @@ func newNATSTargetRegistrar(nc *nats.Conn) *natsTargetRegistrar {
}
func (r *natsTargetRegistrar) Register(ctx context.Context, accountID, tgARN, ip string, port int) error {
_, err := handlers_elbv2.NewNATSELBv2Service(r.nc).RegisterTargets(ctx, &elbv2.RegisterTargetsInput{
_, err := elbv2domain.NewNATSELBv2Service(r.nc).RegisterTargets(ctx, &elbv2.RegisterTargetsInput{
TargetGroupArn: aws.String(tgARN),
Targets: []*elbv2.TargetDescription{{Id: aws.String(ip), Port: aws.Int64(int64(port))}},
}, accountID)
@@ -51,7 +51,7 @@ func (r *natsTargetRegistrar) Register(ctx context.Context, accountID, tgARN, ip
}
func (r *natsTargetRegistrar) Deregister(ctx context.Context, accountID, tgARN, ip string, port int) error {
_, err := handlers_elbv2.NewNATSELBv2Service(r.nc).DeregisterTargets(ctx, &elbv2.DeregisterTargetsInput{
_, err := elbv2domain.NewNATSELBv2Service(r.nc).DeregisterTargets(ctx, &elbv2.DeregisterTargetsInput{
TargetGroupArn: aws.String(tgARN),
Targets: []*elbv2.TargetDescription{{Id: aws.String(ip), Port: aws.Int64(int64(port))}},
}, accountID)
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"encoding/json"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs_test
package ecs_test
import (
"testing"
@@ -6,7 +6,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecs"
ecsv1 "github.com/mulgadc/spinifex/contracts/ecs/v1"
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -14,49 +14,49 @@ import (
// seedTasks writes task records straight to KV. The statuses under test are
// reported by the agent, so driving them through RunTask would require a live
// agent to reach anything other than PENDING.
func seedTasks(t *testing.T, svc *handlers_ecs.Service, cluster string, recs ...*handlers_ecs.TaskRecord) {
func seedTasks(t *testing.T, svc *ecsdomain.Service, cluster string, recs ...*ecsdomain.TaskRecord) {
t.Helper()
kv, err := svc.Bucket(t.Context(), handlers_ecs.TestAccountID)
kv, err := svc.Bucket(t.Context(), ecsdomain.TestAccountID)
require.NoError(t, err)
for _, r := range recs {
r.Cluster = cluster
r.ARN = handlers_ecs.TaskARN(svc.Region(), handlers_ecs.TestAccountID, cluster, r.TaskID)
require.NoError(t, handlers_ecs.PutJSON(t.Context(), kv, handlers_ecs.TaskKey(cluster, r.TaskID), r))
r.ARN = ecsdomain.TaskARN(svc.Region(), ecsdomain.TestAccountID, cluster, r.TaskID)
require.NoError(t, ecsdomain.PutJSON(t.Context(), kv, ecsdomain.TaskKey(cluster, r.TaskID), r))
}
}
func listTaskIDs(t *testing.T, svc *handlers_ecs.Service, input *ecs.ListTasksInput) []string {
func listTaskIDs(t *testing.T, svc *ecsdomain.Service, input *ecs.ListTasksInput) []string {
t.Helper()
out, err := svc.ListTasks(t.Context(), input, handlers_ecs.TestAccountID)
out, err := svc.ListTasks(t.Context(), input, ecsdomain.TestAccountID)
require.NoError(t, err)
ids := make([]string, 0, len(out.TaskArns))
for _, a := range out.TaskArns {
ids = append(ids, handlers_ecs.ContainerInstanceShortID(aws.StringValue(a)))
ids = append(ids, ecsdomain.ContainerInstanceShortID(aws.StringValue(a)))
}
return ids
}
func TestListTasks_DesiredStatusFilters(t *testing.T) {
svc, _ := handlers_ecs.NewTestService(t)
svc, _ := ecsdomain.NewTestService(t)
seedTasks(t, svc, "web",
&handlers_ecs.TaskRecord{TaskID: "run-1", DesiredStatus: handlers_ecs.TaskStatusRunning, LastStatus: handlers_ecs.TaskStatusRunning},
&handlers_ecs.TaskRecord{TaskID: "stop-1", DesiredStatus: handlers_ecs.TaskStatusStopped, LastStatus: handlers_ecs.TaskStatusStopped},
&handlers_ecs.TaskRecord{TaskID: "stop-2", DesiredStatus: handlers_ecs.TaskStatusStopped, LastStatus: handlers_ecs.TaskStatusStopped},
&ecsdomain.TaskRecord{TaskID: "run-1", DesiredStatus: ecsdomain.TaskStatusRunning, LastStatus: ecsdomain.TaskStatusRunning},
&ecsdomain.TaskRecord{TaskID: "stop-1", DesiredStatus: ecsdomain.TaskStatusStopped, LastStatus: ecsdomain.TaskStatusStopped},
&ecsdomain.TaskRecord{TaskID: "stop-2", DesiredStatus: ecsdomain.TaskStatusStopped, LastStatus: ecsdomain.TaskStatusStopped},
)
assert.ElementsMatch(t, []string{"run-1"},
listTaskIDs(t, svc, &ecs.ListTasksInput{Cluster: aws.String("web"), DesiredStatus: aws.String(handlers_ecs.TaskStatusRunning)}))
listTaskIDs(t, svc, &ecs.ListTasksInput{Cluster: aws.String("web"), DesiredStatus: aws.String(ecsdomain.TaskStatusRunning)}))
assert.ElementsMatch(t, []string{"stop-1", "stop-2"},
listTaskIDs(t, svc, &ecs.ListTasksInput{Cluster: aws.String("web"), DesiredStatus: aws.String(handlers_ecs.TaskStatusStopped)}))
listTaskIDs(t, svc, &ecs.ListTasksInput{Cluster: aws.String("web"), DesiredStatus: aws.String(ecsdomain.TaskStatusStopped)}))
}
// The default is the case that broke a caller: asking for nothing used to hand
// back every task the cluster had ever run.
func TestListTasks_OmittedDesiredStatusMeansRunning(t *testing.T) {
svc, _ := handlers_ecs.NewTestService(t)
svc, _ := ecsdomain.NewTestService(t)
seedTasks(t, svc, "web",
&handlers_ecs.TaskRecord{TaskID: "run-1", DesiredStatus: handlers_ecs.TaskStatusRunning, LastStatus: handlers_ecs.TaskStatusRunning},
&handlers_ecs.TaskRecord{TaskID: "stop-1", DesiredStatus: handlers_ecs.TaskStatusStopped, LastStatus: handlers_ecs.TaskStatusStopped},
&ecsdomain.TaskRecord{TaskID: "run-1", DesiredStatus: ecsdomain.TaskStatusRunning, LastStatus: ecsdomain.TaskStatusRunning},
&ecsdomain.TaskRecord{TaskID: "stop-1", DesiredStatus: ecsdomain.TaskStatusStopped, LastStatus: ecsdomain.TaskStatusStopped},
)
assert.ElementsMatch(t, []string{"run-1"},
@@ -64,15 +64,15 @@ func TestListTasks_OmittedDesiredStatusMeansRunning(t *testing.T) {
}
func TestListTasks_FamilyStartedByServiceAndInstanceFilter(t *testing.T) {
svc, _ := handlers_ecs.NewTestService(t)
svc, _ := ecsdomain.NewTestService(t)
seedTasks(t, svc, "web",
&handlers_ecs.TaskRecord{
TaskID: "a", DesiredStatus: handlers_ecs.TaskStatusRunning, LastStatus: handlers_ecs.TaskStatusRunning,
TaskDefFamily: "api", StartedBy: "ci", Group: handlers_ecs.ServiceTaskGroup("web"), ContainerInstanceID: "i-1",
&ecsdomain.TaskRecord{
TaskID: "a", DesiredStatus: ecsdomain.TaskStatusRunning, LastStatus: ecsdomain.TaskStatusRunning,
TaskDefFamily: "api", StartedBy: "ci", Group: ecsdomain.ServiceTaskGroup("web"), ContainerInstanceID: "i-1",
},
&handlers_ecs.TaskRecord{
TaskID: "b", DesiredStatus: handlers_ecs.TaskStatusRunning, LastStatus: handlers_ecs.TaskStatusRunning,
TaskDefFamily: "worker", StartedBy: "human", Group: handlers_ecs.ServiceTaskGroup("batch"), ContainerInstanceID: "i-2",
&ecsdomain.TaskRecord{
TaskID: "b", DesiredStatus: ecsdomain.TaskStatusRunning, LastStatus: ecsdomain.TaskStatusRunning,
TaskDefFamily: "worker", StartedBy: "human", Group: ecsdomain.ServiceTaskGroup("batch"), ContainerInstanceID: "i-2",
},
)
@@ -89,14 +89,14 @@ func TestListTasks_FamilyStartedByServiceAndInstanceFilter(t *testing.T) {
// Filters combine, so a task has to satisfy all of them rather than any.
func TestListTasks_FiltersCombine(t *testing.T) {
svc, _ := handlers_ecs.NewTestService(t)
svc, _ := ecsdomain.NewTestService(t)
seedTasks(t, svc, "web",
&handlers_ecs.TaskRecord{
TaskID: "a", DesiredStatus: handlers_ecs.TaskStatusRunning, LastStatus: handlers_ecs.TaskStatusRunning,
&ecsdomain.TaskRecord{
TaskID: "a", DesiredStatus: ecsdomain.TaskStatusRunning, LastStatus: ecsdomain.TaskStatusRunning,
TaskDefFamily: "api", ContainerInstanceID: "i-1",
},
&handlers_ecs.TaskRecord{
TaskID: "b", DesiredStatus: handlers_ecs.TaskStatusRunning, LastStatus: handlers_ecs.TaskStatusRunning,
&ecsdomain.TaskRecord{
TaskID: "b", DesiredStatus: ecsdomain.TaskStatusRunning, LastStatus: ecsdomain.TaskStatusRunning,
TaskDefFamily: "api", ContainerInstanceID: "i-2",
},
)
@@ -109,49 +109,49 @@ func TestListTasks_FiltersCombine(t *testing.T) {
// A task nobody asked to stop still has to leave the RUNNING filter when it
// exits, or a caller polling for live tasks never stops seeing it.
func TestRecordTaskState_SelfExitMovesDesiredStatusToStopped(t *testing.T) {
svc, _ := handlers_ecs.NewTestService(t)
svc, _ := ecsdomain.NewTestService(t)
seedTasks(t, svc, "web",
&handlers_ecs.TaskRecord{TaskID: "t-1", DesiredStatus: handlers_ecs.TaskStatusRunning, LastStatus: handlers_ecs.TaskStatusRunning},
&ecsdomain.TaskRecord{TaskID: "t-1", DesiredStatus: ecsdomain.TaskStatusRunning, LastStatus: ecsdomain.TaskStatusRunning},
)
require.NoError(t, svc.RecordTaskState(t.Context(), &ecsv1.TaskState{
AccountID: handlers_ecs.TestAccountID, ClusterName: "web", TaskID: "t-1",
LastStatus: handlers_ecs.TaskStatusStopped, Reason: "container exited (1)",
AccountID: ecsdomain.TestAccountID, ClusterName: "web", TaskID: "t-1",
LastStatus: ecsdomain.TaskStatusStopped, Reason: "container exited (1)",
}))
kv, err := svc.Bucket(t.Context(), handlers_ecs.TestAccountID)
kv, err := svc.Bucket(t.Context(), ecsdomain.TestAccountID)
require.NoError(t, err)
var rec handlers_ecs.TaskRecord
found, err := handlers_ecs.GetJSON(t.Context(), kv, handlers_ecs.TaskKey("web", "t-1"), &rec)
var rec ecsdomain.TaskRecord
found, err := ecsdomain.GetJSON(t.Context(), kv, ecsdomain.TaskKey("web", "t-1"), &rec)
require.NoError(t, err)
require.True(t, found)
assert.Equal(t, handlers_ecs.TaskStatusStopped, rec.DesiredStatus)
assert.Equal(t, ecsdomain.TaskStatusStopped, rec.DesiredStatus)
assert.Empty(t, listTaskIDs(t, svc, &ecs.ListTasksInput{Cluster: aws.String("web")}))
assert.ElementsMatch(t, []string{"t-1"}, listTaskIDs(t, svc, &ecs.ListTasksInput{
Cluster: aws.String("web"), DesiredStatus: aws.String(handlers_ecs.TaskStatusStopped),
Cluster: aws.String("web"), DesiredStatus: aws.String(ecsdomain.TaskStatusStopped),
}))
}
func TestDescribeContainerInstances_CountsComeFromTaskRecords(t *testing.T) {
svc, _ := handlers_ecs.NewTestService(t)
svc, _ := ecsdomain.NewTestService(t)
_, err := svc.RegisterContainerInstance(t.Context(), &ecs.RegisterContainerInstanceInput{
Cluster: aws.String("web"),
InstanceIdentityDocument: aws.String("i-1"),
}, handlers_ecs.TestAccountID)
}, ecsdomain.TestAccountID)
require.NoError(t, err)
seedTasks(t, svc, "web",
&handlers_ecs.TaskRecord{TaskID: "r1", LastStatus: handlers_ecs.TaskStatusRunning, ContainerInstanceID: "i-1"},
&handlers_ecs.TaskRecord{TaskID: "p1", LastStatus: handlers_ecs.TaskStatusPending, ContainerInstanceID: "i-1"},
&handlers_ecs.TaskRecord{TaskID: "s1", LastStatus: handlers_ecs.TaskStatusStopped, ContainerInstanceID: "i-1"},
&handlers_ecs.TaskRecord{TaskID: "s2", LastStatus: handlers_ecs.TaskStatusStopped, ContainerInstanceID: "i-1"},
&handlers_ecs.TaskRecord{TaskID: "r2", LastStatus: handlers_ecs.TaskStatusRunning, ContainerInstanceID: "i-2"},
&ecsdomain.TaskRecord{TaskID: "r1", LastStatus: ecsdomain.TaskStatusRunning, ContainerInstanceID: "i-1"},
&ecsdomain.TaskRecord{TaskID: "p1", LastStatus: ecsdomain.TaskStatusPending, ContainerInstanceID: "i-1"},
&ecsdomain.TaskRecord{TaskID: "s1", LastStatus: ecsdomain.TaskStatusStopped, ContainerInstanceID: "i-1"},
&ecsdomain.TaskRecord{TaskID: "s2", LastStatus: ecsdomain.TaskStatusStopped, ContainerInstanceID: "i-1"},
&ecsdomain.TaskRecord{TaskID: "r2", LastStatus: ecsdomain.TaskStatusRunning, ContainerInstanceID: "i-2"},
)
out, err := svc.DescribeContainerInstances(t.Context(), &ecs.DescribeContainerInstancesInput{
Cluster: aws.String("web"), ContainerInstances: []*string{aws.String("i-1")},
}, handlers_ecs.TestAccountID)
}, ecsdomain.TestAccountID)
require.NoError(t, err)
require.Len(t, out.ContainerInstances, 1)
assert.Equal(t, int64(1), aws.Int64Value(out.ContainerInstances[0].RunningTasksCount))
@@ -162,21 +162,21 @@ func TestDescribeContainerInstances_CountsComeFromTaskRecords(t *testing.T) {
// names a task that stopped, so a count taken from it reports a task nobody is
// running.
func TestDescribeContainerInstances_StalePlacedTasksDoNotInflateCount(t *testing.T) {
svc, _ := handlers_ecs.NewTestService(t)
kv, err := svc.Bucket(t.Context(), handlers_ecs.TestAccountID)
svc, _ := ecsdomain.NewTestService(t)
kv, err := svc.Bucket(t.Context(), ecsdomain.TestAccountID)
require.NoError(t, err)
_, err = svc.UpsertInstance(t.Context(), kv, handlers_ecs.TestAccountID, "web", "i-1", func(r *handlers_ecs.InstanceRecord) {
_, err = svc.UpsertInstance(t.Context(), kv, ecsdomain.TestAccountID, "web", "i-1", func(r *ecsdomain.InstanceRecord) {
r.PlacedTasks = []string{"gone-1", "gone-2", "r1"}
})
require.NoError(t, err)
seedTasks(t, svc, "web",
&handlers_ecs.TaskRecord{TaskID: "r1", LastStatus: handlers_ecs.TaskStatusRunning, ContainerInstanceID: "i-1"},
&ecsdomain.TaskRecord{TaskID: "r1", LastStatus: ecsdomain.TaskStatusRunning, ContainerInstanceID: "i-1"},
)
out, err := svc.DescribeContainerInstances(t.Context(), &ecs.DescribeContainerInstancesInput{
Cluster: aws.String("web"), ContainerInstances: []*string{aws.String("i-1")},
}, handlers_ecs.TestAccountID)
}, ecsdomain.TestAccountID)
require.NoError(t, err)
require.Len(t, out.ContainerInstances, 1)
assert.Equal(t, int64(1), aws.Int64Value(out.ContainerInstances[0].RunningTasksCount))
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"context"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"errors"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"fmt"
@@ -1,4 +1,4 @@
package handlers_ecs
package ecs
import (
"testing"
@@ -1,8 +1,8 @@
package handlers_elbv2
package 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,10 +1,10 @@
package handlers_elbv2
package elbv2
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,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"context"
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"context"
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"context"
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"testing"
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"fmt"
@@ -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.
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"context"
@@ -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,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"strings"
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"context"
@@ -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
@@ -1,11 +1,11 @@
package handlers_elbv2
package elbv2
import (
"context"
"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,4 +1,4 @@
package handlers_elbv2
package elbv2
import "github.com/mulgadc/spinifex/spinifex/domains/ec2/systeminstance"
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"sync"
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"log/slog"
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers"
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"context"
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"context"
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"crypto/x509"
@@ -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,10 +1,10 @@
package handlers_elbv2
package elbv2
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"
)
@@ -1,4 +1,4 @@
package handlers_elbv2
package elbv2
import (
"context"

Some files were not shown because too many files have changed in this diff Show More