mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-26 05:59:41 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d2d81b9cb | |||
| c4feca87ed |
@@ -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: "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: "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"},
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (c class) isLegacy(roots ...string) bool {
|
||||
|
||||
// legacyRoots are pre-ADR-0001 application roots still standing.
|
||||
var legacyRoots = map[string]bool{
|
||||
"handlers": true, "gateway": true,
|
||||
"gateway": true,
|
||||
"accountteardown": true,
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,11 @@ var frozenLegacyPackages = []string{
|
||||
"gateway/ecs",
|
||||
"gateway/elbv2",
|
||||
"gateway/spx",
|
||||
"handlers/ecs",
|
||||
}
|
||||
|
||||
// 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", "lbagent", "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.
|
||||
|
||||
+1
-2
@@ -311,7 +311,6 @@ spinifex/
|
||||
│ ├── operator/ # spx CLI commands, admin use cases, installer, upgrade
|
||||
│ ├── 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 service implementation
|
||||
│ └── accountteardown/ # Legacy root: account deletion cleanup
|
||||
├── internal/ # In-guest SigV4 clients, AWS Smithy models and test kits
|
||||
├── architecture/ # Executable architecture rules (package layering)
|
||||
@@ -322,7 +321,7 @@ spinifex/
|
||||
└── e2e/ # Live-cluster suites (see docs/testing/e2e-coverage.md)
|
||||
```
|
||||
|
||||
The three remaining 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
|
||||
|
||||
@@ -349,6 +349,7 @@ while the branch is in flight; they do not describe the umbrella repository's
|
||||
| `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
|
||||
@@ -447,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.
|
||||
@@ -467,8 +468,8 @@ 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.
|
||||
@@ -477,7 +478,7 @@ Enforcement: the owner decided the layering test is blocking now, with a ratchet
|
||||
- `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 `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.*`: `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.
|
||||
- `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.
|
||||
@@ -664,7 +665,7 @@ 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` | — | 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`. |
|
||||
@@ -672,7 +673,7 @@ series above; "needs decision" is open.
|
||||
| `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` | The ECS AWS action adapters and dispatch, not yet registered through ADR-0002. | Deferred: it follows `handlers/ecs`, 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. |
|
||||
@@ -687,7 +688,7 @@ series above; "needs decision" is open.
|
||||
|
||||
### Remaining legacy roots by blocker class
|
||||
|
||||
Three legacy roots remain: `accountteardown`, `gateway` and `handlers`.
|
||||
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.
|
||||
|
||||
@@ -695,7 +696,7 @@ ADR-0001 completion is structural, so none of these is cleared by remediating se
|
||||
|---|---|---|
|
||||
| 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); `accountteardown` (domain cleanup capabilities); `gateway` root (ADR-0002 auth and request-context phase); `gateway/ecs` (follows `handlers/ecs`, 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.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ services:
|
||||
- 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
|
||||
@@ -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.*"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers_ecs
|
||||
package ecs
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_ecs
|
||||
package ecs
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_ecs
|
||||
package ecs
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers_ecs
|
||||
package ecs
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_ecs
|
||||
package ecs
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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"
|
||||
+29
-29
@@ -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
-1
@@ -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"
|
||||
+1
-1
@@ -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
-1
@@ -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"
|
||||
+52
-52
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_ecs
|
||||
package ecs
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -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"
|
||||
@@ -50,7 +50,7 @@ const (
|
||||
ecsTasksServicePrincipal = "ecs-tasks.amazonaws.com"
|
||||
|
||||
// ecsInstanceRoleName is the fixed role the ECS control plane attaches to every
|
||||
// container instance. Duplicated from handlers_ecs, which cannot be imported
|
||||
// container instance. Duplicated from domains/ecs, which cannot be imported
|
||||
// here: it lives on the daemon side of the awsgw trust boundary.
|
||||
ecsInstanceRoleName = "ecsInstanceRole"
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ecs"
|
||||
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -41,7 +41,7 @@ func checkTaskDefinitionRoles(passRoleCheck PassRoleChecker, roleARNs ...*string
|
||||
// checkTaskLaunchRoles resolves taskDefinition's role ARNs and enforces
|
||||
// iam:PassRole before a launch can start tasks under them, since RunTask,
|
||||
// StartTask, CreateService and UpdateService only reference a task definition.
|
||||
func checkTaskLaunchRoles(ctx context.Context, svc handlers_ecs.ECSService, accountID string, taskDefinition *string, passRoleCheck PassRoleChecker) error {
|
||||
func checkTaskLaunchRoles(ctx context.Context, svc ecsdomain.ECSService, accountID string, taskDefinition *string, passRoleCheck PassRoleChecker) error {
|
||||
if passRoleCheck == nil || aws.StringValue(taskDefinition) == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func CreateCluster(ctx context.Context, nc *nats.Conn, accountID string, body []
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).CreateCluster(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).CreateCluster(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DeleteCluster implements the ECS DeleteCluster action, deleting a cluster. The JSON body (empty
|
||||
@@ -74,7 +74,7 @@ func DeleteCluster(ctx context.Context, nc *nats.Conn, accountID string, body []
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).DeleteCluster(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).DeleteCluster(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DescribeClusters implements the ECS DescribeClusters action, describing clusters. The JSON body
|
||||
@@ -84,7 +84,7 @@ func DescribeClusters(ctx context.Context, nc *nats.Conn, accountID string, body
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).DescribeClusters(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).DescribeClusters(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListClusters implements the ECS ListClusters action, listing cluster ARNs. The JSON body (empty
|
||||
@@ -94,7 +94,7 @@ func ListClusters(ctx context.Context, nc *nats.Conn, accountID string, body []b
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).ListClusters(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).ListClusters(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// --- Task definition ---
|
||||
@@ -109,7 +109,7 @@ func RegisterTaskDefinition(ctx context.Context, nc *nats.Conn, accountID string
|
||||
if err := checkTaskDefinitionRoles(passRoleCheck, input.TaskRoleArn, input.ExecutionRoleArn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).RegisterTaskDefinition(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).RegisterTaskDefinition(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DeregisterTaskDefinition implements the ECS DeregisterTaskDefinition action, marking a task
|
||||
@@ -120,7 +120,7 @@ func DeregisterTaskDefinition(ctx context.Context, nc *nats.Conn, accountID stri
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).DeregisterTaskDefinition(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).DeregisterTaskDefinition(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DescribeTaskDefinition implements the ECS DescribeTaskDefinition action, describing a task
|
||||
@@ -131,7 +131,7 @@ func DescribeTaskDefinition(ctx context.Context, nc *nats.Conn, accountID string
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).DescribeTaskDefinition(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).DescribeTaskDefinition(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListTaskDefinitions implements the ECS ListTaskDefinitions action, listing task definition
|
||||
@@ -142,7 +142,7 @@ func ListTaskDefinitions(ctx context.Context, nc *nats.Conn, accountID string, b
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).ListTaskDefinitions(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).ListTaskDefinitions(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// --- Container instance ---
|
||||
@@ -155,17 +155,17 @@ func RegisterContainerInstance(ctx context.Context, nc *nats.Conn, accountID str
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).RegisterContainerInstance(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).RegisterContainerInstance(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ProvisionCapacity launches container-instance EC2 capacity into a cluster.
|
||||
// Input/output are the custom handlers_ecs types, not aws-sdk-go ecs shapes.
|
||||
// Input/output are the custom domains/ecs types, not aws-sdk-go ecs shapes.
|
||||
func ProvisionCapacity(ctx context.Context, nc *nats.Conn, accountID string, body []byte, passRoleCheck PassRoleChecker) (any, error) {
|
||||
input := new(handlers_ecs.ProvisionCapacityInput)
|
||||
input := new(ecsdomain.ProvisionCapacityInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).ProvisionCapacity(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).ProvisionCapacity(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DeregisterContainerInstance implements the ECS DeregisterContainerInstance action, removing a
|
||||
@@ -176,7 +176,7 @@ func DeregisterContainerInstance(ctx context.Context, nc *nats.Conn, accountID s
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).DeregisterContainerInstance(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).DeregisterContainerInstance(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// UpdateContainerInstancesState implements the ECS UpdateContainerInstancesState action, setting
|
||||
@@ -187,7 +187,7 @@ func UpdateContainerInstancesState(ctx context.Context, nc *nats.Conn, accountID
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).UpdateContainerInstancesState(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).UpdateContainerInstancesState(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DescribeContainerInstances implements the ECS DescribeContainerInstances action, describing
|
||||
@@ -198,7 +198,7 @@ func DescribeContainerInstances(ctx context.Context, nc *nats.Conn, accountID st
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).DescribeContainerInstances(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).DescribeContainerInstances(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListContainerInstances implements the ECS ListContainerInstances action, listing container
|
||||
@@ -209,7 +209,7 @@ func ListContainerInstances(ctx context.Context, nc *nats.Conn, accountID string
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).ListContainerInstances(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).ListContainerInstances(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// --- Task ---
|
||||
@@ -222,7 +222,7 @@ func RunTask(ctx context.Context, nc *nats.Conn, accountID string, body []byte,
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := runTaskIdempotent(ctx, handlers_ecs.NewNATSECSService(nc), accountID, input, passRoleCheck,
|
||||
out, err := runTaskIdempotent(ctx, ecsdomain.NewNATSECSService(nc), accountID, input, passRoleCheck,
|
||||
func() (*runTaskStore, error) { return getRunTaskStore(ctx, nc) })
|
||||
if err != nil {
|
||||
// Returned as a typed nil otherwise, which is a non-nil any.
|
||||
@@ -236,7 +236,7 @@ func RunTask(ctx context.Context, nc *nats.Conn, accountID string, body []byte,
|
||||
// the bind, and so a test can supply a store without a live connection.
|
||||
func runTaskIdempotent(
|
||||
ctx context.Context,
|
||||
svc handlers_ecs.ECSService,
|
||||
svc ecsdomain.ECSService,
|
||||
accountID string,
|
||||
input *ecs.RunTaskInput,
|
||||
passRoleCheck PassRoleChecker,
|
||||
@@ -273,7 +273,7 @@ func runTaskIdempotent(
|
||||
|
||||
// runTask is RunTask's core, split out so tests can inject a fake ECSService
|
||||
// instead of a live NATS connection.
|
||||
func runTask(ctx context.Context, svc handlers_ecs.ECSService, accountID string, input *ecs.RunTaskInput, passRoleCheck PassRoleChecker) (*ecs.RunTaskOutput, error) {
|
||||
func runTask(ctx context.Context, svc ecsdomain.ECSService, accountID string, input *ecs.RunTaskInput, passRoleCheck PassRoleChecker) (*ecs.RunTaskOutput, error) {
|
||||
if err := checkTaskLaunchRoles(ctx, svc, accountID, input.TaskDefinition, passRoleCheck); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -287,12 +287,12 @@ func StartTask(ctx context.Context, nc *nats.Conn, accountID string, body []byte
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return startTask(ctx, handlers_ecs.NewNATSECSService(nc), accountID, input, passRoleCheck)
|
||||
return startTask(ctx, ecsdomain.NewNATSECSService(nc), accountID, input, passRoleCheck)
|
||||
}
|
||||
|
||||
// startTask is StartTask's core, split out so tests can inject a fake
|
||||
// ECSService instead of a live NATS connection.
|
||||
func startTask(ctx context.Context, svc handlers_ecs.ECSService, accountID string, input *ecs.StartTaskInput, passRoleCheck PassRoleChecker) (any, error) {
|
||||
func startTask(ctx context.Context, svc ecsdomain.ECSService, accountID string, input *ecs.StartTaskInput, passRoleCheck PassRoleChecker) (any, error) {
|
||||
if err := checkTaskLaunchRoles(ctx, svc, accountID, input.TaskDefinition, passRoleCheck); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -306,7 +306,7 @@ func StopTask(ctx context.Context, nc *nats.Conn, accountID string, body []byte,
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).StopTask(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).StopTask(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DescribeTasks implements the ECS DescribeTasks action, describing tasks. The JSON body (empty
|
||||
@@ -316,7 +316,7 @@ func DescribeTasks(ctx context.Context, nc *nats.Conn, accountID string, body []
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).DescribeTasks(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).DescribeTasks(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListTasks implements the ECS ListTasks action, listing task ARNs. The JSON body (empty means a
|
||||
@@ -326,7 +326,7 @@ func ListTasks(ctx context.Context, nc *nats.Conn, accountID string, body []byte
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).ListTasks(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).ListTasks(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// --- Service ---
|
||||
@@ -342,12 +342,12 @@ func CreateService(ctx context.Context, nc *nats.Conn, accountID string, body []
|
||||
if err := checkAZRebalancing(body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return createService(ctx, handlers_ecs.NewNATSECSService(nc), accountID, input, passRoleCheck)
|
||||
return createService(ctx, ecsdomain.NewNATSECSService(nc), accountID, input, passRoleCheck)
|
||||
}
|
||||
|
||||
// createService is CreateService's core, split out so tests can inject a fake
|
||||
// ECSService instead of a live NATS connection.
|
||||
func createService(ctx context.Context, svc handlers_ecs.ECSService, accountID string, input *ecs.CreateServiceInput, passRoleCheck PassRoleChecker) (any, error) {
|
||||
func createService(ctx context.Context, svc ecsdomain.ECSService, accountID string, input *ecs.CreateServiceInput, passRoleCheck PassRoleChecker) (any, error) {
|
||||
if err := checkTaskLaunchRoles(ctx, svc, accountID, input.TaskDefinition, passRoleCheck); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -364,13 +364,13 @@ func UpdateService(ctx context.Context, nc *nats.Conn, accountID string, body []
|
||||
if err := checkAZRebalancing(body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return updateService(ctx, handlers_ecs.NewNATSECSService(nc), accountID, input, passRoleCheck)
|
||||
return updateService(ctx, ecsdomain.NewNATSECSService(nc), accountID, input, passRoleCheck)
|
||||
}
|
||||
|
||||
// updateService is UpdateService's core, split out so tests can inject a fake
|
||||
// ECSService instead of a live NATS connection. A nil/empty TaskDefinition
|
||||
// (desiredCount-only update) skips the check — nothing new to pass.
|
||||
func updateService(ctx context.Context, svc handlers_ecs.ECSService, accountID string, input *ecs.UpdateServiceInput, passRoleCheck PassRoleChecker) (any, error) {
|
||||
func updateService(ctx context.Context, svc ecsdomain.ECSService, accountID string, input *ecs.UpdateServiceInput, passRoleCheck PassRoleChecker) (any, error) {
|
||||
if err := checkTaskLaunchRoles(ctx, svc, accountID, input.TaskDefinition, passRoleCheck); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -384,7 +384,7 @@ func DeleteService(ctx context.Context, nc *nats.Conn, accountID string, body []
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).DeleteService(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).DeleteService(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DescribeServices implements the ECS DescribeServices action, describing services. The JSON body
|
||||
@@ -394,7 +394,7 @@ func DescribeServices(ctx context.Context, nc *nats.Conn, accountID string, body
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).DescribeServices(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).DescribeServices(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListServices implements the ECS ListServices action, listing service ARNs. The JSON body (empty
|
||||
@@ -404,7 +404,7 @@ func ListServices(ctx context.Context, nc *nats.Conn, accountID string, body []b
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).ListServices(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).ListServices(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// SubmitTaskStateChange is the agent's task-state report path over the gateway
|
||||
@@ -415,7 +415,7 @@ func SubmitTaskStateChange(ctx context.Context, nc *nats.Conn, accountID string,
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).SubmitTaskStateChange(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).SubmitTaskStateChange(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// PollAssignments drains the calling instance's assignment inbox (replaces the
|
||||
@@ -423,22 +423,22 @@ func SubmitTaskStateChange(ctx context.Context, nc *nats.Conn, accountID string,
|
||||
// shape; the response carries ecsv1.Assign with an RFC3339 time, so the gateway
|
||||
// encodes it with encoding/json (RawJSONActions), not the jsonutil marshaler.
|
||||
func PollAssignments(ctx context.Context, nc *nats.Conn, accountID string, body []byte, passRoleCheck PassRoleChecker) (any, error) {
|
||||
input := new(handlers_ecs.PollAssignmentsInput)
|
||||
input := new(ecsdomain.PollAssignmentsInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).PollAssignments(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).PollAssignments(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ReportTaskGPU carries the agent's local ledger's per-task GPU device
|
||||
// assignment (agent → gateway). Internal action, not an AWS SDK shape: real
|
||||
// AWS's SubmitTaskStateChange has no gpuIds field.
|
||||
func ReportTaskGPU(ctx context.Context, nc *nats.Conn, accountID string, body []byte, passRoleCheck PassRoleChecker) (any, error) {
|
||||
input := new(handlers_ecs.ReportTaskGPUInput)
|
||||
input := new(ecsdomain.ReportTaskGPUInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).ReportTaskGPU(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).ReportTaskGPU(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// --- Tags ---
|
||||
@@ -450,7 +450,7 @@ func TagResource(ctx context.Context, nc *nats.Conn, accountID string, body []by
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).TagResource(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).TagResource(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// UntagResource implements the ECS UntagResource action, removing tags from an ECS resource. The
|
||||
@@ -461,7 +461,7 @@ func UntagResource(ctx context.Context, nc *nats.Conn, accountID string, body []
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).UntagResource(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).UntagResource(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListTagsForResource implements the ECS ListTagsForResource action, listing an ECS resource's
|
||||
@@ -472,7 +472,7 @@ func ListTagsForResource(ctx context.Context, nc *nats.Conn, accountID string, b
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).ListTagsForResource(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).ListTagsForResource(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// --- Capacity providers ---
|
||||
@@ -485,7 +485,7 @@ func PutClusterCapacityProviders(ctx context.Context, nc *nats.Conn, accountID s
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).PutClusterCapacityProviders(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).PutClusterCapacityProviders(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// CreateCapacityProvider implements the ECS CreateCapacityProvider action, creating a capacity
|
||||
@@ -496,7 +496,7 @@ func CreateCapacityProvider(ctx context.Context, nc *nats.Conn, accountID string
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).CreateCapacityProvider(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).CreateCapacityProvider(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DescribeCapacityProviders implements the ECS DescribeCapacityProviders action, describing
|
||||
@@ -507,7 +507,7 @@ func DescribeCapacityProviders(ctx context.Context, nc *nats.Conn, accountID str
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).DescribeCapacityProviders(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).DescribeCapacityProviders(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DeleteCapacityProvider implements the ECS DeleteCapacityProvider action, deleting a capacity
|
||||
@@ -518,5 +518,5 @@ func DeleteCapacityProvider(ctx context.Context, nc *nats.Conn, accountID string
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_ecs.NewNATSECSService(nc).DeleteCapacityProvider(ctx, input, accountID)
|
||||
return ecsdomain.NewNATSECSService(nc).DeleteCapacityProvider(ctx, input, accountID)
|
||||
}
|
||||
|
||||
@@ -8,20 +8,20 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ecs"
|
||||
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testTaskRoleARN = "arn:aws:iam::123456789012:role/ecs-task-role"
|
||||
|
||||
// fakeECSService is a minimal handlers_ecs.ECSService test double. Embedding
|
||||
// fakeECSService is a minimal ecsdomain.ECSService test double. Embedding
|
||||
// the (nil) interface satisfies the contract; any method not overridden below
|
||||
// nil-panics if called, surfacing an unexpected call the same way the EC2
|
||||
// gateway tests' fakeIAMService does.
|
||||
type fakeECSService struct {
|
||||
handlers_ecs.ECSService
|
||||
ecsdomain.ECSService
|
||||
|
||||
describeOut *ecs.DescribeTaskDefinitionOutput
|
||||
describeErr error
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
|
||||
"github.com/mulgadc/spinifex/spinifex/ingress/aws/bodyscope"
|
||||
"github.com/mulgadc/spinifex/spinifex/ingress/aws/query"
|
||||
)
|
||||
@@ -181,7 +181,7 @@ func ResourceARNs(action, region, accountID string, body []byte) ([]string, erro
|
||||
func resolve(source resourceSource, action, region, accountID string, scope bodyscope.Scope) ([]string, error) {
|
||||
// The handler resolves an omitted cluster to "default", so the gate must
|
||||
// too, or a fence on the default cluster never fires.
|
||||
cluster := handlers_ecs.ClusterShortName(scope.String("cluster"))
|
||||
cluster := ecsdomain.ClusterShortName(scope.String("cluster"))
|
||||
|
||||
switch source {
|
||||
case sourceAny:
|
||||
@@ -191,7 +191,7 @@ func resolve(source resourceSource, action, region, accountID string, scope body
|
||||
return one(clusterARN(region, accountID, cluster)), nil
|
||||
|
||||
case sourceNewCluster:
|
||||
name := handlers_ecs.ClusterShortName(scope.String("clusterName"))
|
||||
name := ecsdomain.ClusterShortName(scope.String("clusterName"))
|
||||
return one(clusterARN(region, accountID, name)), nil
|
||||
|
||||
case sourceClusters:
|
||||
@@ -202,7 +202,7 @@ func resolve(source resourceSource, action, region, accountID string, scope body
|
||||
return one(clusterARN(region, accountID, cluster)), nil
|
||||
}
|
||||
return each(refs, func(ref string) string {
|
||||
return clusterARN(region, accountID, handlers_ecs.ClusterShortName(ref))
|
||||
return clusterARN(region, accountID, ecsdomain.ClusterShortName(ref))
|
||||
})
|
||||
|
||||
case sourceService:
|
||||
@@ -283,39 +283,39 @@ func clusterARN(region, accountID, cluster string) string {
|
||||
if region == "" || accountID == "" || cluster == "" {
|
||||
return anyResource
|
||||
}
|
||||
return handlers_ecs.ClusterARN(region, accountID, cluster)
|
||||
return ecsdomain.ClusterARN(region, accountID, cluster)
|
||||
}
|
||||
|
||||
func serviceARN(region, accountID, cluster, ref string) string {
|
||||
name := handlers_ecs.ServiceShortName(ref)
|
||||
name := ecsdomain.ServiceShortName(ref)
|
||||
if region == "" || accountID == "" || cluster == "" || name == "" {
|
||||
return anyResource
|
||||
}
|
||||
return handlers_ecs.ServiceARN(region, accountID, cluster, name)
|
||||
return ecsdomain.ServiceARN(region, accountID, cluster, name)
|
||||
}
|
||||
|
||||
func taskARN(region, accountID, cluster, ref string) string {
|
||||
id := handlers_ecs.TaskShortID(ref)
|
||||
id := ecsdomain.TaskShortID(ref)
|
||||
if region == "" || accountID == "" || cluster == "" || id == "" {
|
||||
return anyResource
|
||||
}
|
||||
return handlers_ecs.TaskARN(region, accountID, cluster, id)
|
||||
return ecsdomain.TaskARN(region, accountID, cluster, id)
|
||||
}
|
||||
|
||||
func containerInstanceARN(region, accountID, cluster, ref string) string {
|
||||
id := handlers_ecs.ContainerInstanceShortID(ref)
|
||||
id := ecsdomain.ContainerInstanceShortID(ref)
|
||||
if region == "" || accountID == "" || cluster == "" || id == "" {
|
||||
return anyResource
|
||||
}
|
||||
return handlers_ecs.ContainerInstanceARN(region, accountID, cluster, id)
|
||||
return ecsdomain.ContainerInstanceARN(region, accountID, cluster, id)
|
||||
}
|
||||
|
||||
func capacityProviderARN(region, accountID, ref string) string {
|
||||
name := handlers_ecs.CapacityProviderShortName(ref)
|
||||
name := ecsdomain.CapacityProviderShortName(ref)
|
||||
if region == "" || accountID == "" || name == "" {
|
||||
return anyResource
|
||||
}
|
||||
return handlers_ecs.CapacityProviderARN(region, accountID, name)
|
||||
return ecsdomain.CapacityProviderARN(region, accountID, name)
|
||||
}
|
||||
|
||||
// taskDefARN resolves "family", "family:revision" or a full ARN the way the
|
||||
@@ -342,7 +342,7 @@ func tagARN(region, accountID, resourceARN string) string {
|
||||
if region == "" || accountID == "" {
|
||||
return anyResource
|
||||
}
|
||||
segment, err := handlers_ecs.ResourceARNSegment(resourceARN)
|
||||
segment, err := ecsdomain.ResourceARNSegment(resourceARN)
|
||||
if err != nil {
|
||||
// An ARN the tag handler rejects stays its own validation fault.
|
||||
return anyResource
|
||||
|
||||
@@ -63,6 +63,7 @@ import (
|
||||
ec2volume "github.com/mulgadc/spinifex/spinifex/domains/ec2/volume"
|
||||
ec2vpc "github.com/mulgadc/spinifex/spinifex/domains/ec2/vpc"
|
||||
"github.com/mulgadc/spinifex/spinifex/domains/ecr"
|
||||
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
elbv2domain "github.com/mulgadc/spinifex/spinifex/domains/elbv2"
|
||||
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
|
||||
@@ -79,7 +80,6 @@ import (
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/state/clustersize"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/telemetry"
|
||||
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
|
||||
"github.com/mulgadc/spinifex/spinifex/providers/ebs"
|
||||
"github.com/mulgadc/spinifex/spinifex/providers/objectstore"
|
||||
"github.com/mulgadc/spinifex/spinifex/runtime/compute/gpu"
|
||||
@@ -167,8 +167,8 @@ type Daemon struct {
|
||||
eipService ec2eip.EIPService
|
||||
elbv2Service *elbv2domain.ELBv2ServiceImpl
|
||||
eksService *ekscluster.EKSServiceImpl
|
||||
ecsService *handlers_ecs.Service
|
||||
ecsScheduler *handlers_ecs.Scheduler
|
||||
ecsService *ecsdomain.Service
|
||||
ecsScheduler *ecsdomain.Scheduler
|
||||
rdsService *rdsinstance.Service
|
||||
rdsReconciler *rdsinstance.Reconciler
|
||||
bedrockService *ochre.Service
|
||||
@@ -2106,13 +2106,13 @@ func (d *Daemon) startCluster() error {
|
||||
// scheduler goroutine that owns the Layer-2 bus subscriptions and heartbeat
|
||||
// reaper. The scheduler is disabled (handlers still serve) when JetStream is
|
||||
// unavailable.
|
||||
d.ecsService = handlers_ecs.NewService(d.natsConn, d.config.Region, d.clusterConfig.AWS.ServicesDomain).WithDeps(d.buildECSServiceDeps())
|
||||
d.ecsService = ecsdomain.NewService(d.natsConn, d.config.Region, d.clusterConfig.AWS.ServicesDomain).WithDeps(d.buildECSServiceDeps())
|
||||
if js, jsErr := jetstream.New(d.natsConn); jsErr != nil {
|
||||
slog.Warn("ECS scheduler disabled: JetStream unavailable", "err", jsErr)
|
||||
} else if _, lbErr := handlers_ecs.InitLeaderBucket(d.ctx, js); lbErr != nil {
|
||||
} else if _, lbErr := ecsdomain.InitLeaderBucket(d.ctx, js); lbErr != nil {
|
||||
slog.Warn("ECS scheduler disabled: leader bucket init failed", "err", lbErr)
|
||||
} else {
|
||||
d.ecsScheduler = handlers_ecs.NewScheduler(d.natsConn, d.ecsService, d.node)
|
||||
d.ecsScheduler = ecsdomain.NewScheduler(d.natsConn, d.ecsService, d.node)
|
||||
d.shutdownWg.Go(func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
|
||||
@@ -6,15 +6,15 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mulgadc/bluebottle/pkg/masterkey"
|
||||
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
|
||||
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
|
||||
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
|
||||
)
|
||||
|
||||
// buildECSServiceDeps assembles the ECS service Deps for ProvisionCapacity: the
|
||||
// gateway endpoint/CA seeded into container-instance user-data, the image
|
||||
// resolver, the customer RunInstances path, and (when the master key loads) a
|
||||
// KV-backed IAM service for the ecsInstanceRole instance profile.
|
||||
func (d *Daemon) buildECSServiceDeps() handlers_ecs.Deps {
|
||||
func (d *Daemon) buildECSServiceDeps() ecsdomain.Deps {
|
||||
gatewayCA := ""
|
||||
if d.config.NATS.CACert != "" {
|
||||
if caBytes, readErr := os.ReadFile(d.config.NATS.CACert); readErr == nil {
|
||||
@@ -25,7 +25,7 @@ func (d *Daemon) buildECSServiceDeps() handlers_ecs.Deps {
|
||||
}
|
||||
}
|
||||
|
||||
deps := handlers_ecs.Deps{
|
||||
deps := ecsdomain.Deps{
|
||||
GatewayBaseURL: d.resolveGatewayBaseURL(),
|
||||
GatewayCACert: gatewayCA,
|
||||
Images: d.imageService,
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/bootstrap/config"
|
||||
acmdomain "github.com/mulgadc/spinifex/spinifex/domains/acm"
|
||||
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
|
||||
ecsdomain "github.com/mulgadc/spinifex/spinifex/domains/ecs"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
elbv2domain "github.com/mulgadc/spinifex/spinifex/domains/elbv2"
|
||||
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
|
||||
@@ -47,6 +47,6 @@ func StartServiceDaemonLite(t *testing.T, gw *Gateway) {
|
||||
t.Cleanup(eks.Shutdown)
|
||||
subscribeServiceMethods(t, nc, "eks", eks)
|
||||
|
||||
subscribeServiceMethods(t, nc, "ecs", handlers_ecs.NewService(nc, testRegion, ""))
|
||||
subscribeServiceMethods(t, nc, "ecs", ecsdomain.NewService(nc, testRegion, ""))
|
||||
subscribeServiceMethods(t, nc, "rds", rdsinstance.NewService(nc, testRegion))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user