mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-11-02 17:23:32 +00:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7aaf6bf5e7 | |||
| 6f9875abb9 | |||
| 3503215bae | |||
| e5509443b3 | |||
| ee4f571687 | |||
| 49aea1b365 | |||
| d9f19be941 | |||
| 598e3aec8d | |||
| 7957186998 | |||
| 3a778d579e | |||
| fffa2e4a47 | |||
| 31b5356109 | |||
| 6eac30217f | |||
| 2511cc4cc3 | |||
| a6b6bb73a9 | |||
| 744a73ef1e | |||
| 2df87981f2 | |||
| 7fc79d95bd | |||
| 0c440a6717 | |||
| 00e38f9e76 | |||
| 57ea08be2a | |||
| 10fc0af2c0 | |||
| 524c20db8b | |||
| 2ed2a1c9dc | |||
| ac403f57bc | |||
| 6a41d269b9 | |||
| 1bb470517c | |||
| c545aa6220 | |||
| e006837e1c | |||
| f9dbe9526d | |||
| 738f1e6d4e | |||
| 42ca999e0b | |||
| f35853c1d3 | |||
| b586b7a1f9 | |||
| 24e2572ebd | |||
| 0ac0a106c0 | |||
| b2b7822f6a | |||
| fbf76cbedc | |||
| b557386c4b | |||
| e04a551185 | |||
| 0e81918345 | |||
| 8de934767d | |||
| 8b3b23deff | |||
| 8a4ab4438c | |||
| 6daa340044 | |||
| b4d4fa65f8 | |||
| 2e2465dd25 | |||
| 5d2c3e749f | |||
| fb0f8e9291 | |||
| bdfa01459c |
@@ -28,6 +28,9 @@
|
||||
# that runs identically on either, and because a quota left switched on would
|
||||
# cap every suite sharing the environment — so it must be proved to restore
|
||||
# itself wherever it runs, not only on one node.
|
||||
# route53 belongs to both topologies because every node serves every hosted
|
||||
# zone from one shared bucket: on one node it proves the zone loads at all, on
|
||||
# three that each node's sync reaches the same set.
|
||||
# storagefault runs last in both lists and belongs to both topologies, for
|
||||
# different reasons. On one node it is RS(1,0), so freezing the only predastore
|
||||
# is a total outage and the guest-survival assertion is unambiguous. On three it
|
||||
@@ -37,7 +40,7 @@
|
||||
# received. It is last because it SIGSTOPs a cluster-wide
|
||||
# service: anything sharing the environment would see the same outage and report
|
||||
# a failure that says nothing about itself.
|
||||
E2E_SUITES_SINGLE="single iam cert eks ecs storagegrowth partialblock rds quota storagefault"
|
||||
E2E_SUITES_SINGLE="single iam cert eks ecs storagegrowth partialblock rds quota route53 storagefault"
|
||||
|
||||
# Suites runnable against a multi-node environment.
|
||||
#
|
||||
@@ -57,7 +60,7 @@ E2E_SUITES_SINGLE="single iam cert eks ecs storagegrowth partialblock rds quota
|
||||
# replicated rather than sitting on one node, so it runs before the pair that
|
||||
# take a node away — a cluster whose buckets are already wrong should report
|
||||
# that, not report a recovery failing.
|
||||
E2E_SUITES_MULTI="multinode lb cert quota kvquorum lbrecovery instancerecovery storagefault"
|
||||
E2E_SUITES_MULTI="multinode lb cert quota route53 kvquorum lbrecovery instancerecovery storagefault"
|
||||
|
||||
# grep -xE alternation form, for narrowing a requested set to the eligible one.
|
||||
E2E_SUITES_SINGLE_RE="$(printf '%s' "${E2E_SUITES_SINGLE}" | tr ' ' '|')"
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ sudo apt-get install -y shellcheck
|
||||
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b "$(go env GOPATH)/bin" v2.14.0
|
||||
```
|
||||
|
||||
The web console additionally needs Node.js 24 and `pnpm`.
|
||||
The web console additionally needs `pnpm` and the Node.js version pinned in `spinifex/runtime/roles/spinifexui/frontend/.nvmrc`.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
|
||||
@@ -10,28 +10,28 @@ type allowance struct {
|
||||
// docs/PACKAGE_BOUNDARY_MIGRATION.md. It only shrinks: delete an entry as soon
|
||||
// as its use is gone, and never add one to make a new consumer pass.
|
||||
var allowlist = []allowance{
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.LocalStatePath", Inv: "INV-01"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.ReadLocalState", Inv: "INV-01"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.MarshalLocalState", Inv: "INV-01"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.WriteLocalStateBytes", Inv: "INV-01"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.InitKVBucket", Inv: "INV-02"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.InitTerminatedInstanceBucket", Inv: "INV-02"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.WriteNodeMarkerBestEffort", Inv: "INV-03"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.WriteRunningSet", Inv: "INV-03"},
|
||||
{File: "spinifex/daemon/instance_record_repair.go", Symbol: "JetStreamManager.WriteRunningSet", Inv: "INV-03"},
|
||||
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-04"},
|
||||
{File: "spinifex/runtime/daemon/daemon.go", Symbol: "daemon.LocalStatePath", Inv: "INV-01"},
|
||||
{File: "spinifex/runtime/daemon/daemon.go", Symbol: "daemon.ReadLocalState", Inv: "INV-01"},
|
||||
{File: "spinifex/runtime/daemon/daemon.go", Symbol: "daemon.MarshalLocalState", Inv: "INV-01"},
|
||||
{File: "spinifex/runtime/daemon/daemon.go", Symbol: "daemon.WriteLocalStateBytes", Inv: "INV-01"},
|
||||
{File: "spinifex/runtime/daemon/daemon.go", Symbol: "JetStreamManager.InitKVBucket", Inv: "INV-02"},
|
||||
{File: "spinifex/runtime/daemon/daemon.go", Symbol: "JetStreamManager.InitTerminatedInstanceBucket", Inv: "INV-02"},
|
||||
{File: "spinifex/runtime/daemon/daemon.go", Symbol: "JetStreamManager.WriteNodeMarkerBestEffort", Inv: "INV-03"},
|
||||
{File: "spinifex/runtime/daemon/daemon.go", Symbol: "JetStreamManager.WriteRunningSet", Inv: "INV-03"},
|
||||
{File: "spinifex/runtime/daemon/instance_record_repair.go", Symbol: "JetStreamManager.WriteRunningSet", Inv: "INV-03"},
|
||||
{File: "spinifex/runtime/daemon/daemon.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-04"},
|
||||
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.LoadState", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.WriteStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.LoadStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.DeleteStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.UpdateStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.ClaimStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.ListStoppedInstances", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.UpdateTerminatedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-05"},
|
||||
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.DeleteTerminatedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/runtime/daemon/vm_adapters.go", Symbol: "JetStreamManager.LoadState", Inv: "INV-05"},
|
||||
{File: "spinifex/runtime/daemon/vm_adapters.go", Symbol: "JetStreamManager.WriteStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/runtime/daemon/vm_adapters.go", Symbol: "JetStreamManager.LoadStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/runtime/daemon/vm_adapters.go", Symbol: "JetStreamManager.DeleteStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/runtime/daemon/vm_adapters.go", Symbol: "JetStreamManager.UpdateStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/runtime/daemon/vm_adapters.go", Symbol: "JetStreamManager.ClaimStoppedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/runtime/daemon/vm_adapters.go", Symbol: "JetStreamManager.ListStoppedInstances", Inv: "INV-05"},
|
||||
{File: "spinifex/runtime/daemon/vm_adapters.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/runtime/daemon/vm_adapters.go", Symbol: "JetStreamManager.UpdateTerminatedInstance", Inv: "INV-05"},
|
||||
{File: "spinifex/runtime/daemon/vm_adapters.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-05"},
|
||||
{File: "spinifex/runtime/daemon/vm_adapters.go", Symbol: "JetStreamManager.DeleteTerminatedInstance", Inv: "INV-05"},
|
||||
|
||||
{File: "spinifex/runtime/compute/vm/migrate.go", Symbol: "JetStreamManager.WriteStoppedInstance", Inv: "INV-06"},
|
||||
{File: "spinifex/runtime/compute/vm/migrate.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-06"},
|
||||
@@ -50,23 +50,23 @@ var allowlist = []allowance{
|
||||
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-07"},
|
||||
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.DeleteStoppedInstance", Inv: "INV-07"},
|
||||
|
||||
{File: "spinifex/daemon/daemon_handlers_image.go", Symbol: "JetStreamManager.LoadStoppedInstance", Inv: "INV-08"},
|
||||
{File: "spinifex/daemon/daemon_handlers_image.go", Symbol: "JetStreamManager.UpdateStoppedInstance", Inv: "INV-08"},
|
||||
{File: "spinifex/runtime/daemon/daemon_handlers_image.go", Symbol: "JetStreamManager.LoadStoppedInstance", Inv: "INV-08"},
|
||||
{File: "spinifex/runtime/daemon/daemon_handlers_image.go", Symbol: "JetStreamManager.UpdateStoppedInstance", Inv: "INV-08"},
|
||||
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "vm.InstanceRecord", Inv: "INV-10"},
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-10"},
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.ClaimRecoverableInstance", Inv: "INV-10"},
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.ReleaseRecoveredInstance", Inv: "INV-10"},
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.AbandonRecovery", Inv: "INV-10"},
|
||||
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.LoadInstanceRecord", Inv: "INV-10"},
|
||||
{File: "spinifex/runtime/daemon/instance_recovery.go", Symbol: "vm.InstanceRecord", Inv: "INV-10"},
|
||||
{File: "spinifex/runtime/daemon/instance_recovery.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-10"},
|
||||
{File: "spinifex/runtime/daemon/instance_recovery.go", Symbol: "JetStreamManager.ClaimRecoverableInstance", Inv: "INV-10"},
|
||||
{File: "spinifex/runtime/daemon/instance_recovery.go", Symbol: "JetStreamManager.ReleaseRecoveredInstance", Inv: "INV-10"},
|
||||
{File: "spinifex/runtime/daemon/instance_recovery.go", Symbol: "JetStreamManager.AbandonRecovery", Inv: "INV-10"},
|
||||
{File: "spinifex/runtime/daemon/instance_recovery.go", Symbol: "JetStreamManager.LoadInstanceRecord", Inv: "INV-10"},
|
||||
|
||||
{File: "spinifex/daemon/dns_reconcile.go", Symbol: "daemon.InstanceRecordPrefix", Inv: "INV-11"},
|
||||
{File: "spinifex/daemon/dns_reconcile.go", Symbol: "daemon.InstanceStateBucket", Inv: "INV-11"},
|
||||
{File: "spinifex/daemon/dns_reconcile.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-11"},
|
||||
{File: "spinifex/runtime/daemon/dns_reconcile.go", Symbol: "daemon.InstanceRecordPrefix", Inv: "INV-11"},
|
||||
{File: "spinifex/runtime/daemon/dns_reconcile.go", Symbol: "daemon.InstanceStateBucket", Inv: "INV-11"},
|
||||
{File: "spinifex/runtime/daemon/dns_reconcile.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-11"},
|
||||
|
||||
{File: "spinifex/daemon/eni_orphan_reaper.go", Symbol: "vm.InstanceRecord", Inv: "INV-12"},
|
||||
{File: "spinifex/daemon/eni_orphan_reaper.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-12"},
|
||||
{File: "spinifex/daemon/eni_orphan_reaper.go", Symbol: "JetStreamManager.ListTerminatedInstanceRecords", Inv: "INV-12"},
|
||||
{File: "spinifex/runtime/daemon/eni_orphan_reaper.go", Symbol: "vm.InstanceRecord", Inv: "INV-12"},
|
||||
{File: "spinifex/runtime/daemon/eni_orphan_reaper.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-12"},
|
||||
{File: "spinifex/runtime/daemon/eni_orphan_reaper.go", Symbol: "JetStreamManager.ListTerminatedInstanceRecords", Inv: "INV-12"},
|
||||
|
||||
{File: "spinifex/domains/ec2/instance/state/imds_readers.go", Symbol: "daemon.LocalState", Inv: "INV-13"},
|
||||
{File: "spinifex/domains/ec2/instance/state/imds_readers.go", Symbol: "daemon.LocalStatePath", Inv: "INV-13"},
|
||||
|
||||
@@ -95,7 +95,7 @@ const faultSource = `package %s
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
spxd "github.com/mulgadc/spinifex/spinifex/daemon"
|
||||
spxd "github.com/mulgadc/spinifex/spinifex/runtime/daemon"
|
||||
"github.com/mulgadc/spinifex/spinifex/runtime/compute/vm"
|
||||
)
|
||||
|
||||
@@ -176,11 +176,11 @@ func TestADR0007_S6_RatchetDetectsFaults(t *testing.T) {
|
||||
// must see those too.
|
||||
func TestADR0007_S6_RatchetSeesBareIdentifiersInOwnerPackages(t *testing.T) {
|
||||
src := map[string]string{
|
||||
"spinifex/daemon/new_consumer.go": "package daemon\n\nfunc f() string { return InstanceStateBucket + LocalStatePath(\"\") }\n",
|
||||
"spinifex/runtime/daemon/new_consumer.go": "package daemon\n\nfunc f() string { return InstanceStateBucket + LocalStatePath(\"\") }\n",
|
||||
"spinifex/runtime/compute/vm/new_reader.go": "package vm\n\nfunc g(r *InstanceRecord) *VM { return VMFromRecord(r) }\n",
|
||||
}
|
||||
want := map[string][]string{
|
||||
"spinifex/daemon/new_consumer.go": {"daemon.InstanceStateBucket", "daemon.LocalStatePath"},
|
||||
"spinifex/runtime/daemon/new_consumer.go": {"daemon.InstanceStateBucket", "daemon.LocalStatePath"},
|
||||
"spinifex/runtime/compute/vm/new_reader.go": {"vm.InstanceRecord", "vm.VMFromRecord"},
|
||||
}
|
||||
for rel, code := range src {
|
||||
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
daemonPath = "github.com/mulgadc/spinifex/spinifex/daemon"
|
||||
daemonPath = "github.com/mulgadc/spinifex/spinifex/runtime/daemon"
|
||||
vmPath = "github.com/mulgadc/spinifex/spinifex/runtime/compute/vm"
|
||||
daemonDir = "spinifex/daemon"
|
||||
daemonDir = "spinifex/runtime/daemon"
|
||||
vmDir = "spinifex/runtime/compute/vm"
|
||||
)
|
||||
|
||||
@@ -69,12 +69,12 @@ var literals = set(`"spinifex-instance-state"`, `"spinifex-terminated-instances"
|
||||
// ownerFiles define the symbols above. They are the implementation being
|
||||
// migrated, not consumers of it, and are listed so a deleted one is noticed.
|
||||
var ownerFiles = []string{
|
||||
"spinifex/daemon/instance_membership.go",
|
||||
"spinifex/daemon/instance_records.go",
|
||||
"spinifex/daemon/instance_records_migrate.go",
|
||||
"spinifex/daemon/instance_running_set.go",
|
||||
"spinifex/daemon/jetstream.go",
|
||||
"spinifex/daemon/local_state.go",
|
||||
"spinifex/runtime/daemon/instance_membership.go",
|
||||
"spinifex/runtime/daemon/instance_records.go",
|
||||
"spinifex/runtime/daemon/instance_records_migrate.go",
|
||||
"spinifex/runtime/daemon/instance_running_set.go",
|
||||
"spinifex/runtime/daemon/jetstream.go",
|
||||
"spinifex/runtime/daemon/local_state.go",
|
||||
"spinifex/runtime/compute/vm/record.go",
|
||||
}
|
||||
|
||||
|
||||
@@ -11,62 +11,62 @@ type allowance struct {
|
||||
// docs/PACKAGE_BOUNDARY_MIGRATION.md. It only shrinks: add an edge only with
|
||||
// recorded debt, and delete an entry as soon as its import is gone.
|
||||
var allowlist = []allowance{
|
||||
{From: "accountteardown", To: "domains/ec2/awsapi/eip", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "accountteardown", To: "domains/ec2/awsapi/igw", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "accountteardown", To: "domains/ec2/awsapi/image", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "accountteardown", To: "domains/ec2/awsapi/instance", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "accountteardown", To: "domains/ec2/awsapi/key", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "accountteardown", To: "domains/ec2/awsapi/launchtemplate", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "accountteardown", To: "domains/ec2/awsapi/placementgroup", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "accountteardown", To: "domains/ec2/awsapi/routetable", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "accountteardown", To: "domains/ec2/awsapi/snapshot", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{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: "operator/accountlifecycle", To: "domains/ec2/awsapi/eip", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "operator/accountlifecycle", To: "domains/ec2/awsapi/igw", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "operator/accountlifecycle", To: "domains/ec2/awsapi/image", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "operator/accountlifecycle", To: "domains/ec2/awsapi/instance", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "operator/accountlifecycle", To: "domains/ec2/awsapi/key", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "operator/accountlifecycle", To: "domains/ec2/awsapi/launchtemplate", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "operator/accountlifecycle", To: "domains/ec2/awsapi/placementgroup", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "operator/accountlifecycle", To: "domains/ec2/awsapi/routetable", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "operator/accountlifecycle", To: "domains/ec2/awsapi/snapshot", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "operator/accountlifecycle", To: "domains/ec2/awsapi/volume", Debt: "accountteardown (dependency)", Reason: "EC2 reapers call the awsapi action adapters; needs domain-owned cleanup capabilities"},
|
||||
{From: "operator/accountlifecycle", 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: "handlers/eks", Debt: "agents/eks/tokenwebhook", Reason: "WebhookTokenReviewResult from the EKS implementation; needs an EKS contract or projection"},
|
||||
{From: "agents/eks/tokenwebhook", To: "domains/eks/cluster", Debt: "agents/eks/tokenwebhook", Reason: "WebhookTokenReviewResult from the EKS implementation; needs an EKS contract or projection"},
|
||||
|
||||
{From: "agents/rds/agent", To: "handlers/rds", Debt: "agents/rds/agent (dependency, narrowed)", Reason: "command protocol, wire and record types; needs a guest/controller wire contract"},
|
||||
{From: "agents/rds/agent", To: "domains/rds/instance", Debt: "agents/rds/agent (dependency, narrowed)", Reason: "command protocol, wire and record types; needs a guest/controller wire contract"},
|
||||
|
||||
{From: "cmd/aws-model-coverage", To: "domains/acm/awsapi", Debt: "cmd/aws-model-coverage", Reason: "reads declared ECR/ACM inventories; named exception, must not construct a registration"},
|
||||
{From: "cmd/aws-model-coverage", To: "domains/ecr/awsapi", Debt: "cmd/aws-model-coverage", Reason: "reads declared ECR/ACM inventories; named exception, must not construct a registration"},
|
||||
|
||||
{From: "daemon", To: "domains/ec2/awsapi/instance", Debt: "Instance adapter consumers outside EC2", Reason: "daemon/eks_cp_control.go and rds_deps.go call the instance adapter; needs an EC2 capability"},
|
||||
{From: "runtime/daemon", To: "domains/ec2/awsapi/instance", Debt: "Instance adapter consumers outside EC2", Reason: "runtime/daemon/eks_cp_control.go and rds_deps.go call the instance adapter; needs an EC2 capability"},
|
||||
|
||||
{From: "domains/acm", To: "handlers/iam", Debt: "ACM: the IAM AES-GCM helper import (ACM relocation residual)", Reason: "IAM AES-GCM helpers; needs a shared crypto boundary"},
|
||||
{From: "domains/acm", To: "domains/iam", Debt: "ACM: the IAM AES-GCM helper import (ACM relocation residual)", Reason: "IAM AES-GCM helpers; needs a shared crypto boundary"},
|
||||
|
||||
{From: "domains/admission/quota", To: "domains/ec2/awsapi/eip", Debt: "domains/admission/quota (dependency, moved from handlers/quota unchanged)", Reason: "describe adapter used as a counter; needs a reader capability"},
|
||||
{From: "domains/admission/quota", To: "domains/ec2/awsapi/instance", Debt: "domains/admission/quota (dependency, moved from handlers/quota unchanged)", Reason: "vcpu.go counts instances through the instance adapter; needs an EC2 reader capability"},
|
||||
{From: "domains/admission/quota", To: "domains/ec2/awsapi/volume", Debt: "domains/admission/quota (dependency, moved from handlers/quota unchanged)", Reason: "describe adapter used as a counter; needs a reader capability"},
|
||||
{From: "domains/admission/quota", To: "domains/ec2/awsapi/vpc", Debt: "domains/admission/quota (dependency, moved from handlers/quota unchanged)", Reason: "describe adapter used as a counter; needs a reader capability"},
|
||||
{From: "domains/admission/quota", To: "domains/ec2/instancetypes", Debt: "domains/admission/quota → domains/ec2/instancetypes", Reason: "DefaultVCPUs sizing; needs a consumer-owned capability or EC2 projection"},
|
||||
{From: "domains/admission/quota", To: "gateway/elbv2", Debt: "domains/admission/quota (dependency, moved from handlers/quota unchanged)", Reason: "live ELBv2 counter; needs a reader capability"},
|
||||
{From: "domains/admission/quota", To: "handlers/rds", Debt: "domains/admission/quota (dependency, moved from handlers/quota unchanged)", Reason: "live RDS counter; needs a reader capability"},
|
||||
{From: "domains/admission/quota", To: "domains/elbv2/awsapi", Debt: "domains/admission/quota (dependency, moved from handlers/quota unchanged)", Reason: "live ELBv2 counter; needs a reader capability"},
|
||||
{From: "domains/admission/quota", To: "domains/rds/instance", Debt: "domains/admission/quota (dependency, moved from handlers/quota unchanged)", Reason: "live RDS counter; needs a reader capability"},
|
||||
{From: "domains/admission/quota", To: "runtime/compute/vm", Debt: "domains/admission/quota (dependency, moved from handlers/quota unchanged)", Reason: "instance-record vCPU sweep; needs a reader capability"},
|
||||
|
||||
{From: "domains/dns", To: "domains/network/reconcile", Debt: "domains/dns", Reason: "takes the reconcile lock from network's implementation"},
|
||||
{From: "domains/dns", To: "runtime/compute/vm", Debt: "domains/dns", Reason: "domain-to-runtime import of VM state"},
|
||||
|
||||
{From: "domains/ec2/awsapi/instance", To: "handlers/iam", Debt: "domains/ec2/awsapi/instance (dependency)", Reason: "IAM edge needs an IAM capability"},
|
||||
{From: "domains/ec2/awsapi/instance", To: "domains/iam", Debt: "domains/ec2/awsapi/instance (dependency)", Reason: "IAM edge needs an IAM capability"},
|
||||
{From: "domains/ec2/awsapi/instance", To: "runtime/compute/cache", Debt: "domains/ec2/awsapi/instance (dependency)", Reason: "node liveness and VM state; needs an EC2-owned projection"},
|
||||
{From: "domains/ec2/awsapi/instance", To: "runtime/compute/vm", Debt: "domains/ec2/awsapi/instance (dependency)", Reason: "node liveness and VM state; needs an EC2-owned projection"},
|
||||
|
||||
{From: "domains/ec2/awsapi/spotinstance", To: "domains/admission/quota", Debt: "domains/ec2/awsapi/spotinstance (dependency)", Reason: "RequestSpotInstances.go uses IAMService and quota.Service"},
|
||||
{From: "domains/ec2/awsapi/spotinstance", To: "handlers/iam", Debt: "domains/ec2/awsapi/spotinstance (dependency)", Reason: "RequestSpotInstances.go uses IAMService and quota.Service"},
|
||||
{From: "domains/ec2/awsapi/spotinstance", To: "domains/iam", Debt: "domains/ec2/awsapi/spotinstance (dependency)", Reason: "RequestSpotInstances.go uses IAMService and quota.Service"},
|
||||
|
||||
{From: "domains/ec2/eip", To: "domains/network/topology", Debt: "EC2 → network implementation", Reason: "EC2 reads network implementation; clears with the EC2-owned projection"},
|
||||
|
||||
{From: "domains/ec2/guestmetadata", To: "domains/dns", Debt: "Guest metadata: the cross-domain and runtime imports above", Reason: "guest-metadata relocation residual; needs capabilities, projections or contracts"},
|
||||
{From: "domains/ec2/guestmetadata", To: "handlers/iam", Debt: "Guest metadata: the cross-domain and runtime imports above", Reason: "guest-metadata relocation residual; needs capabilities, projections or contracts"},
|
||||
{From: "domains/ec2/guestmetadata", To: "handlers/sts", Debt: "Guest metadata: the cross-domain and runtime imports above", Reason: "guest-metadata relocation residual; needs capabilities, projections or contracts"},
|
||||
{From: "domains/ec2/guestmetadata", To: "domains/iam", Debt: "Guest metadata: the cross-domain and runtime imports above", Reason: "guest-metadata relocation residual; needs capabilities, projections or contracts"},
|
||||
{From: "domains/ec2/guestmetadata", To: "domains/sts", Debt: "Guest metadata: the cross-domain and runtime imports above", Reason: "guest-metadata relocation residual; needs capabilities, projections or contracts"},
|
||||
{From: "domains/ec2/guestmetadata", To: "runtime/compute/vm", Debt: "Guest metadata: the cross-domain and runtime imports above", Reason: "guest-metadata relocation residual; needs capabilities, projections or contracts"},
|
||||
|
||||
{From: "domains/ec2/instance", To: "domains/dns", Debt: "domains/ec2/instance", Reason: "cross-domain implementation import of DNS"},
|
||||
{From: "domains/ec2/instance", To: "domains/network/topology", Debt: "EC2 → network implementation", Reason: "EC2 reads network implementation; clears with the EC2-owned projection"},
|
||||
{From: "domains/ec2/instance", To: "runtime/compute/gpu", Debt: "domains/ec2/instance", Reason: "VM/GPU runtime state read directly; needs a domain-owned projection"},
|
||||
{From: "domains/ec2/instance", To: "runtime/compute/vm", Debt: "domains/ec2/instance", Reason: "VM/GPU runtime state read directly; needs a domain-owned projection"},
|
||||
{From: "domains/ec2/instance/state", To: "daemon", Debt: "domains/ec2/instance/state (dependency, IMDS readers)", Reason: "wraps the daemon's local-file reader and JetStreamManager bucket open unchanged for vpcd's IMDS lookup"},
|
||||
{From: "domains/ec2/instance/state", To: "runtime/daemon", Debt: "domains/ec2/instance/state (dependency, IMDS readers)", Reason: "wraps the daemon's local-file reader and JetStreamManager bucket open unchanged for vpcd's IMDS lookup"},
|
||||
{From: "domains/ec2/instance/state", To: "runtime/compute/vm", Debt: "domains/ec2/instance/state (dependency)", Reason: "the vpcd IMDS local reader returns vm.VM"},
|
||||
|
||||
{From: "domains/ec2/volume", To: "runtime/compute/vm", Debt: "Domain → runtime", Reason: "VM runtime state read directly; needs a domain-owned projection"},
|
||||
@@ -76,7 +76,7 @@ var allowlist = []allowance{
|
||||
{From: "domains/ec2/vpc", To: "domains/network/identifiers", Debt: "EC2 → network implementation", Reason: "EC2 reads network implementation; clears with the EC2-owned projection"},
|
||||
{From: "domains/ec2/vpc", To: "domains/network/topology", Debt: "EC2 → network implementation", Reason: "EC2 reads network implementation; clears with the EC2-owned projection"},
|
||||
|
||||
{From: "domains/ecr/auth", To: "handlers/iam", Debt: "Cross-domain", Reason: "IAM implementation import; needs an IAM capability"},
|
||||
{From: "domains/ecr/auth", To: "domains/iam", Debt: "Cross-domain", Reason: "IAM implementation import; needs an IAM capability"},
|
||||
|
||||
{From: "domains/network/host", To: "runtime/compute/vm", Debt: "domains/network", Reason: "domain-to-runtime import"},
|
||||
{From: "domains/network/host", To: "runtime/host/command", Debt: "domains/network", Reason: "domain-to-runtime import"},
|
||||
@@ -89,11 +89,12 @@ var allowlist = []allowance{
|
||||
{From: "domains/network/reconcile", To: "runtime/compute/vm", Debt: "domains/network", Reason: "domain-to-runtime import"},
|
||||
|
||||
{From: "domains/ochre", To: "domains/ec2/systeminstance", Debt: "Cross-domain", Reason: "consumes another domain's implementation rather than a capability"},
|
||||
{From: "domains/ochre", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "secret encrypt/decrypt helpers; needs a shared crypto boundary"},
|
||||
{From: "domains/ochre", To: "domains/network/systemvpc", Debt: "Cross-domain", Reason: "consumes another domain's implementation rather than a capability"},
|
||||
{From: "domains/ochre", To: "gateway/bedrock", Debt: "gateway/bedrock", Reason: "Ochre domain core still lives in gateway/bedrock; needs the recorded split"},
|
||||
{From: "domains/ochre", To: "ingress/aws/bodyscope", Debt: "domains/ochre (action adapters)", Reason: "Bedrock action adapters still live in the Ochre package; they move to domains/ochre/awsapi"},
|
||||
{From: "domains/ochre", To: "runtime/compute/gpu", Debt: "Domain → runtime", Reason: "GPU runtime state read directly; needs a domain-owned projection"},
|
||||
|
||||
{From: "domains/ochre/vector", To: "handlers/iam", Debt: "Cross-domain", Reason: "consumes another domain's implementation rather than a capability"},
|
||||
{From: "domains/ochre/vector", To: "domains/iam", Debt: "Cross-domain", Reason: "consumes another domain's implementation rather than a capability"},
|
||||
|
||||
{From: "gateway", To: "domains/ec2/awsapi", Debt: "gateway/ec2.go (registration)", Reason: "EC2 dispatch map stays in gateway until the ingress registration seam"},
|
||||
{From: "gateway", To: "domains/ec2/awsapi/account", Debt: "gateway/ec2.go (registration)", Reason: "EC2 dispatch map stays in gateway until the ingress registration seam"},
|
||||
@@ -116,49 +117,48 @@ var allowlist = []allowance{
|
||||
{From: "gateway", To: "domains/ec2/awsapi/vpc", Debt: "gateway/ec2.go (registration)", Reason: "EC2 dispatch map stays in gateway until the ingress registration seam"},
|
||||
{From: "gateway", To: "domains/ec2/awsapi/zone", Debt: "gateway/ec2.go (registration)", Reason: "EC2 dispatch map stays in gateway until the ingress registration seam"},
|
||||
|
||||
{From: "gateway/bedrock", To: "handlers/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "secret encrypt/decrypt helpers; needs a shared crypto boundary"},
|
||||
{From: "domains/rds/awsapi", To: "domains/ec2/awsapi/instance", Debt: "Instance adapter consumers outside EC2", Reason: "enginecatalog.go calls the instance adapter; needs an EC2 capability"},
|
||||
|
||||
{From: "gateway/rds", To: "domains/ec2/awsapi/instance", Debt: "Instance adapter consumers outside EC2", Reason: "enginecatalog.go calls the instance adapter; needs an EC2 capability"},
|
||||
{From: "domains/sts/awsapi", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "GetCallerIdentity takes IAMService; needs an IAM capability"},
|
||||
|
||||
{From: "gateway/sts", To: "handlers/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "GetCallerIdentity takes IAMService; needs an IAM capability"},
|
||||
{From: "gateway", To: "domains/eks/awsapi", Debt: "gateway (service registration)", Reason: "IAM, STS, EKS and RDS dispatch stays in gateway until the ingress registration seam"},
|
||||
{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: "gateway", To: "domains/ecs/awsapi", Debt: "gateway (service registration)", Reason: "ECS and ELBv2 dispatch stays in gateway until the ingress registration seam"},
|
||||
{From: "gateway", To: "domains/elbv2/awsapi", Debt: "gateway (service registration)", Reason: "ECS and ELBv2 dispatch stays in gateway until the ingress registration seam"},
|
||||
{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: "handlers/ecs", To: "domains/ec2/ebs/metadata", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
|
||||
{From: "handlers/ecs", To: "domains/ec2/eip", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
|
||||
{From: "handlers/ecs", To: "domains/ec2/instancetypes", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
|
||||
{From: "handlers/ecs", To: "handlers/elbv2", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
|
||||
{From: "handlers/ecs", To: "handlers/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"},
|
||||
{From: "domains/eks/cluster", To: "domains/ec2/instancetypes", 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/placementgroup", 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/systeminstance", 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/network/systemvpc", 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/iam", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
|
||||
{From: "domains/eks/cluster", To: "runtime/compute/vm", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
|
||||
|
||||
{From: "handlers/eks", 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: "handlers/eks", 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"},
|
||||
{From: "handlers/eks", To: "domains/ec2/instancetypes", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
|
||||
{From: "handlers/eks", To: "domains/ec2/placementgroup", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
|
||||
{From: "handlers/eks", To: "domains/ec2/systeminstance", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
|
||||
{From: "handlers/eks", To: "domains/network/systemvpc", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
|
||||
{From: "handlers/eks", To: "handlers/iam", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
|
||||
{From: "handlers/eks", To: "runtime/compute/vm", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
|
||||
{From: "domains/elbv2", To: "domains/acm", Debt: "domains/elbv2 (cross-domain)", Reason: "ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
|
||||
{From: "domains/elbv2", To: "domains/dns", Debt: "domains/elbv2 (cross-domain)", Reason: "ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
|
||||
{From: "domains/elbv2", To: "domains/ec2/systeminstance", Debt: "domains/elbv2 (cross-domain)", Reason: "ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
|
||||
{From: "domains/elbv2", To: "domains/ec2/vpc", Debt: "domains/elbv2 (cross-domain)", Reason: "ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
|
||||
{From: "domains/elbv2", To: "domains/network/topology", Debt: "domains/elbv2 (cross-domain)", Reason: "ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
|
||||
{From: "domains/elbv2", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "system-instance role ensurer; needs an IAM capability"},
|
||||
{From: "domains/elbv2", To: "agents/elbv2", Debt: "domains/elbv2/{agent_types,haproxy,health_checker,nginx}.go import lbagent", Reason: "health-report and cert/PID-path vocabulary; needs a guest/controller contract"},
|
||||
|
||||
{From: "handlers/elbv2", To: "domains/acm", Debt: "handlers/elbv2 (cross-domain)", Reason: "legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
|
||||
{From: "handlers/elbv2", To: "domains/dns", Debt: "handlers/elbv2 (cross-domain)", Reason: "legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
|
||||
{From: "handlers/elbv2", To: "domains/ec2/systeminstance", Debt: "handlers/elbv2 (cross-domain)", Reason: "legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
|
||||
{From: "handlers/elbv2", To: "domains/ec2/vpc", Debt: "handlers/elbv2 (cross-domain)", Reason: "legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
|
||||
{From: "handlers/elbv2", To: "domains/network/topology", Debt: "handlers/elbv2 (cross-domain)", Reason: "legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
|
||||
{From: "handlers/elbv2", To: "handlers/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "system-instance role ensurer; needs an IAM capability"},
|
||||
{From: "handlers/elbv2", To: "lbagent", Debt: "handlers/elbv2/{agent_types,haproxy,health_checker,nginx}.go import lbagent", Reason: "health-report and cert/PID-path vocabulary; needs a guest/controller contract"},
|
||||
{From: "domains/rds/instance", To: "domains/dns", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "domains/rds/instance", To: "domains/ec2/instance", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "domains/rds/instance", To: "domains/ec2/instancetypes", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "domains/rds/instance", To: "domains/ec2/systeminstance", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "domains/rds/instance", To: "domains/network/systemvpc", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "domains/rds/instance", To: "domains/iam", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "domains/rds/instance", To: "runtime/compute/vm", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
|
||||
{From: "handlers/rds", To: "domains/dns", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "handlers/rds", To: "domains/ec2/instance", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "handlers/rds", To: "domains/ec2/instancetypes", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "handlers/rds", To: "domains/ec2/systeminstance", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "handlers/rds", To: "domains/network/systemvpc", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "handlers/rds", To: "handlers/iam", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "handlers/rds", To: "runtime/compute/vm", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
|
||||
{From: "domains/sts", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "STS reads IAM service, trust-policy and OIDC-provider state; needs an IAM capability"},
|
||||
|
||||
{From: "handlers/sts", To: "handlers/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "STS reads IAM service, trust-policy and OIDC-provider state; needs an IAM capability"},
|
||||
|
||||
{From: "operator/cli", To: "accountteardown", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
|
||||
{From: "operator/cli", To: "daemon", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
|
||||
{From: "operator/cli", To: "gateway", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
|
||||
{From: "operator/cli", To: "gateway/bedrock", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
|
||||
{From: "operator/cli", To: "handlers/eks", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
|
||||
{From: "operator/cli", To: "handlers/iam", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
|
||||
}
|
||||
|
||||
@@ -191,3 +191,25 @@ func TestADR0001_LayeringCheckerDetectsFaults(t *testing.T) {
|
||||
t.Errorf("allow-listed cross-domain edge %s was not detected by R3, so the allowlist entry would read as stale", allowedEdge)
|
||||
}
|
||||
}
|
||||
|
||||
// TestADR0001_InterimDaemonDirectionCoversRuntimeDaemon: the daemon left its
|
||||
// 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) {
|
||||
daemon, _ := classify("spinifex/runtime/daemon")
|
||||
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"); ruleByID("Interim_DaemonGatewayHandlersDirection").forbids(gw, other, "spinifex/gateway") {
|
||||
t.Error("gateway -> runtime/host/process is rejected, but it is not the daemon")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ type class struct {
|
||||
legacy string // legacy root name when layer is layerLegacy
|
||||
domain string // owning domain for domain-shaped and agent packages
|
||||
kind domainKind
|
||||
daemon bool // runtime/daemon, the interim daemon -> gateway -> handlers root
|
||||
}
|
||||
|
||||
func (c class) domainShaped() bool { return c.kind != notDomain }
|
||||
@@ -59,8 +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, "daemon": true,
|
||||
"accountteardown": true, "lbagent": true,
|
||||
"gateway": true,
|
||||
}
|
||||
|
||||
// gatewayDomains names the domain each gateway/<service> adapter belongs to;
|
||||
@@ -70,13 +70,6 @@ var gatewayDomains = map[string]string{
|
||||
"rds": "rds", "sts": "sts",
|
||||
}
|
||||
|
||||
// runtimeLegacy and operatorLegacy are the legacy roots whose ADR-0001 target
|
||||
// is runtime/ and operator/ respectively.
|
||||
var (
|
||||
runtimeLegacy = []string{"daemon"}
|
||||
operatorLegacy = []string{"accountteardown"}
|
||||
)
|
||||
|
||||
// classify maps a module-relative package directory to its layer. ok is false
|
||||
// for a package that has no deliberate classification.
|
||||
func classify(full string) (class, bool) {
|
||||
@@ -112,7 +105,7 @@ func classifyApp(app []string) (class, bool) {
|
||||
case "ingress":
|
||||
return class{layer: layerIngress}, true
|
||||
case "runtime":
|
||||
return class{layer: layerRuntime}, true
|
||||
return class{layer: layerRuntime, daemon: len(app) >= 2 && app[1] == "daemon"}, true
|
||||
case "operator":
|
||||
return class{layer: layerOperator}, true
|
||||
case "providers":
|
||||
|
||||
@@ -10,30 +10,13 @@ import (
|
||||
// It only shrinks: a new entry is a new legacy package, a missing one is a
|
||||
// completed move whose line must be deleted.
|
||||
var frozenLegacyPackages = []string{
|
||||
"accountteardown",
|
||||
"daemon",
|
||||
"gateway",
|
||||
"gateway/bedrock",
|
||||
"gateway/ecs",
|
||||
"gateway/eks",
|
||||
"gateway/elbv2",
|
||||
"gateway/iam",
|
||||
"gateway/rds",
|
||||
"gateway/spx",
|
||||
"gateway/sts",
|
||||
"handlers/ecs",
|
||||
"handlers/eks",
|
||||
"handlers/elbv2",
|
||||
"handlers/iam",
|
||||
"handlers/iam/mock",
|
||||
"handlers/rds",
|
||||
"handlers/sts",
|
||||
"lbagent",
|
||||
}
|
||||
|
||||
// retiredLegacyRoots were legacy roots whose every package has moved to its
|
||||
// ADR-0001 target; recreating one would reopen a closed migration row.
|
||||
var retiredLegacyRoots = []string{"admin", "services", "utils", "vpcd"}
|
||||
var retiredLegacyRoots = []string{"accountteardown", "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.
|
||||
|
||||
@@ -72,8 +72,7 @@ var rules = []rule{
|
||||
hint: "expose the runtime or operator state through a domain-owned projection or a capability wired at the composition root",
|
||||
subject: func(f class, _ string) bool { return f.domainShaped() },
|
||||
forbids: func(f, t class, _ string) bool {
|
||||
return f.domainShaped() && (isLayer(t, layerRuntime, layerOperator) ||
|
||||
t.isLegacy(runtimeLegacy...) || t.isLegacy(operatorLegacy...))
|
||||
return f.domainShaped() && isLayer(t, layerRuntime, layerOperator)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -121,7 +120,7 @@ var rules = []rule{
|
||||
return false
|
||||
}
|
||||
switch {
|
||||
case t.layer == layerAgents || t.isLegacy("lbagent"):
|
||||
case t.layer == layerAgents:
|
||||
return true
|
||||
case t.layer == layerIngress:
|
||||
return f.kind != domainAWSAPI && f.kind != legacyAdapter
|
||||
@@ -137,9 +136,9 @@ var rules = []rule{
|
||||
id: "Agents_ImportNoDomainOrLegacyImplementation",
|
||||
clause: "area table: agents/<domain> own in-guest and service-role behaviour, not tenant API authority or another domain's private state",
|
||||
hint: "move the shared wire vocabulary behind a guest/controller contract under contracts/",
|
||||
subject: func(f class, _ string) bool { return f.layer == layerAgents || f.isLegacy("lbagent") },
|
||||
subject: func(f class, _ string) bool { return f.layer == layerAgents },
|
||||
forbids: func(f, t class, _ string) bool {
|
||||
return (f.layer == layerAgents || f.isLegacy("lbagent")) && (t.domainShaped() || t.isLegacy())
|
||||
return f.layer == layerAgents && (t.domainShaped() || t.isLegacy())
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -160,9 +159,9 @@ var rules = []rule{
|
||||
forbids: func(f, t class, _ string) bool {
|
||||
switch {
|
||||
case f.isLegacy("handlers"):
|
||||
return t.isLegacy("gateway", "daemon")
|
||||
return t.isLegacy("gateway") || t.daemon
|
||||
case f.isLegacy("gateway"):
|
||||
return t.isLegacy("daemon")
|
||||
return t.daemon
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ARG VERSION=dev
|
||||
|
||||
FROM golang:1.27.3-bookworm@sha256:5c3e9b43cfd3bb95c7937462e29d87ada57b481a96fa23ad0eb61778d7a00110 AS builder
|
||||
FROM golang:1.27.3-bookworm@sha256:972b4110ba0f47539a975c31e52c64765e1ff19f2d74469ae4a595c6676e80a5 AS builder
|
||||
|
||||
ARG VERSION
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/mulgadc/bluebottle/pkg/otelsetup"
|
||||
"github.com/mulgadc/spinifex/spinifex/lbagent"
|
||||
lbagent "github.com/mulgadc/spinifex/spinifex/agents/elbv2"
|
||||
|
||||
_ "github.com/mulgadc/bluebottle/pkg/fipsboot"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Admission contract, version 1
|
||||
|
||||
This package owns the cross-process contract for the admission domain's
|
||||
quota override. It is not an in-process service interface or the owner of
|
||||
quota state.
|
||||
|
||||
## Seal account quota override
|
||||
|
||||
`SealAccountQuotaOverrideSubject` (`admission.v1.SealAccountQuotaOverride`) is
|
||||
a request/reply route on which a gateway, in the `awsgw-quota-seal` queue
|
||||
group, irreversibly seals one account's quota override record.
|
||||
`domains/admission/quota` owns it: `awsgw` serves it.
|
||||
`accountteardown` is the consumer: it calls this subject to fence every
|
||||
override writer at the quota owner before the account record is removed.
|
||||
|
||||
The request names the account in its body (`account_id`) and may carry an
|
||||
opaque `correlation_ref` that is echoed for logs.
|
||||
`awsgw` ignores `X-Account-ID` for this subject: the account sealed is the one
|
||||
named in the body.
|
||||
A seal is refused with `IncorrectState` unless IAM already holds the account
|
||||
as `TERMINATING`; a precondition read failure, or no termination checker
|
||||
wired, answers `InternalError`.
|
||||
Both refusals write nothing and never open the override bucket.
|
||||
|
||||
A reply is sent only on success: `disposition` is always `sealed` and
|
||||
`revision` is the stream leader's accepted write for that seal.
|
||||
A failure is the standard awserrors error payload, not this reply shape.
|
||||
Sealing is idempotent: a seal on an already-sealed record re-writes the same
|
||||
value and returns a fresh, higher revision.
|
||||
Any current gateway may answer; the leader-accepted revision is the evidence
|
||||
whichever one does, and the override bucket is opened at seal time even on a
|
||||
gateway whose quotas were disabled at startup.
|
||||
|
||||
### Compatibility boundary
|
||||
|
||||
The subject name carries the contract major; `account_quota_seal_test.go`
|
||||
preserves the version-1 JSON shape.
|
||||
Producer and consumer are in the coordinated Spinifex release, so the window
|
||||
is N and N-1.
|
||||
A gateway that predates the subject has no subscription, so the request gets
|
||||
no responder.
|
||||
An incompatible change to the subject or fields needs a new major.
|
||||
@@ -0,0 +1,31 @@
|
||||
// Package admissionv1 holds the admission domain's cross-process contract:
|
||||
// the subjects, request/reply shapes and wire vocabulary the quota owner and
|
||||
// its consumers share.
|
||||
package admissionv1
|
||||
|
||||
// SealAccountQuotaOverrideSubject is the request/reply subject on which a
|
||||
// gateway seals one account's quota override record. Sealing is irreversible
|
||||
// and refused unless IAM already holds the account as TERMINATING.
|
||||
const SealAccountQuotaOverrideSubject = "admission.v1.SealAccountQuotaOverride"
|
||||
|
||||
// AccountQuotaOverrideDispositionSealed is the only disposition a successful
|
||||
// reply carries: a reply is sent only on success, so there is no other value.
|
||||
const AccountQuotaOverrideDispositionSealed = "sealed"
|
||||
|
||||
// SealAccountQuotaOverrideRequest asks for one account's quota override to be
|
||||
// sealed. CorrelationRef is an opaque caller reference echoed for logs; it
|
||||
// grants nothing and is never a fence or authority to write.
|
||||
type SealAccountQuotaOverrideRequest struct {
|
||||
AccountID string `json:"account_id"`
|
||||
CorrelationRef string `json:"correlation_ref,omitempty"`
|
||||
}
|
||||
|
||||
// SealAccountQuotaOverrideReply confirms accountID's quota override record
|
||||
// was sealed at Revision, the stream leader's accepted write for that seal.
|
||||
// A failure is the standard awserrors error payload, not this type.
|
||||
type SealAccountQuotaOverrideReply struct {
|
||||
AccountID string `json:"account_id"`
|
||||
CorrelationRef string `json:"correlation_ref,omitempty"`
|
||||
Disposition string `json:"disposition"`
|
||||
Revision uint64 `json:"revision"`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package admissionv1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSealAccountQuotaOverrideSubject(t *testing.T) {
|
||||
require.Equal(t, "admission.v1.SealAccountQuotaOverride", SealAccountQuotaOverrideSubject)
|
||||
}
|
||||
|
||||
func TestSealAccountQuotaOverrideRequestJSONContract(t *testing.T) {
|
||||
data, err := json.Marshal(SealAccountQuotaOverrideRequest{AccountID: "111122223333", CorrelationRef: "req-1"})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"account_id":"111122223333","correlation_ref":"req-1"}`, string(data))
|
||||
|
||||
data, err = json.Marshal(SealAccountQuotaOverrideRequest{AccountID: "111122223333"})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"account_id":"111122223333"}`, string(data))
|
||||
}
|
||||
|
||||
func TestSealAccountQuotaOverrideReplyJSONContract(t *testing.T) {
|
||||
reply := SealAccountQuotaOverrideReply{
|
||||
AccountID: "111122223333",
|
||||
CorrelationRef: "req-1",
|
||||
Disposition: AccountQuotaOverrideDispositionSealed,
|
||||
Revision: 42,
|
||||
}
|
||||
data, err := json.Marshal(reply)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{
|
||||
"account_id": "111122223333",
|
||||
"correlation_ref": "req-1",
|
||||
"disposition": "sealed",
|
||||
"revision": 42
|
||||
}`, string(data))
|
||||
|
||||
data, err = json.Marshal(SealAccountQuotaOverrideReply{AccountID: "111122223333", Disposition: AccountQuotaOverrideDispositionSealed})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"account_id":"111122223333","disposition":"sealed","revision":0}`, string(data))
|
||||
}
|
||||
@@ -60,3 +60,42 @@ The subject name carries the contract major; `platform_inventory_test.go` preser
|
||||
Producer and consumer are in the coordinated Spinifex release, so the window is N and N-1.
|
||||
A daemon that predates the subject has no subscription, so the request gets no responder; the consumer treats no responder, a timeout, an error reply, an undecodable reply, a reply for another account, and a missing, repeated or unknown-status kind as `scan-incomplete`, never clean.
|
||||
An incompatible change to the subject, fields or vocabulary needs a new major.
|
||||
|
||||
## Seal account settings
|
||||
|
||||
`SealAccountSettingsSubject` (`ec2.v1.SealAccountSettings`) is a request/reply route on which a daemon in the `spinifex-workers` queue group irreversibly seals one account's EC2 settings record.
|
||||
EC2 owns it: `domains/ec2/account` serves it through the daemon.
|
||||
`accountteardown`'s StageAccount seal reaper is the consumer: it calls this subject only after the seal-capability probe below reports every configured node compatible, and completes only once it holds a successful sealed reply.
|
||||
|
||||
The request names the account in its body (`account_id`) and may carry an opaque `correlation_ref` that is echoed for logs.
|
||||
The daemon ignores `X-Account-ID` for this subject: the account sealed is the one named in the body.
|
||||
A seal is refused with `IncorrectState` unless IAM already holds the account as `TERMINATING`; a precondition read failure answers `InternalError`.
|
||||
Both refusals write nothing.
|
||||
|
||||
A reply is sent only on success: `disposition` is always `sealed` and `revision` is the stream leader's accepted write for that seal.
|
||||
A failure is the standard awserrors error payload, not this reply shape.
|
||||
Sealing is idempotent: a seal on an already-sealed record re-writes the same value and returns a fresh, higher revision.
|
||||
|
||||
### Compatibility boundary
|
||||
|
||||
The subject name carries the contract major; `account_settings_seal_test.go` preserves the version-1 JSON shape.
|
||||
Producer and consumer are in the coordinated Spinifex release, so the window is N and N-1.
|
||||
A daemon that predates the subject has no subscription, so the request gets no responder.
|
||||
An incompatible change to the subject or fields needs a new major.
|
||||
|
||||
## Seal-account-settings capability
|
||||
|
||||
`AccountSettingsSealCapabilitySubject(node)` (`ec2.v1.AccountSettingsSealCapability.<node>`) is a per-node, read-only request/reply route on which `accountteardown` asks one configured daemon whether it supports the account-settings seal, immediately before every seal attempt.
|
||||
EC2 owns it: the daemon serves it with no queue group, so a request addressed to one node reaches only subscribers configured as that node, and two daemons sharing a node ID both answer.
|
||||
`accountteardown` is the consumer: its StageAccount seal reaper probes every configured node and sends no seal request unless every one answers compatible.
|
||||
|
||||
`AccountSettingsSealCapabilityVersion` is a claim about the whole binary's writers, not about the responder: version 1 means every EC2 account-settings writer in the binary is revision-guarded and refuses a sealed record, and the binary serves the seal subject.
|
||||
A daemon binary without this responder — predating the capability, or carrying only the seal subject without it — has no subscriber, so the probe for that node reads as incomplete (`no-reply`), the same as a stopped or partitioned node.
|
||||
The reply proves nothing beyond the moment it was sent: a daemon downgraded or replaced after a successful probe is outside the guarantee.
|
||||
|
||||
### Compatibility boundary
|
||||
|
||||
The subject name carries the contract major; `account_settings_seal_capability_test.go` preserves the version-1 JSON shape and the subject builder.
|
||||
Producer and consumer are in the coordinated Spinifex release, so the window is N and N-1.
|
||||
A daemon that predates the capability has no subscription, so the probe gets no responder for that node.
|
||||
An incompatible change to the subject or fields needs a new major.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package ec2v1
|
||||
|
||||
// SealAccountSettingsSubject is the request/reply subject on which a daemon
|
||||
// seals one account's EC2 settings record. Sealing is irreversible and refused
|
||||
// unless IAM already holds the account as TERMINATING.
|
||||
const SealAccountSettingsSubject = "ec2.v1.SealAccountSettings"
|
||||
|
||||
// AccountSettingsDispositionSealed is the only disposition a successful reply
|
||||
// carries: a reply is sent only on success, so there is no other value.
|
||||
const AccountSettingsDispositionSealed = "sealed"
|
||||
|
||||
// SealAccountSettingsRequest asks for one account's EC2 settings to be
|
||||
// sealed. CorrelationRef is an opaque caller reference echoed for logs; it
|
||||
// grants nothing and is never a fence or authority to write.
|
||||
type SealAccountSettingsRequest struct {
|
||||
AccountID string `json:"account_id"`
|
||||
CorrelationRef string `json:"correlation_ref,omitempty"`
|
||||
}
|
||||
|
||||
// SealAccountSettingsReply confirms accountID's settings record was sealed at
|
||||
// Revision, the stream leader's accepted write for that seal. A failure is
|
||||
// the standard awserrors error payload, not this type.
|
||||
type SealAccountSettingsReply struct {
|
||||
AccountID string `json:"account_id"`
|
||||
CorrelationRef string `json:"correlation_ref,omitempty"`
|
||||
Disposition string `json:"disposition"`
|
||||
Revision uint64 `json:"revision"`
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package ec2v1
|
||||
|
||||
import "strings"
|
||||
|
||||
// AccountSettingsSealCapabilityVersion is the version this binary reports.
|
||||
// Version 1: every EC2 account-settings writer in the binary is
|
||||
// revision-guarded and refuses a sealed record, and the seal subject is served.
|
||||
const AccountSettingsSealCapabilityVersion = 1
|
||||
|
||||
// AccountSettingsSealCapabilitySubject is one daemon's read-only
|
||||
// seal-capability route, addressed per node so a probe reaches exactly the
|
||||
// node it asks about.
|
||||
func AccountSettingsSealCapabilitySubject(node string) string {
|
||||
return "ec2.v1.AccountSettingsSealCapability." + node
|
||||
}
|
||||
|
||||
// ValidSealCapabilityNode reports whether node is exactly one NATS subject
|
||||
// token. Responder and probe both check it before building a subject, so
|
||||
// neither can address a wildcard or another node's route.
|
||||
func ValidSealCapabilityNode(node string) bool {
|
||||
return node != "" && !strings.ContainsAny(node, ".*>\t\n\r ")
|
||||
}
|
||||
|
||||
// AccountSettingsSealCapabilityRequest asks node whether it supports the
|
||||
// account-settings seal. CorrelationRef is an opaque caller reference echoed
|
||||
// for logs; it grants nothing and is never a fence or authority to write.
|
||||
type AccountSettingsSealCapabilityRequest struct {
|
||||
Node string `json:"node"`
|
||||
CorrelationRef string `json:"correlation_ref,omitempty"`
|
||||
}
|
||||
|
||||
// AccountSettingsSealCapabilityReply reports the responding node's capability
|
||||
// version. It carries no account data and proves nothing beyond the moment it
|
||||
// was sent.
|
||||
type AccountSettingsSealCapabilityReply struct {
|
||||
Node string `json:"node"`
|
||||
CapabilityVersion int `json:"capability_version"`
|
||||
CorrelationRef string `json:"correlation_ref,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package ec2v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAccountSettingsSealCapabilitySubject(t *testing.T) {
|
||||
require.Equal(t, "ec2.v1.AccountSettingsSealCapability.node-1", AccountSettingsSealCapabilitySubject("node-1"))
|
||||
require.Equal(t, "ec2.v1.AccountSettingsSealCapability.node-2", AccountSettingsSealCapabilitySubject("node-2"))
|
||||
}
|
||||
|
||||
func TestAccountSettingsSealCapabilityRequestJSONContract(t *testing.T) {
|
||||
data, err := json.Marshal(AccountSettingsSealCapabilityRequest{Node: "node-1", CorrelationRef: "req-1"})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"node":"node-1","correlation_ref":"req-1"}`, string(data))
|
||||
|
||||
data, err = json.Marshal(AccountSettingsSealCapabilityRequest{Node: "node-1"})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"node":"node-1"}`, string(data))
|
||||
}
|
||||
|
||||
func TestAccountSettingsSealCapabilityReplyJSONContract(t *testing.T) {
|
||||
reply := AccountSettingsSealCapabilityReply{Node: "node-1", CapabilityVersion: 1, CorrelationRef: "req-1"}
|
||||
data, err := json.Marshal(reply)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"node":"node-1","capability_version":1,"correlation_ref":"req-1"}`, string(data))
|
||||
|
||||
data, err = json.Marshal(AccountSettingsSealCapabilityReply{Node: "node-1", CapabilityVersion: 1})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"node":"node-1","capability_version":1}`, string(data))
|
||||
}
|
||||
|
||||
func TestValidSealCapabilityNode(t *testing.T) {
|
||||
for _, node := range []string{"node-1", "node_2", "n3"} {
|
||||
require.True(t, ValidSealCapabilityNode(node), node)
|
||||
}
|
||||
for _, node := range []string{"", "a.b", "*", ">", "node-*", "node >", "node\t1", "node\n1"} {
|
||||
require.False(t, ValidSealCapabilityNode(node), "%q", node)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package ec2v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSealAccountSettingsSubject(t *testing.T) {
|
||||
require.Equal(t, "ec2.v1.SealAccountSettings", SealAccountSettingsSubject)
|
||||
}
|
||||
|
||||
func TestSealAccountSettingsRequestJSONContract(t *testing.T) {
|
||||
data, err := json.Marshal(SealAccountSettingsRequest{AccountID: "111122223333", CorrelationRef: "req-1"})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"account_id":"111122223333","correlation_ref":"req-1"}`, string(data))
|
||||
|
||||
data, err = json.Marshal(SealAccountSettingsRequest{AccountID: "111122223333"})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"account_id":"111122223333"}`, string(data))
|
||||
}
|
||||
|
||||
func TestSealAccountSettingsReplyJSONContract(t *testing.T) {
|
||||
reply := SealAccountSettingsReply{
|
||||
AccountID: "111122223333",
|
||||
CorrelationRef: "req-1",
|
||||
Disposition: AccountSettingsDispositionSealed,
|
||||
Revision: 42,
|
||||
}
|
||||
data, err := json.Marshal(reply)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{
|
||||
"account_id": "111122223333",
|
||||
"correlation_ref": "req-1",
|
||||
"disposition": "sealed",
|
||||
"revision": 42
|
||||
}`, string(data))
|
||||
|
||||
data, err = json.Marshal(SealAccountSettingsReply{AccountID: "111122223333", Disposition: AccountSettingsDispositionSealed})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"account_id":"111122223333","disposition":"sealed","revision":0}`, string(data))
|
||||
}
|
||||
@@ -17,7 +17,7 @@ func TestVolumeAbandonSubject_BothForms(t *testing.T) {
|
||||
assert.Equal(t, "ebs.abandon", viperblocklegacyv1.VolumeAbandonSubject(""))
|
||||
}
|
||||
|
||||
// TestVolumeAbandonRequest_JSON pins the request daemon/vm_adapters.go sends
|
||||
// TestVolumeAbandonRequest_JSON pins the request runtime/daemon/vm_adapters.go sends
|
||||
// on the abandon route.
|
||||
func TestVolumeAbandonRequest_JSON(t *testing.T) {
|
||||
golden := `{"volume":"vol-1","reason":"instance owned by another node"}`
|
||||
|
||||
@@ -48,6 +48,11 @@ type EBSRequest struct {
|
||||
NBDURI string `json:"NBDURI"`
|
||||
DeviceName string `json:"DeviceName"`
|
||||
HotplugPort int `json:"HotplugPort,omitempty"`
|
||||
// BlockDevice and BlockSizeBytes describe a host block device export, the
|
||||
// alternative to NBDURI; the guest sees BlockSizeBytes of it. Exactly one
|
||||
// of NBDURI and BlockDevice may be set.
|
||||
BlockDevice string `json:"BlockDevice,omitempty"`
|
||||
BlockSizeBytes uint64 `json:"BlockSizeBytes,omitempty"`
|
||||
}
|
||||
|
||||
// EBSMountResponse is the response to an EBSRequest on MountSubject.
|
||||
|
||||
@@ -35,6 +35,18 @@ func TestEBSRequestJSONContract(t *testing.T) {
|
||||
"NBDURI":"nbd:unix:/run/spinifex/vol-123.sock",
|
||||
"DeviceName":"/dev/sdf", "HotplugPort":3
|
||||
}`, string(data))
|
||||
|
||||
block, err := json.Marshal(EBSRequest{
|
||||
Name: "vol-456",
|
||||
BlockDevice: "/dev/sdx",
|
||||
BlockSizeBytes: 10 << 30,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{
|
||||
"Name":"vol-456", "VolType":"", "Boot":false, "EFI":false,
|
||||
"DeleteOnTermination":false, "NBDURI":"", "DeviceName":"",
|
||||
"BlockDevice":"/dev/sdx", "BlockSizeBytes":10737418240
|
||||
}`, string(block))
|
||||
}
|
||||
|
||||
func TestVolumeResponseJSONContracts(t *testing.T) {
|
||||
|
||||
+4
-2
@@ -95,7 +95,7 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
|
||||
|
||||
| Command | Flags | Description |
|
||||
|---------|-------|-------------|
|
||||
| `spx admin init` | `--nodes`, `--node`, `--bind`, `--advertise` (external IP off-host clients dial; auto-detected from the default route), `--port`, `--region`, `--az`, `--cluster-name`, `--cluster-bind`, `--cluster-routes`, `--predastore-nodes`, `--predastore-compaction-interval` (seconds; test clusters set a short one), `--services`, `--formation-timeout`, `--token-ttl`, `--force`, `--discard-jetstream` (with `--nodes` >= 2, remove this node's existing JetStream streams once every node has joined; without it a multi-node init over a store that holds streams is refused, and it never proceeds while NATS is running), `--skip-host-dns`, `--email` (operator address for update and security notices), `--no-telemetry`, `--gpu-passthrough`, `--ipsec` (default true; encrypt intra-AZ Geneve via OVN native IPsec cluster-wide — disable only for a trusted single-rack lab), `--host` (leader to join; falls back to multicast discovery), `--external-mode` (`pool` \| `nat` routed NAT for non-bridgeable uplinks, single-node; pair with `setup-ovn.sh --nat-uplink`; note: system instances (ECS/EKS/load-balancer agents) are unsupported in nat v1 — they require `pool` mode), `--external-iface` (WAN NIC for br-external, auto-detected), `--external-source` (`dhcp` \| `static`), `--external-pool` (`start-end`), `--external-prefix-len` (default 24), `--external-gateway` (auto-detected), `--external-bind-bridge` (default `br-wan` when `--external-source=dhcp`), `--gateway-ip` (OVN gateway router's external IP for SNAT; defaults to the pool's range start, required for `nat` mode without DHCP) | Generates root IAM credentials (AKIA-prefixed access key + secret) → creates master.key (AES-256, 32 bytes, 0600) → writes bootstrap.json (consumed on first start) → generates CA + server TLS certificates → generates join token (written to `join-token` file, displayed in join command) → creates NATS config with auth token → writes spinifex.toml, awsgw.toml, predastore.toml → configures AWS CLI `spx` profile → creates directory structure under `~/spinifex/` → points the host resolver at this node's local northstar so the Spinifex zones (ELB/EKS names) resolve from the node itself (opt out with `--skip-host-dns`). `--force` on an already-initialized node is idempotent for crypto: it preserves the existing keys (master.key, predastore/viperblock encryption keys), system + admin credentials, and CA, and refreshes only the config files and the CA-signed server certificate (so a changed bind IP / SANs is picked up without breaking already-joined nodes or CA-baked AMIs). A genuine clean slate requires removing the data dirs first. |
|
||||
| `spx admin init` | `--nodes`, `--node`, `--bind`, `--advertise` (external IP off-host clients dial; auto-detected from the default route), `--port`, `--region`, `--az`, `--cluster-name`, `--cluster-bind`, `--cluster-routes`, `--predastore-nodes`, `--predastore-compaction-interval` (seconds; test clusters set a short one), `--services`, `--formation-timeout`, `--token-ttl`, `--force`, `--discard-jetstream` (with `--nodes` >= 2, remove this node's existing JetStream streams once every node has joined; without it a multi-node init over a store that holds streams is refused, and it never proceeds while NATS is running), `--skip-host-dns`, `--email` (operator address for update and security notices), `--no-telemetry`, `--gpu-passthrough`, `--ipsec` (default true; encrypt intra-AZ Geneve via OVN native IPsec cluster-wide — disable only for a trusted single-rack lab), `--host` (leader to join; falls back to multicast discovery), `--external-mode` (`pool` \| `nat` routed NAT for non-bridgeable uplinks, single-node; pair with `setup-ovn.sh --nat-uplink`; note: system instances (ECS/EKS/load-balancer agents) are unsupported in nat v1 — they require `pool` mode), `--external-iface` (WAN NIC for br-external, auto-detected), `--external-source` (`dhcp` \| `static`), `--external-pool` (`start-end`), `--external-prefix-len` (default 24), `--external-gateway` (auto-detected), `--external-dns` (comma-separated IPv4 addresses, at most 3; the DNS servers written to the external pool's `dns_servers` and handed to guests, verbatim; when omitted they are detected from the host resolver at init time, so set it to give every node of a cluster the same value), `--external-bind-bridge` (default `br-wan` when `--external-source=dhcp`), `--gateway-ip` (OVN gateway router's external IP for SNAT; defaults to the pool's range start, required for `nat` mode without DHCP) | Generates root IAM credentials (AKIA-prefixed access key + secret) → creates master.key (AES-256, 32 bytes, 0600) → writes bootstrap.json (consumed on first start) → generates CA + server TLS certificates → generates join token (written to `join-token` file, displayed in join command) → creates NATS config with auth token → writes spinifex.toml, awsgw.toml, predastore.toml → configures AWS CLI `spx` profile → creates directory structure under `~/spinifex/` → points the host resolver at this node's local northstar so the Spinifex zones (ELB/EKS names) resolve from the node itself (opt out with `--skip-host-dns`). `--force` on an already-initialized node is idempotent for crypto: it preserves the existing keys (master.key, predastore/viperblock encryption keys), system + admin credentials, and CA, and refreshes only the config files and the CA-signed server certificate (so a changed bind IP / SANs is picked up without breaking already-joined nodes or CA-baked AMIs). A genuine clean slate requires removing the data dirs first. |
|
||||
| `spx admin join` | `--host` (required), `--node` (required), `--token` (required), `--bind`, `--advertise` (defaults to `--bind`, or the auto-detected WAN IP when `--bind` is 0.0.0.0), `--port`, `--region`, `--az`, `--cluster-bind`, `--cluster-routes`, `--data-dir`, `--services`, `--skip-host-dns`, `--email`, `--no-telemetry`, `--predastore-compaction-interval`, `--join-timeout` (how long to keep retrying while the formation server is unreachable, default 20m), `--force` (join even though this node is already initialized or holds JetStream streams, **discarding its own CA, master key and JetStream store**; the store is removed only after formation succeeds, and the join is refused while NATS is running even with `--force`) | Connects to leader node with join token (Authorization: Bearer header) → retrieves cluster configuration → configures local node to join cluster and participate in distributed operations → points the host resolver at this node's local northstar so the Spinifex zones resolve from the node itself (opt out with `--skip-host-dns`) |
|
||||
|
||||
### Version
|
||||
@@ -110,6 +110,7 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
|
||||
|---------|-------|-------------|
|
||||
| `spx admin kv digest` | `--store-dir` (default `<spinifex-dir>/nats/jetstream`), `--work-dir` (holds the temporary copy, default the system temp dir), `--stream` (repeatable filter), `--seqs` (also print a hash per sequence), `--json` (print one JSON document, the input `spx admin kv compare` reads) | Read-only per-replica consistency check. Copies this node's `$G` streams tree (without consumer state) to a temporary directory, opens the copy in a private in-process NATS server with no listener, and prints one line per stream with first/last sequence, message count and a SHA-256 over every stored message (sequence, subject, timestamp, headers, data). Run it on every node with `sudo` and diff the output with the `#` lines removed: replicas of one stream must match. A direct get over NATS is not used because any replica may answer it. Writes landing during the copy can change a replica's tail, so gate API writes or re-run before calling a tail difference divergence. Prefer `--json --seqs` on every node followed by `spx admin kv compare`, which tells a diverged replica from one that is merely behind. |
|
||||
| `spx admin kv compare <digest.json>...` | — (`-` reads any number of digests from stdin) | Reads the `spx admin kv digest --json --seqs` output of every node and checks that each stream's replicas hold the same content. A replica that is merely behind is not divergence; these are: the same sequence holding different content on two replicas, a message missing from one replica that nothing on it explains (no later write to the same subject, no expiry under `max_age` or a message TTL, no stream limit dropping its head), a stream held by fewer or more nodes than its replica count, replicas disagreeing on that count, or no sequence range common to every replica. A stream with a `max_age` whose replicas were copied at different times and share no sequence range is reported as `skipped` (volatile) rather than diverged, since a short-lived bucket such as a leader lease turns over between copies; any sequence two of its replicas share must still match, and each message a replica lacks must be superseded, expired, or written after that replica was copied. Sequences past the slowest replica's last are not compared. Exits 0 when every stream is consistent, 1 on divergence, 2 on bad input. Digests are taken from live stores seconds apart, so re-run before trusting a failure that a write landing between two copies could explain. `install-node.sh` gates cluster formation on it. |
|
||||
| `spx admin dns nameservers` | `--check` (compare against the parent zone's delegation), `--timeout` (per DNS query, default 5s) | Prints the `nsN` nameservers this cluster publishes for its base domain, with the node and address behind each. At most four are published however many nodes run northstar; `[northstar] nameserver_count` (1–4) and `nameserver_nodes` (ordered pin) in `spinifex.toml` set the count and order, and every northstar node keeps serving every zone either way. Passing `northstar` in `spx admin init` or `join --services` writes that node into `nameserver_nodes` in name order, and formation refuses a fifth. Each DNS reconcile pass holds the base zone's apex NS set and `nsN` glue to the published set, so a join or a pin change reaches the zone without a rebuild. `--check` asks a server of the parent zone (a `.net` server for `spx3.net`) for the delegation without recursion and reports each name as `ok`, `not delegated`, `no glue`, `glue mismatch` or `not published here`, exiting non-zero unless all are `ok`. |
|
||||
| `spx admin cluster shutdown` | `--force` (shutdown even if nodes don't respond), `--timeout` (max wait per phase, default 120s), `--dry-run` (print phase plan without executing) | Performs coordinated, phased shutdown of entire cluster. Phases execute in order: GATE (stop API/UI) → DRAIN (stop VMs) → STORAGE (stop viperblock) → PERSIST (stop predastore) → INFRA (stop NATS/daemon). Each phase waits for all nodes to ACK before proceeding. Uses JetStream state tracking. |
|
||||
| `spx admin cluster drain-dhcp` | `--timeout` (reply-collection window, default 30s) | Asks each vpcd to DHCPRELEASE every external-pool DHCP lease it currently holds, returning them to the upstream DHCP server. Run on teardown before stopping services — an env reset otherwise strands held leases upstream until their TTL expires, eventually exhausting the upstream scope. Best-effort: warns and exits 0 if the cluster is already down. |
|
||||
| `spx admin node drain --local` | `--local` (drain the local node only, required), `--timeout` (max wait per phase, default 120s), `--unless-restarting` (drain on a real host shutdown or a plain target stop, and skip only when a restart of the stack is already queued; unset, it always drains) | Runs the GATE and DRAIN phases against the local node only: powers down its guests via QMP and unmounts their volumes (flushing the viperblock WAL) while every service is still up. STORAGE/PERSIST/INFRA are left to systemd's ordered teardown. Wired as the `ExecStop` of `spinifex-shutdown.service`, so `systemctl stop spinifex.target` and host reboot/poweroff drain guests before any storage service stops. |
|
||||
@@ -118,7 +119,7 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
|
||||
|
||||
| Command | Flags | Description |
|
||||
|---------|-------|-------------|
|
||||
| `spx admin cert renew` | `--extra-ip` (additional IPs for SANs), `--extra-dns` (additional DNS names for SANs) | Reads existing CA → regenerates server certificate with all current network interface IPs and machine hostname in SANs → writes new cert. Use after adding a new network interface or changing IP addresses. |
|
||||
| `spx admin cert renew` | `--extra-ip` (additional IPs for SANs), `--extra-dns` (additional DNS names for SANs) | Reads existing CA and `spinifex.toml` → regenerates server certificate keeping the SANs `spx admin init` gave it (bind IP, mgmt-bridge IP and the AWS service names for the configured region and services domain) plus all current network interface IPs and machine hostname → writes new cert. Use after adding a new network interface or changing IP addresses. |
|
||||
| `spx admin cert create-tenant-ca` | `--domain` (required, repeatable — permitted domains baked into the CA's name constraints), `--regenerate` (replace an existing tenant CA, requires confirmation), `--yes` (skip the `--regenerate` confirmation prompt) | Creates the tenant root CA that ACM's `PRIVATE_CA` validation mode signs leaf certificates from — a separate, independent root from the platform CA. Idempotent: running it again against an existing tenant CA reports the current state rather than regenerating; `--regenerate` is required to replace the root, which invalidates every device's existing trust. |
|
||||
| `spx admin cert force-renew` | `--arn` (required, certificate ARN), `--account` (defaults to the bootstrap account) | Reissues an ACM certificate under its existing ARN immediately, instead of waiting for the renewal worker's proportional window. Only `PRIVATE_CA` (tenant-CA-issued) certificates can be force-renewed; imported and public ones are refused. Talks to the running daemon over NATS and takes the same per-certificate lease the scheduled worker uses, so it is safe against a live deployment. It exists mainly to verify the ELBv2 fan-out — that every load balancer referencing the certificate re-renders with the new leaf — without waiting out the real window. |
|
||||
|
||||
@@ -136,6 +137,7 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
|
||||
|---------|-------|-------------|
|
||||
| `spx admin volumes orphans` | — | Lists volumes the storage provider holds that the control plane has no record of. Their blocks consume space but they have no API handle, so they can be neither described nor deleted through the EC2 API. **Reports only, never deletes**: the evidence that an orphan is still wanted is the record that went missing, so removal is an operator decision taken per volume. |
|
||||
| `spx admin volumes unsealed` | — | Lists volumes a node has open, or had open, without a confirmed seal to the backend — the node named holds writes predastore may not have. Starting such a volume elsewhere is allowed, and is preferred over leaving the instance unable to run, but it opens from the last checkpoint that did reach the backend. This is the list of volumes where that would cost something. |
|
||||
| `spx admin storage status` | `--endpoint` (default: this node's AWS gateway), `--region` (default: this node's region), `--ca-bundle` (default: this node's CA) | Sends the signed `GetStorageStatus` action to the AWS gateway with credentials from the standard AWS chain (e.g. `AWS_PROFILE=spinifex`) and prints what the console's Service Metrics page shows: predastore encoding, meta and blob node counts, cluster health, a meta node table (health, raft state, leader, term, commit and applied index) and the blob nodes. Cluster health is `Degraded` when any meta node is unreachable or the nodes reporting raft state do not agree on one leader. A meta node read only through its host's `/readyz` shows healthy with blank raft fields. |
|
||||
|
||||
### Upgrade Management
|
||||
|
||||
|
||||
+38
-56
@@ -50,14 +50,12 @@ Request routing (`spinifex/gateway/gateway.go`):
|
||||
1. **Authentication**: SigV4 middleware validates AWS credentials and resolves the account ID
|
||||
2. **Throttling**: Per-account+action token bucket rejects bursts post-auth
|
||||
3. **Service Detection**: Extracts service name from the Authorization header
|
||||
4. **Action Dispatch**: Routes to service-specific handler. A service registered through the ingress dispatch registry (`ingress/aws/dispatch`, wired by `runtime/roles/awsgw`) is dispatched by its registration; currently that is ECR's control plane, registered from `domains/ecr/awsapi`. Every other service uses the legacy switch:
|
||||
4. **Action Dispatch**: Routes to service-specific handler. A service registered through the ingress dispatch registry (`ingress/aws/dispatch`, wired by `runtime/roles/awsgw`) is dispatched by its registration; currently those are the ECR and ACM control planes, registered from `domains/ecr/awsapi` and `domains/acm/awsapi`. Every other service uses the legacy switch:
|
||||
|
||||
```go
|
||||
switch svc {
|
||||
case "ec2":
|
||||
err = gw.EC2_Request(w, r)
|
||||
case "account":
|
||||
err = gw.Account_Request(w, r)
|
||||
case "iam":
|
||||
err = gw.IAM_Request(w, r)
|
||||
case "sts":
|
||||
@@ -66,10 +64,13 @@ case "elasticloadbalancing":
|
||||
err = gw.ELBv2_Request(w, r)
|
||||
case "eks":
|
||||
err = gw.EKS_Request(w, r)
|
||||
case "bedrock":
|
||||
err = gw.Bedrock_Request(w, r)
|
||||
// ... bedrock-runtime, bedrock-agent, bedrock-agent-runtime
|
||||
case "ecs":
|
||||
err = gw.ECS_Request(w, r)
|
||||
case "acm":
|
||||
err = gw.ACM_Request(w, r)
|
||||
case "rds":
|
||||
err = gw.RDS_Request(w, r)
|
||||
case "tagging":
|
||||
err = gw.Tagging_Request(w, r)
|
||||
case "spinifex":
|
||||
@@ -79,14 +80,15 @@ case "spinifex":
|
||||
|
||||
### 3. EC2 Handler
|
||||
|
||||
The EC2 handler (`spinifex/gateway/ec2.go`) parses the `Action` parameter and delegates to specific handlers via a generic `ec2Handler` wrapper. The account ID resolved by SigV4 auth is threaded into every handler so requests are scoped per IAM principal:
|
||||
The EC2 handler (`spinifex/gateway/ec2.go`) parses the `Action` parameter and delegates to specific handlers via the generic `ec2Handler` and `ec2HandlerWithReq` wrappers. The account ID resolved by SigV4 auth is threaded into every handler so requests are scoped per IAM principal:
|
||||
|
||||
```go
|
||||
"RunInstances": ec2Handler(func(input *ec2.RunInstancesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return ec2instanceapi.RunInstances(input, gw.NATSConn, accountID)
|
||||
"RunInstances": ec2HandlerWithReq(func(ctx context.Context, input *ec2.RunInstancesInput, gw *GatewayConfig, accountID string, r *http.Request) (any, error) {
|
||||
// ... PassRole and launch-quota checks, then the vCPU quota charge
|
||||
return ec2instanceapi.RunInstances(ctx, input, gw.NATSConn, gw.IAMService, accountID, passRoleCheck, launchQuotaCheck, gw.ExpectedNodes)
|
||||
}),
|
||||
"DescribeInstances": ec2Handler(func(input *ec2.DescribeInstancesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return ec2instanceapi.DescribeInstances(input, gw.NATSConn, gw.DiscoverActiveNodes(), accountID)
|
||||
"DescribeInstances": ec2Handler(func(ctx context.Context, input *ec2.DescribeInstancesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return ec2instanceapi.DescribeInstancesChecked(ctx, input, gw.NATSConn, 0, gw.NodeIDs, accountID)
|
||||
}),
|
||||
// ... + volumes, snapshots, VPCs, subnets, route tables, IGWs, NAT gateways,
|
||||
// security groups, network interfaces, elastic IPs, placement groups, key pairs,
|
||||
@@ -98,13 +100,13 @@ The EC2 handler (`spinifex/gateway/ec2.go`) parses the `Action` parameter and de
|
||||
The gateway communicates with daemons via NATS request/response. Most calls go through `natsmsg.NATSRequest`, which marshals the input, attaches the account ID as a NATS header, and unmarshals the typed response:
|
||||
|
||||
```go
|
||||
func (s *NATSInstanceService) RunInstances(input *ec2.RunInstancesInput, accountID string) (*ec2.Reservation, error) {
|
||||
func (s *NATSInstanceService) RunInstances(ctx context.Context, input *ec2.RunInstancesInput, accountID string) (*ec2.Reservation, error) {
|
||||
topic := fmt.Sprintf("ec2.RunInstances.%s", aws.StringValue(input.InstanceType))
|
||||
return natsmsg.NATSRequest[ec2.Reservation](s.natsConn, topic, input, 5*time.Minute, accountID)
|
||||
return natsmsg.NATSRequest[ec2.Reservation](ctx, s.natsConn, topic, input, 5*time.Minute, accountID)
|
||||
}
|
||||
```
|
||||
|
||||
`RunInstances` uses a per-instance-type subject so NATS only delivers the request to a node with spare capacity for that type — no application-level reject-and-retry.
|
||||
The gateway's `RunInstances` places instances itself (`domains/ec2/awsapi/instance/placement.go`): it gathers capacity from every node on `spinifex.node.status`, spreads the launch across eligible nodes, and sends each node its share on the node-targeted subject `ec2.RunInstances.{type}.{nodeId}`, rolling back on partial failure.
|
||||
|
||||
### Cross-Process Contract Ownership
|
||||
|
||||
@@ -124,14 +126,14 @@ refactor. It needs a versioned successor or an explicit compatibility plan.
|
||||
|
||||
### 5. Daemon Processing
|
||||
|
||||
Daemons (`spinifex/daemon/daemon.go`) subscribe to NATS topics and handle requests. A table-driven `subscribeAll()` registers the static EC2/ELBv2 surface at startup:
|
||||
Daemons (`spinifex/runtime/daemon/daemon.go`) subscribe to NATS topics and handle requests. A table-driven `subscribeAll()` registers the static EC2/ELBv2 surface at startup:
|
||||
|
||||
```go
|
||||
// Queue group → load-balanced (one daemon handles each request)
|
||||
{"ec2.CreateKeyPair", d.handleEC2CreateKeyPair, "spinifex-workers"},
|
||||
{"ec2.CreateKeyPair", handleNATSRequest(d.node, d.keyService.CreateKeyPair), "spinifex-workers"},
|
||||
|
||||
// No queue group → fan-out (every daemon responds)
|
||||
{"ec2.DescribeInstanceTypes", d.handleEC2DescribeInstanceTypes, ""},
|
||||
{"ec2.DescribeInstanceTypes", handleNATSRequest(d.node, d.instanceService.DescribeInstanceTypes), ""},
|
||||
```
|
||||
|
||||
`ec2.RunInstances` is dynamic: `ResourceManager` subscribes to `ec2.RunInstances.{type}` only while the node has capacity for that type, and unsubscribes when full. The node-targeted subject `ec2.RunInstances.{type}.{nodeId}` stays subscribed even at full capacity — the launch-time `allocate()` enforces limits there. Per-instance commands flow over `ec2.cmd.{instanceID}`, subscribed only by the owning node.
|
||||
@@ -144,20 +146,17 @@ When a daemon handles `RunInstances`:
|
||||
|
||||
1. **Resource Check**: Validates CPU/memory availability via `ResourceManager`
|
||||
2. **Volume Generation**: Creates boot and EFI volumes via Viperblock
|
||||
3. **Volume Mount**: Sends `ebs.mount` request to Viperblock, receives NBD URI
|
||||
3. **Volume Mount**: Sends a mount request to the colocated Viperblock worker on `ebs.{node}.mount`, receives NBD URI
|
||||
4. **QEMU Launch**: Builds and executes QEMU command with NBD-backed disks
|
||||
5. **QMP Monitoring**: Establishes QEMU Machine Protocol connection for VM management
|
||||
6. **Command subscription**: Subscribes to `ec2.cmd.{instanceID}` for subsequent commands on this VM
|
||||
|
||||
```go
|
||||
// Volume mounting via NATS
|
||||
msg, err := d.natsConn.Request("ebs.mount", ebsMountRequest, 10*time.Second)
|
||||
// Volume mounting via NATS (runtime/daemon/vm_adapters.go)
|
||||
reply, err := ebsRequestWithTrace(ctx, a.nc, instance.AccountID, viperblocklegacyv1.MountSubject(a.node), ebsMountRequest, 30*time.Second)
|
||||
|
||||
// QEMU launch with NBD storage
|
||||
cmd := exec.Command("qemu-system-x86_64",
|
||||
"-drive", fmt.Sprintf("file=nbd:%s,format=raw", nbdURI),
|
||||
// ... additional QEMU args
|
||||
)
|
||||
// QEMU launch with NBD-backed drives in args (runtime/compute/vm/vm.go)
|
||||
cmd := exec.Command(qemuArchitecture, args...) // qemu-system-x86_64 or qemu-system-aarch64
|
||||
```
|
||||
|
||||
## Key Components
|
||||
@@ -199,33 +198,17 @@ type ResourceManager struct {
|
||||
|
||||
### Multi-Node Aggregation
|
||||
|
||||
For operations that need data from all nodes (like `DescribeInstances`), the gateway uses inbox-based fan-out (`spinifex/domains/ec2/awsapi/instance/DescribeInstances.go`):
|
||||
For operations that need data from all nodes (like `DescribeInstances`), the gateway uses the scatter-gather helper `natsmsg.Gather` (`spinifex/foundation/messaging/nats/nats.go`), called from `spinifex/domains/ec2/awsapi/instance/DescribeInstances.go`:
|
||||
|
||||
```go
|
||||
func DescribeInstances(...) {
|
||||
// Create unique inbox for collecting responses
|
||||
inbox := nats.NewInbox()
|
||||
sub, _ := natsConn.SubscribeSync(inbox)
|
||||
|
||||
// Publish to all nodes (no queue group = all daemons respond).
|
||||
// Account ID is propagated as a NATS header so each daemon scopes its response.
|
||||
pubMsg := nats.NewMsg("ec2.DescribeInstances")
|
||||
pubMsg.Reply = inbox
|
||||
pubMsg.Data = jsonData
|
||||
pubMsg.Header.Set(natsmsg.AccountIDHeader, accountID)
|
||||
_ = natsConn.PublishMsg(pubMsg)
|
||||
|
||||
// Collect responses, returning early once expectedNodes have replied
|
||||
// or the 3s timeout is hit.
|
||||
for {
|
||||
msg, err := sub.NextMsg(3 * time.Second)
|
||||
allReservations = append(allReservations, nodeOutput.Reservations...)
|
||||
}
|
||||
|
||||
return allReservations
|
||||
}
|
||||
// Publish to every node (no queue group = all daemons respond) and collect
|
||||
// the replies. The account ID travels as a NATS header so each daemon scopes
|
||||
// its response.
|
||||
frames, sum, err := natsmsg.Gather(ctx, natsConn, "ec2.DescribeInstances", jsonData, gatherOpts)
|
||||
```
|
||||
|
||||
Given the configured node IDs, `Gather` checks each reply's `X-Node-ID` header against them, and `DescribeInstancesChecked` fails closed rather than returning a partial listing when a node is missing, duplicated or unidentified.
|
||||
|
||||
## Storage Integration
|
||||
|
||||
### Viperblock (EBS)
|
||||
@@ -240,6 +223,7 @@ type EBSRequest struct {
|
||||
Boot bool // Boot volume flag
|
||||
EFI bool // EFI boot volume
|
||||
NBDURI string // NBD URI returned from mount
|
||||
// ... + DeviceName, DeleteOnTermination, hotplug port and host block-device fields
|
||||
}
|
||||
```
|
||||
|
||||
@@ -298,6 +282,7 @@ type Config struct {
|
||||
Predastore PredastoreConfig // S3 endpoint + service credentials
|
||||
Viperblock ViperblockConfig
|
||||
VPCD VPCDConfig // OVN/OVS endpoints
|
||||
// ... + EBS, Northstar, RDS, ACM, Bedrock, OchreVector
|
||||
}
|
||||
```
|
||||
|
||||
@@ -320,15 +305,12 @@ spinifex/
|
||||
│ ├── ingress/ # AWS protocol ingress: query/REST parsing, dispatch registry, rate limiting
|
||||
│ ├── runtime/
|
||||
│ │ ├── roles/ # Long-running service roles (awsgw, spinifex, vpcd, nats, predastore, ...)
|
||||
│ │ ├── daemon/ # Per-node daemon and its NATS subscription table
|
||||
│ │ └── compute/ # VM lifecycle, QMP, NBD, GPU
|
||||
│ ├── bootstrap/ # Cluster and node configuration structures, preflight checks
|
||||
│ ├── operator/ # spx CLI commands, admin use cases, installer, upgrade
|
||||
│ ├── agents/ # In-guest agent libraries (ecs, eks, rds)
|
||||
│ ├── gateway/ # Legacy root: HTTP route assembly, SigV4 auth, legacy per-service dispatch
|
||||
│ ├── handlers/ # Legacy root: ecs, eks, elbv2, iam, rds, sts service implementations
|
||||
│ ├── daemon/ # Legacy root: per-node daemon and its NATS subscription table
|
||||
│ ├── accountteardown/ # Legacy root: account deletion cleanup
|
||||
│ └── lbagent/ # Legacy root: load-balancer agent
|
||||
│ ├── operator/ # spx CLI commands, admin use cases, account lifecycle, installer, upgrade
|
||||
│ ├── agents/ # In-guest agent libraries (ecs, eks, elbv2, rds)
|
||||
│ └── gateway/ # Legacy root: HTTP route assembly, SigV4 auth, legacy per-service dispatch
|
||||
├── internal/ # In-guest SigV4 clients, AWS Smithy models and test kits
|
||||
├── architecture/ # Executable architecture rules (package layering)
|
||||
├── build/ # systemd units, node helper scripts, microVM build
|
||||
@@ -338,8 +320,8 @@ spinifex/
|
||||
└── e2e/ # Live-cluster suites (see docs/testing/e2e-coverage.md)
|
||||
```
|
||||
|
||||
The five legacy roots predate ADR-0001 and are frozen: they may shrink but not gain packages.
|
||||
[`PACKAGE_BOUNDARY_MIGRATION.md`](PACKAGE_BOUNDARY_MIGRATION.md) records what remains in each and what blocks its move.
|
||||
The one remaining legacy root, `gateway`, predates ADR-0001 and is frozen: it may shrink but not gain packages.
|
||||
[`PACKAGE_BOUNDARY_MIGRATION.md`](PACKAGE_BOUNDARY_MIGRATION.md) records what remains in it and what blocks its move.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -340,6 +340,19 @@ while the branch is in flight; they do not describe the umbrella repository's
|
||||
| `0d830e901` | — | `spinifex/domains/tagging` (`TaggedLister`, `TaggedResource`) | The read-only list capability for the Resource Groups Tagging aggregation, declared by its consumer per ADR-0001. `ListTagged` takes the caller's account and the lower-cased `ResourceTypeFilters` set, where an entry is a full `service:type` or a bare service, and returns ARNs with read-only tag maps. The package imports no domain, handler or contract package and the layering ratchet classifies it with no allowlist row. It carries no mutation, limit or ever-tagged surface. |
|
||||
| `f3545e3ea` | `spinifex/gateway/tagging` (`listEC2`, `buildEC2Mappings`, `ec2ARN`, `elbv2Resources`, `matchesType`), copied | `spinifex/runtime/roles/awsgw` (`ec2IndexLister`, `elbv2Lister`), `spinifex/domains/tagging` (`MatchesType`) | AWSGW-side NATS client adapters implement `tagging.TaggedLister`, each with its compile-time check beside it in the composition root; no owner package carries one. The EC2 index adapter wraps `ec2.DescribeTags` and builds each ARN in the gateway's region from the stored resource type, including the unresolved-ID shapes today's code produces. The ELBv2 adapter lists load balancers and target groups admitted by the type filter, tagged or not, and reads tags in `DescribeTags` batches of 20. Both reuse existing subjects and clients, so no wire contract is added, and each call builds fresh tag maps. Type matching is exported once from `domains/tagging` so no copy survives the move. Embedded-JetStream tests cover type filters including a bare service, untagged ELBv2 resources, EC2 ARN mapping, account scoping, batch sizes and owner error propagation. |
|
||||
| `aedd0b2c4` | `spinifex/gateway/tagging` (`GetResources` parsing, tag filtering, sorting, offset token, authorization, response writing) | `spinifex/domains/tagging` (`Aggregator`, `NewAggregator`, `HasScope`, `ScopedActions`), wired by `spinifex/runtime/roles/awsgw` | The aggregation moves unchanged and consumes `[]TaggedLister`: ELBv2 is called before EC2, the non-stable ARN sort is kept, lister errors propagate with the same codes, and tag maps return as key-sorted tag lists with `[]` for untagged resources. AWSGW builds the aggregator with both adapters and sets the new `GatewayConfig.TaggingResources` field; the dispatch in `gateway/tagging.go` stays and still answers `ServerInternal` when NATS or the aggregator is not wired. The `TestCurrentBehaviour_Tagging_GetResources*` tests move to `runtime/roles/awsgw` with unchanged assertions, seeding EC2 tags through the existing EC2 tags client over NATS; pure aggregator tests move to `domains/tagging` against fake listers; the gateway keeps dispatch and error-path tests. The gateway's private copies of the listing helpers and the legacy NATS lister are deleted with their tests, `gateway/tagging` is removed, and its two allowlist rows (`gateway/tagging` → `domains/ec2/tags`, `gateway/tagging` → `handlers/elbv2`) and its frozen-legacy entry go with it. |
|
||||
| `spinifex#1249` | `spinifex/handlers/iam`, `spinifex/handlers/iam/mock` | `spinifex/domains/iam` (`iam`, imported as `iamdomain`), `spinifex/domains/iam/mock` | A mechanical relocation of IAM's implementation to its ADR-0001 home; APIs, NATS subjects, KV buckets and behaviour are unchanged and only the package name changed from `handlers_iam`. Per-consumer IAM capabilities remain dependency-rule debt but were not a prerequisite. The two frozen-legacy entries are removed, the cross-domain allowlist entries now target `domains/iam`, and the `operator/cli` → `handlers/iam` entry is removed because an operator import of a domain package does not break the legacy-implementation rule. |
|
||||
| `spinifex#1250` | `spinifex/handlers/sts` | `spinifex/domains/sts` (`sts`, imported as `stsdomain`) | A mechanical relocation of STS's implementation to its ADR-0001 home; APIs, NATS subjects, KV buckets and behaviour are unchanged and only the package name changed from `handlers_sts`. The frozen-legacy entry is removed, and the allowlist entries for `domains/ec2/guestmetadata` → STS and STS → `domains/iam` now name `domains/sts`. |
|
||||
| `spinifex#1251` | `spinifex/handlers/rds` | `spinifex/domains/rds/instance` (`instance`, imported as `rdsinstance`) | A mechanical relocation of the remaining RDS instance resource to its ADR-0001 resource package; APIs, NATS subjects, KV buckets and behaviour are unchanged and only the package name changed from `handlers_rds`. The frozen-legacy entry is removed and the allowlist entries naming `handlers/rds` now name `domains/rds/instance`. |
|
||||
| `spinifex#1264` | `spinifex/handlers/eks` | `spinifex/domains/eks/cluster` (`cluster`, imported as `ekscluster`) | A mechanical relocation of the remaining EKS implementation, nodegroups included; APIs, NATS subjects, KV buckets and behaviour are unchanged and only the package name changed from `handlers_eks`. The frozen-legacy entry is removed, the allowlist entries naming `handlers/eks` now name `domains/eks/cluster`, and the `operator/cli` entry is removed because an operator importing a domain package no longer breaks the legacy rule. Nodegroup service extraction is deferred. |
|
||||
| `spinifex#1265` | `spinifex/gateway/{iam,sts,eks,rds}` | `spinifex/domains/{iam,sts,eks,rds}/awsapi` (`awsapi`, imported as `iamawsapi`, `stsawsapi`, `eksawsapi`, `rdsawsapi`) | A mechanical relocation of the IAM, STS, EKS and RDS AWS action adapters to their ADR-0001 domain homes; APIs, NATS subjects, KV buckets and behaviour are unchanged. The root `gateway` dispatch still imports each package, recorded as `gateway` (service registration) debt. `gateway/bedrock` stays: ADR-0001 moves only its action adapters, and the package also holds the Ochre domain core, stores and inference engine. |
|
||||
| `spinifex#1266` | `spinifex/gateway/bedrock` | `spinifex/domains/ochre` (merged into package `ochre`) | A mechanical whole-package relocation: the Ochre domain core, stores, inference engine and Bedrock action layer join the existing `domains/ochre` package with their package-private code and tests intact. No identifier, API, NATS subject, KV bucket, metric, error string or behaviour changes; `domains/ochre` no longer imports a legacy package. Separating the action adapters into `domains/ochre/awsapi` is deferred, recorded as `domains/ochre` (action adapters) debt. |
|
||||
| `spinifex#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. |
|
||||
| `spinifex#1273` | `spinifex/gateway/{ecs,elbv2}` | `spinifex/domains/{ecs,elbv2}/awsapi` (`awsapi`, imported as `ecsawsapi`, `elbv2awsapi`) | A mechanical relocation of the ECS and ELBv2 AWS action adapters to their ADR-0001 domain homes, as `spinifex#1265` did for IAM, STS, EKS and RDS; APIs, NATS subjects, KV buckets and behaviour are unchanged. The root `gateway` dispatch still imports each package, recorded as `gateway` (service registration) debt, and `domains/admission/quota` keeps its live ELBv2 counter import, now of `domains/elbv2/awsapi`. |
|
||||
| `#1277` | `spinifex/accountteardown` | `spinifex/operator/accountlifecycle` (package `accountlifecycle`, imported as `accountteardown`) | A mechanical whole-package relocation under the 2026-10-27 ADR-0001 amendment: the account deletion coordinator keeps its APIs, NATS subjects, KV access and behaviour; importers keep the `accountteardown` name as an alias, so only their import paths change. Replacing its direct domain and adapter cleanup calls with owner capabilities stays deferred debt. |
|
||||
| `#1285` | `spinifex/gateway/requestaudit.go` and `slogRequestLogger` from `spinifex/gateway/gateway.go` | `spinifex/ingress/aws/audit` (package `audit`) | The first generic-ingress extraction from the root gateway: the request-audit middleware and access-log middleware move unchanged in behaviour. The gateway's authentication flow and dispatch still populate the record, now through the exported `audit.From` and `Record` setters at the same call sites; the package imports only `foundation/telemetry` and `ingress/http`. Its unit tests moved with it; the tests that run it behind SigV4 stay in `gateway`. |
|
||||
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
|
||||
@@ -407,65 +420,67 @@ These residuals were recorded by individual slices and must be resolved once the
|
||||
|
||||
- Guest metadata: the cross-domain and runtime imports above.
|
||||
- EC2 → network implementation: `domains/ec2/vpc/eni.go:18` and `eni_lease_rebind.go:11` import `domains/network/topology`, `eni.go:17` also imports `domains/network/identifiers`, `external_ipam.go:10-11` imports `domains/network/external` and `domains/network/external/dhcp`, `domains/ec2/eip/service_impl.go:17` and `lease_rebind.go:11` import `domains/network/topology`, and `domains/ec2/instance/service_impl.go:31` imports `domains/network/topology`. EC2 reads network's implementation directly rather than through a capability or projection; this clears when EC2's network-facing reads and writes go through the EC2-owned projection recorded above (step 7).
|
||||
- Cross-domain: `domains/ecr/auth/signingkey.go:24` and `domains/ochre/vector/appliance.go:15` import `handlers/iam`; this needs an IAM capability rather than the IAM implementation package. `domains/ochre/launch.go:17-18` and `domains/ochre/vpc.go:9` import `domains/ec2/systeminstance` and `domains/network/systemvpc`; Ochre consumes managed-service launch and system-VPC realisation as domain implementations rather than capabilities.
|
||||
- Cross-domain: `domains/ecr/auth/signingkey.go:24` and `domains/ochre/vector/appliance.go:15` import `domains/iam`; this needs an IAM capability rather than the IAM implementation package. `domains/ochre/launch.go:17-18` and `domains/ochre/vpc.go:9` import `domains/ec2/systeminstance` and `domains/network/systemvpc`; Ochre consumes managed-service launch and system-VPC realisation as domain implementations rather than capabilities.
|
||||
- Domain → runtime: `domains/ochre/capacity.go:9` imports `runtime/compute/gpu`; `domains/ec2/volume/service_impl.go:30` and `volume_leak_reaper.go:8` import `runtime/compute/vm`; `domains/ec2/instance/status_selection.go:9` also imports `runtime/compute/vm`. Each reads VM or GPU runtime state directly instead of through a domain-owned projection.
|
||||
- STS web-identity verification never consults cluster status: tokens verify while a cluster is DELETING until teardown sweeps the prefix, and a FAILED launch keeps its JWKS. AWS behaviour must be established before calling it a divergence. Removal condition: AWS behaviour verified, and the resolver or EKS lifecycle aligned with it.
|
||||
- The public `/keys` endpoint (root `gateway`, `oidc_discovery.go`) serves malformed stored JWKS bytes with HTTP 200. Removal condition: validate before serving, or record it as accepted.
|
||||
- The public discovery and `/keys` endpoints return 404 for KV read errors, including timeouts; only a bucket-open failure returns 500. Removal condition: distinguish dependency failure from not-found.
|
||||
- The root `gateway` package imports `handlers/eks` directly (`gateway/oidc_discovery.go`: `AccountBucketName`, `GetClusterMeta`). The layering rules don't see it because the root gateway has no domain classification, so it has no allowlist entry. Removal condition: the public OIDC discovery and JWKS endpoints consume an EKS-owned resolver injected at composition, the same pattern as STS, leaving the root gateway with no `handlers/eks` import.
|
||||
- `handlers/elbv2/{agent_types,haproxy,health_checker,nginx}.go` import `lbagent` (`HealthReport`, `ServerStatus`, `CertDir`, `NginxPIDPath`, `NginxCertDir`), a domain reading an agent's implementation package rather than a declared wire contract. Removal condition: the health-report and cert/PID-path vocabulary moves behind a guest/controller contract the domain and the agent both consume.
|
||||
- `domains/ec2/instance/state/imds_readers.go` (dependency, IMDS readers) imports `daemon` for `LocalState`, `ReadLocalState`, `LocalStatePath`, `NewJetStreamManager` and `InstanceStateBucket`. The local-file reader and the instance-record loader's `JetStreamManager` open, which `vpcd/imds_instance_state.go` held directly, moved here unchanged and are now injected into `vpcd` through `guestmetadata.LocalStateReader` and `guestmetadata.RecordLoader`. The readers already live in `domains/ec2/instance/state`. Removal condition: the legacy dependencies they call are extracted from `daemon` — `LocalStatePath`, `ReadLocalState` and `LocalState`, the instance bucket constants, and `JetStreamManager` with its bucket initialisation (`InitKVBucket`) — leaving `domains/ec2/instance/state` with no `daemon` import.
|
||||
- The root `gateway` package imports `domains/eks/cluster` directly (`gateway/oidc_discovery.go`: `AccountBucketName`, `GetClusterMeta`). The layering rules don't see it because the root gateway has no domain classification, so it has no allowlist entry. Removal condition: the public OIDC discovery and JWKS endpoints consume an EKS-owned resolver injected at composition, the same pattern as STS, leaving the root gateway with no `domains/eks/cluster` import.
|
||||
- `domains/elbv2/{agent_types,haproxy,health_checker,nginx}.go` import `agents/elbv2` (`HealthReport`, `ServerStatus`, `CertDir`, `NginxPIDPath`, `NginxCertDir`), a domain reading an agent's implementation package rather than a declared wire contract. Removal condition: the health-report and cert/PID-path vocabulary moves behind a guest/controller contract the domain and the agent both consume.
|
||||
- `agents/elbv2` (Q-30 data-plane boundary): the agent moved whole, keeping its current dependencies and the `domains/elbv2` import above. Q-30's data-plane boundary and the common agent layout remain deferred by ADR-0001 and are not paid by the move.
|
||||
- `domains/ec2/instance/state/imds_readers.go` (dependency, IMDS readers) imports `runtime/daemon` for `LocalState`, `ReadLocalState`, `LocalStatePath`, `NewJetStreamManager` and `InstanceStateBucket`. The local-file reader and the instance-record loader's `JetStreamManager` open, which `vpcd/imds_instance_state.go` held directly, moved here unchanged and are now injected into `vpcd` through `guestmetadata.LocalStateReader` and `guestmetadata.RecordLoader`. The readers already live in `domains/ec2/instance/state`. Removal condition: the legacy dependencies they call are extracted from `daemon` — `LocalStatePath`, `ReadLocalState` and `LocalState`, the instance bucket constants, and `JetStreamManager` with its bucket initialisation (`InitKVBucket`) — leaving `domains/ec2/instance/state` with no `daemon` import.
|
||||
- Advisory: 27 production files across `domains/{acm,dns,ec2/*,network/host,network/ovn,ochre}` import `bootstrap/config` directly rather than through typed constructor inputs, which ADR-0001's `config` row expects. `bootstrap/preflight/preflight.go:15` imports `runtime/host/command` and `services.go:13` imports `operator/host/systemd`; both are expected bootstrap-to-runtime/operator edges for startup validation and are not domain-rule violations.
|
||||
- Operator bootstrap credential generation: `operator/cli/admin.go` mints the bootstrap admin and Northstar access keys and secrets directly with `awscredentials` and writes them into config, rather than asking an IAM bootstrap capability to issue them. `awscredentials` only formats the values. Removal condition: a real IAM bootstrap capability issues these credentials and `operator/cli` calls it.
|
||||
- `domains/dns`: imports `runtime/compute/vm` (domain-to-runtime), `domains/network/reconcile` for the reconcile lock and `bootstrap/config` for node configuration.
|
||||
- ACM: the IAM AES-GCM helper import (ACM relocation residual).
|
||||
- `runtime/roles/awsgw`: the quota reconcile loop belongs to `domains/admission/quota`, and the EKS token-verify responder belongs with STS or EKS identity.
|
||||
- `runtime/roles/awsgw` → `handlers/elbv2` (inherited from `gateway/tagging`): this is a new physical import in AWSGW that inherits the former `gateway/tagging` dependency, because the `elbv2Lister` adapter builds its client with `handlers_elbv2.NewNATSELBv2Service` and calls the `ELBv2Service` interface. It is permitted temporarily because AWSGW is the composition root, which may wire legacy packages, so the layering test needs no allowlist row. Removal condition: it must clear when ELBv2 exposes a domain-owned boundary (its NATS client or contract under its domain owner) and the adapter consumes that instead.
|
||||
- `runtime/roles/awsgw` → `domains/elbv2` (inherited from `gateway/tagging`): this is a new physical import in AWSGW that inherits the former `gateway/tagging` dependency, because the `elbv2Lister` adapter builds its client with `elbv2domain.NewNATSELBv2Service` and calls the `ELBv2Service` interface. AWSGW is the composition root, so the layering test needs no allowlist row. Removal condition: it must clear when ELBv2 exposes a domain-owned boundary (its NATS client or contract under its domain owner) and the adapter consumes that instead.
|
||||
- `runtime/roles/vpcd` (placement): the role still mixes process/role supervision with network-controller responsibilities. `lease_reaper.go` (orphaned external-pool DHCP lease sweep), `ovn_controller_watchdog.go` (OVN controller liveness and restart), `host_eip_loop.go` (host-EIP reconcile loop) and `lease_datapath_reconcile.go` (lease datapath reconcile) are periodic network-control loops, not role wiring; `vpcd.go` itself also carries bridge-mode detection and verification, chassis-list parsing, external-CIDR resolution, DHCP manager startup and lease/IP-change handling, and gateway/host-EIP allocator and neighbour flush/prime construction alongside `Service.Start`. The future placement question is whether these belong in `domains/network`. Removal condition: an accepted decision on network-controller placement, and extraction of those loops and helpers behind the role.
|
||||
- ECR control-plane registration: `awsapi.Deps.NATS` is a raw `*nats.Conn`, not a narrow capability. Every NATS-backed `Handler` passes it to `domains/ecr.NewNATSMetaStore`, which calls `natsmsg.NATSRequest`, so narrowing it means changing the exported `Handler` type, every handler and the meta store; it must not be broadened further meanwhile.
|
||||
- ACM control-plane registration: `domains/acm/awsapi.Deps.NATS` (`registration.go`) is likewise a raw `*nats.Conn` carried by every `Handler`, for the same reason; it must not be broadened further meanwhile.
|
||||
- `cmd/aws-model-coverage` imports `ingress/aws/dispatch`, `domains/ecr/awsapi`, `domains/acm/awsapi` and `gateway` to read ECR's and ACM's declared inventories. It is a non-serving tool and the only named exception to the ADR-0002 composition invariant; it must not construct a registration.
|
||||
- Ingress pipeline (ADR-0002 phase 4): the request-audit record, the trace action enricher, the throttle key functions and the gateway request-context keys stay in `gateway` until the auth/IAM and request-context boundary is designed. SigV4 authentication writes the audit record and context keys from four places, so moving them now would export exactly the setters ADR-0002 says must not be exported solely to make a directory move possible.
|
||||
- NAT projection delivery is inconsistent and is preserved as found: EIP `AssociateAddress` sends `vpc.add-nat` unacknowledged (`AnnounceNATAdd`), skipping the 45s OVN barrier every other add path waits on, and `DeleteNetworkInterface` sends `vpc.delete-nat` unacknowledged (`AnnounceNATDelete`), skipping the ack and the ErrNoResponders retry. The reconciler's orphan sweep is the only backstop for both. Clears when those call sites use `AddNAT`/`AddNATBestEffort` and `RemoveNAT`, a reliability change of its own.
|
||||
- `vpc.add-system-egress` has a `vpcd` subscriber and a contract entry but no producer (its former `domains/ec2/eip` publisher is gone); `handlers/eks` still publishes the delete. Either a producer is missing or the add route is dead; it needs an owner decision.
|
||||
- `vpc.add-system-egress` has a `vpcd` subscriber and a contract entry but no producer (its former `domains/ec2/eip` publisher is gone); `domains/eks/cluster` still publishes the delete. Either a producer is missing or the add route is dead; it needs an owner decision.
|
||||
- EBS encryption posture: `domains/ec2/ebs/encryption` reads the Viperblock key-file setting to decide what EC2 reports. Clears when `providers/ebs` reports each volume's encryption posture and EC2 stops reading key files.
|
||||
- `foundation/messaging/nats/nats_trace.go` still names its OpenTelemetry tracer `github.com/mulgadc/spinifex/spinifex/utils`. It is an emitted instrumentation-scope name that dashboards may filter on, so renaming it is a telemetry change, not a path fix.
|
||||
- `runtime/compute/cache.New`: two construction-time panics carry a lint exemption pending a constructor that returns an error.
|
||||
- `domains/ec2/systeminstance`: managed-service parent metadata is only the `ManagedBy` tag value.
|
||||
- `gateway/bedrock`: holds the Ochre domain core (model catalogue, provisioned throughput, guardrail, access, credential, usage, price, logging and weights stores), the inference engine (provider, vLLM, Anthropic, embeddings, reranker, admission) and the Bedrock action layer in one package with internal cycles. `domains/ochre` imports it, which is the ADR step 7 Ochre gateway import. Splitting it needs a plan: domain core and stores to `domains/ochre/<resource>`, inference to an Ochre-owned package, and only the action layer to `domains/ochre/awsapi`. The top-level `gateway/bedrock*.go` HTTP handlers additionally wait for the ingress registration seam.
|
||||
- `domains/ochre` (action adapters): since `spinifex#1266` the Bedrock action layer (handlers, action authorization, event-stream framing and the CRUD, catalogue and Converse/Invoke entry points) shares package `ochre` with the domain core, stores and inference engine, and imports `ingress/aws/bodyscope`. Moving it to `domains/ochre/awsapi` is not mechanical: the adapters reach package-private store internals (the guardrail and provisioned-throughput store handles and get/put/update methods), and adapter tests override the package-private model catalogue. It needs core capabilities first, then the move; the top-level `gateway/bedrock*.go` HTTP handlers additionally wait for the ingress registration seam.
|
||||
- `domains/network`: `reconcile/intent.go` builds desired network state by reading EC2's KV buckets and decoding EC2's private VPC, subnet, ENI, route-table, gateway, EIP, NAT and security-group records, and `reconcile/drift.go` watches those buckets. ADR-0001 names this pair explicitly: EC2 and network communicate through a capability/projection or versioned contract, never by one being the other's KV reader. Step 7 fix: an EC2-owned `domains/ec2/projection` exposing network intent with list and watch (it may stay KV-backed, so no data moves); a narrow intent interface declared by `reconcile`; wiring at the composition root; and the duplicated `PurposeEIP`/`PurposeIGWLRP` constants and mirrored `SGRule`/`SGEvent` folded into the projection or `contracts/ec2/v1`. The drift-watch coverage test and reconcile scenario tests are the evidence the projection must keep. `domains/network` also imports `runtime/compute/vm` and `runtime/host/command` (domain-to-runtime).
|
||||
- `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 `handlers/eks` 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/rds/agent` (wire contract): imports `handlers/rds` 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`.
|
||||
- `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 `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.
|
||||
- `handlers/eks` (potential security risk): `ClusterMeta.ControlPlaneTemplate` (`handlers/eks/service_impl.go`) clears the template's credentials but keeps `OIDCPrivateKeyPEM` and the k3s `JoinToken`, so both are persisted in the `eks-account-{acct}` cluster record, which the gateway also reads. Whether that record is encrypted at rest is unconfirmed; if it is not, a reader of the bucket can forge service-account tokens for the cluster's OIDC issuer and join nodes to it. This predates the refactor and is not fixed in it. Removal condition: the key and token are held by a dedicated secret store (a future KMS or Secrets Manager capability is the likely owner) and the cluster record keeps only a reference, with a test proving the persisted record contains neither secret.
|
||||
- `operator/cli` (dependency): the command tree still imports domain and service implementations directly (`handlers/{iam,eks}`, `domains/admission/quota`, `gateway`, `gateway/bedrock`, `daemon`, `vpcd`, `accountteardown`, the Viperblock adapter and runtime roles). The ADR's `cmd/spinifex` current-to-target map row requires command packages to reach domains through public capabilities; splitting supported use cases into `operator/*` packages is later work.
|
||||
- `domains/eks/cluster` (potential security risk): `ClusterMeta.ControlPlaneTemplate` (`domains/eks/cluster/service_impl.go`) clears the template's credentials but keeps `OIDCPrivateKeyPEM` and the k3s `JoinToken`, so both are persisted in the `eks-account-{acct}` cluster record, which the gateway also reads. Whether that record is encrypted at rest is unconfirmed; if it is not, a reader of the bucket can forge service-account tokens for the cluster's OIDC issuer and join nodes to it. This predates the refactor and is not fixed in it. Removal condition: the key and token are held by a dedicated secret store (a future KMS or Secrets Manager capability is the likely owner) and the cluster record keeps only a reference, with a test proving the persisted record contains neither secret.
|
||||
- `operator/cli` (dependency): the command tree still imports domain and service implementations directly (`handlers/{iam,eks}`, `domains/admission/quota`, `gateway`, `domains/ochre`, `vpcd`, the Viperblock adapter and runtime roles). The ADR's `cmd/spinifex` current-to-target map row requires command packages to reach domains through public capabilities; splitting supported use cases into `operator/*` packages is later work.
|
||||
- `providers/ebs/viperblock` → `runtime/compute/nbd` and `runtime/host/process` (dependency, moved unchanged from `services/viperblockd`): the adapter formats NBD endpoints and manages nbdkit processes through runtime packages. ADR-0001 does not list providers → runtime as forbidden and the layering test does not enforce it (same category as `providers/ebs/conformance` → `runtime/compute/nbd`), but a provider should not depend on node runtime. Clears when NBD endpoint naming has a provider-facing home (a candidate for `contracts/ebs/v1`) and process supervision is injected by the role. The adapter also imports `providers/objectstore`, a provider-to-provider edge that is allowed.
|
||||
- `gateway/ec2.go` (registration): the EC2 dispatch map stays a `GatewayConfig` method in `gateway` and imports every `domains/ec2/awsapi/*` package; `gateway/ec2_idempotency.go` also imports `domains/ec2/awsapi/idem` (`KVBucket`, `TTL`, `TokenAndParams`) for the dispatch-time idempotency wrapper. ADR-0001 makes the domain own its action registration; moving the map waits on the generic ingress registration seam and must not be done as a package move.
|
||||
- `gateway/requestaudit.go`: the outermost request-audit middleware records AWS fields and is updated from four places in the gateway's authentication flow through the gateway's own context key. Moving it would export about seven names used only by that flow, so it stays in `gateway` until the generic ingress registration seam defines how ingress middleware receives authentication results.
|
||||
- `accountteardown` (dependency): its EC2 reapers call `domains/ec2/awsapi/*` action adapters directly, as they did under `gateway/ec2`. The ADR's `accountteardown` current-to-target map row replaces this with domain-owned cleanup capabilities before the package moves. It also holds a read-only consumer of the EC2 platform-inventory contract (`contracts/ec2/v1`, `ec2.v1.PlatformInventory`), served by `domains/ec2/accountinventory`; only the explicit dry run (`Engine.DryRun`) calls it, to log a shadow comparison, and the key-pair, placement-group and launch-template reapers still use the `awsapi` adapters. A domain-owned cleanup capability for the proven-safe subset of those three kinds (key pairs and empty placement groups no instance, spot request or live launch-template version names, and launch-template residue; never live launch templates) exists as `domains/ec2/accountcleanup` (a Go interface with injected owners and reference readers, no NATS subject); nothing calls it yet.
|
||||
- `domains/admission/quota` (dependency, moved from `handlers/quota` unchanged): `live.go` imports `gateway/elbv2` and `handlers/rds` for its live ELBv2 and RDS counters; `records.go` imports `runtime/compute/vm` for the instance-record vCPU sweep; `live.go`, `vcpu.go` and `storage.go` call `domains/ec2/awsapi/{vpc,eip,volume}` describe adapters to count VPCs, subnets, addresses and volumes, as they did under `gateway/ec2`. The ADR's `handlers/quota` current-to-target map row replaces these with reader capabilities once their final locations exist.
|
||||
- `domains/ec2/awsapi/instance` (dependency): imports `handlers/iam` (`IAMService` in `DescribeInstances.go`, `RunInstances.go` and `IamInstanceProfileAssociation.go`; `InstanceProfile` in the latter two), `runtime/compute/cache` (`NodeState`, `NodeUnknown`, `NodeLive`) and `runtime/compute/vm` (`VM`), both in `status_synthesis.go`. The IAM edge needs an IAM capability; the runtime edges are domain-to-runtime and need the instance record and node liveness exposed through an EC2-owned projection.
|
||||
- `domains/ec2/awsapi/spotinstance` (dependency): imports `handlers/iam.IAMService` and `domains/admission/quota.Service` in `RequestSpotInstances.go`.
|
||||
- Instance adapter consumers outside EC2: `gateway/rds/enginecatalog.go`, `daemon/eks_cp_control.go`, `daemon/rds_deps.go`, `domains/admission/quota`'s `vcpu.go` and `gateway/iam.go` (`CountInstanceProfileAssociations`) call `domains/ec2/awsapi/instance` directly; each needs an EC2 capability rather than the AWS action adapter.
|
||||
- `gateway` (service registration): the root `gateway` dispatch for IAM, STS, EKS, RDS, ECS and ELBv2 imports `domains/{iam,sts,eks,rds,ecs,elbv2}/awsapi`, which only AWSGW may wire. It clears when each service registers through the ADR-0002 ingress registration seam; as with EC2, moving the dispatch must not be done as a package move.
|
||||
- `operator/accountlifecycle` (dependency): its EC2 reapers call `domains/ec2/awsapi/*` action adapters directly, as they did under `gateway/ec2`. The amended ADR-0001 `accountteardown` row keeps replacing these with domain-owned cleanup capabilities as deferred debt; it was not a relocation prerequisite. It also holds a read-only consumer of the EC2 platform-inventory contract (`contracts/ec2/v1`, `ec2.v1.PlatformInventory`), served by `domains/ec2/accountinventory`; only the explicit dry run (`Engine.DryRun`) calls it, to log a shadow comparison, and the key-pair, placement-group and launch-template reapers still use the `awsapi` adapters. A domain-owned cleanup capability for the proven-safe subset of those three kinds (key pairs and empty placement groups no instance, spot request or live launch-template version names, and launch-template residue; never live launch templates) exists as `domains/ec2/accountcleanup` (a Go interface with injected owners and reference readers, no NATS subject); nothing calls it yet. It also holds the StageAccount settings-seal reaper, a consumer of `contracts/ec2/v1`'s `ec2.v1.SealAccountSettings` and per-node `ec2.v1.AccountSettingsSealCapability.<node>` subjects, served by `domains/ec2/account` through the daemon; the reaper reads no KV and imports no domain package, reaching both subjects only through NATS and the configured node set `NewClusterEngine` already receives.
|
||||
- `domains/admission/quota` (dependency, moved from `handlers/quota` unchanged): `live.go` imports `domains/elbv2/awsapi` and `domains/rds/instance` for its live ELBv2 and RDS counters; `records.go` imports `runtime/compute/vm` for the instance-record vCPU sweep; `live.go`, `vcpu.go` and `storage.go` call `domains/ec2/awsapi/{vpc,eip,volume}` describe adapters to count VPCs, subnets, addresses and volumes, as they did under `gateway/ec2`. The ADR's `handlers/quota` current-to-target map row replaces these with reader capabilities once their final locations exist.
|
||||
- `domains/ec2/awsapi/instance` (dependency): imports `domains/iam` (`IAMService` in `DescribeInstances.go`, `RunInstances.go` and `IamInstanceProfileAssociation.go`; `InstanceProfile` in the latter two), `runtime/compute/cache` (`NodeState`, `NodeUnknown`, `NodeLive`) and `runtime/compute/vm` (`VM`), both in `status_synthesis.go`. The IAM edge needs an IAM capability; the runtime edges are domain-to-runtime and need the instance record and node liveness exposed through an EC2-owned projection.
|
||||
- `domains/ec2/awsapi/spotinstance` (dependency): imports `domains/iam.IAMService` and `domains/admission/quota.Service` in `RequestSpotInstances.go`.
|
||||
- Instance adapter consumers outside EC2: `domains/rds/awsapi/enginecatalog.go`, `runtime/daemon/eks_cp_control.go`, `runtime/daemon/rds_deps.go`, `domains/admission/quota`'s `vcpu.go` and `gateway/iam.go` (`CountInstanceProfileAssociations`) call `domains/ec2/awsapi/instance` directly; each needs an EC2 capability rather than the AWS action adapter.
|
||||
- `runtime/daemon` (Q-28 ownership and deployment): the daemon moved whole, keeping its current ownership, deployment and `daemon → gateway → handlers` dependencies. Q-28's write-side consolidation and any daemon decomposition or split remain deferred by ADR-0001 and are not paid by the move.
|
||||
|
||||
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.
|
||||
- `handlers/rds` → `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.
|
||||
- `handlers/rds` → `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.
|
||||
- `handlers/eks` → `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.
|
||||
- `handlers/eks` → `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 `handlers/eks`, `gateway/eks` and `agents/eks/gatewayfetch` import directly.
|
||||
- `handlers/eks` and `gateway/eks` → `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 `gateway/eks/authz.go`; this clears when authorization obtains the ARN from the owner, which an incarnation suffix would require. The owner's `Manifest`/`Report` beside the `contracts/eks/v1` types is deliberate duplication, not debt.
|
||||
- `agents/rds/agent` (dependency, narrowed): engine metadata now arrives through the agent-declared `EngineCatalog`, but the agent still imports `handlers/rds` for the command protocol, wire and record types (`Command*`, `Parameter`, `EngineHealth*`, `BootstrapMode*`) and API output types. This clears when those move behind a guest/controller wire contract.
|
||||
- `gateway/rds` → `handlers/rds` (`InstanceSizing`): the gateway obtains its `Sizing` through the RDS handler package's adapter rather than its own composition root. This clears when the gateway's RDS composition moves to `runtime/roles/awsgw` and builds `Sizing` there.
|
||||
- `domains/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.
|
||||
- `domains/ecs` → `domains/ecs/taskdefinition` (owner bypass): `tags.go` reads and writes the revision record through its generic tag registry; this clears when ECS tagging invokes an owner-provided tag mutation capability, and moving `tags.go` to `domains/ecs/awsapi` alone does not clear it. `taskDefinitionResolver` returns the persisted `Record` to task and service launch code; when the task and service owners are extracted, replace the record crossing that boundary with a consumer-owned resolved task-definition (launch-spec) projection. Moving packages has not made the record private.
|
||||
- `domains/ecs/taskdefinition` (deliberate duplication, not debt): private `getJSON`, `putJSON` and `keysWithPrefix` copy the raw-KV helpers in `domains/ecs` so the extraction keeps ECS's get/put/list error and recovery semantics. Revisit only if a later, tested state abstraction proves it preserves those semantics; do not migrate ECS to `kvstore` merely to remove them.
|
||||
- `domains/eks/cluster` → `domains/eks/access` (legacy edges): `ResolveTokenReview` opens the EKS bucket in the gateway process and reads records through the exported `access.Get`; this clears when token review asks the owner across a contract rather than opening the bucket itself. `DeleteClusterPrefix` erases access-entry keys as part of its blanket cluster sweep; this clears when cluster delete invokes an owner-provided removal of the cluster's entries. The owner returns the persisted `Record` to the AWS adapter and token review; replace it with consumer-owned projections when those consumers are extracted.
|
||||
- `domains/eks/cluster` → `domains/eks/addon` (legacy edges): `DeleteClusterPrefix` erases add-on records and manifests in its blanket cluster sweep; this clears when cluster delete invokes an add-on-owned teardown capability. `stageGPUDeviceAddon` is a temporary node-group-to-add-on orchestration edge: best-effort, errors logged and swallowed, never undone on node-group delete; it clears when the node-group owner is extracted and the dependency is designed. `ClusterReconciler` hosts the add-on status subscription; it clears when add-on delivery observation has its own host under the lifecycle correction. Cluster existence is checked in the handler, not through an owner-declared capability; it moves into the owner when cluster admission is defined (today status is ignored). Guest wire types (`AddonStatusReport`, `AddonDeliveryPhase`, `AddonStatusSubject`, `StagedAddonManifest`, `InternalAddonsResponse`) are owned by `contracts/eks/v1`, which `domains/eks/cluster`, `gateway/eks` and `agents/eks/gatewayfetch` import directly.
|
||||
- `domains/eks/cluster` and `domains/eks/awsapi` → `domains/eks/addon` (further edges): the AWS adapter reads the persisted `Record` through `addon.Get`/`addon.List`; this clears when the owner returns a view and the add-on adapter moves to `domains/eks/awsapi`. The guest route reaches the owner through the `handlers_eks` NATS RPC (`ListStagedAddonManifests`); this clears with per-resource commands behind EKS registration. The add-on ARN is derived in both the owner and `domains/eks/awsapi/authz.go`; this clears when authorization obtains the ARN from the owner, which an incarnation suffix would require. The owner's `Manifest`/`Report` beside the `contracts/eks/v1` types is deliberate duplication, not debt.
|
||||
- `agents/rds/agent` (dependency, narrowed): engine metadata now arrives through the agent-declared `EngineCatalog`, but the agent still imports `domains/rds/instance` for the command protocol, wire and record types (`Command*`, `Parameter`, `EngineHealth*`, `BootstrapMode*`) and API output types. This clears when those move behind a guest/controller wire contract.
|
||||
- `domains/rds/awsapi` → `domains/rds/instance` (`InstanceSizing`): the gateway obtains its `Sizing` through the RDS handler package's adapter rather than its own composition root. This clears when the gateway's RDS composition moves to `runtime/roles/awsgw` and builds `Sizing` there.
|
||||
- `domains/admission/quota` → `domains/ec2/instancetypes`: `reconcile.go`, `records.go` and `vcpu.go` read `DefaultVCPUs` to size instances without a recorded vCPU count. This clears when quota receives vCPU sizing through a consumer-owned capability or an EC2 projection.
|
||||
- IAM implementation consumers in STS, Ochre and ELBv2: `handlers/sts` imports `handlers/iam` across five files (`IAMService`, `SessionRevoker`, `EncryptSecret`/`DecryptSecret`, `AccessKeyStatusActive`, `ValidateTrustPolicyDocument`, `PolicyEffectAllow`/`PolicyEffectDeny`, `IAMAccountBucketName`, `OIDCProviderARN`, `OIDCProviderKey`), not only `oidc_jwks.go`; `gateway/sts/GetCallerIdentity.go` takes `IAMService`; `gateway/bedrock/credentials.go` uses `EncryptSecret`/`DecryptSecret`; and `handlers/elbv2/{lb_role,service_impl}.go` use `EnsureSystemInstanceProfile` and `SystemInstanceRoleEnsurer`. Each clears when its consumer receives a narrow IAM capability, or a shared crypto boundary for the secret helpers, rather than importing the IAM implementation.
|
||||
- `handlers/elbv2` (cross-domain): `service_impl.go` imports `domains/acm` (`Store`, `NewStore`), `domains/dns` (`ELBChanges`, `ELBName`, `Change`, `ActionUpsert`, `ResolveBaseDomain`), `domains/ec2/vpc` (`VPCServiceImpl`) and `domains/network/topology` (`Port`), and `launcher.go` re-exports `domains/ec2/systeminstance` launch types as aliases. These clear when ELBv2 is extracted and each edge becomes a consumer-owned capability, which ADR-0001 sequences after the ELBv2 data-plane boundary is settled.
|
||||
- `docs/service-interfaces.yaml` (ECS coverage gap, closed): the traced `ecs.bus.*` family and the ECS AWS-API action subjects are both listed. The action subjects are declared as `ecs.*`, the same wildcard convention as `rds.*`: `handlers/ecs/service_nats.go` publishes them from the gateway and `daemon/daemon.go` subscribes to them. Declaring `ecs.bus.*` alone had made `ecs` a managed prefix, so `manifest-lint` failed CI on all 34 undeclared action subjects until this was added.
|
||||
- IAM implementation consumers in STS, Ochre and ELBv2: `domains/sts` imports `domains/iam` across five files (`IAMService`, `SessionRevoker`, `EncryptSecret`/`DecryptSecret`, `AccessKeyStatusActive`, `ValidateTrustPolicyDocument`, `PolicyEffectAllow`/`PolicyEffectDeny`, `IAMAccountBucketName`, `OIDCProviderARN`, `OIDCProviderKey`), not only `oidc_jwks.go`; `domains/sts/awsapi/GetCallerIdentity.go` takes `IAMService`; `domains/ochre/credentials.go` uses `EncryptSecret`/`DecryptSecret`; and `domains/elbv2/{lb_role,service_impl}.go` use `EnsureSystemInstanceProfile` and `SystemInstanceRoleEnsurer`. Each clears when its consumer receives a narrow IAM capability, or a shared crypto boundary for the secret helpers, rather than importing the IAM implementation.
|
||||
- `domains/elbv2` (cross-domain): `service_impl.go` imports `domains/acm` (`Store`, `NewStore`), `domains/dns` (`ELBChanges`, `ELBName`, `Change`, `ActionUpsert`, `ResolveBaseDomain`), `domains/ec2/vpc` (`VPCServiceImpl`) and `domains/network/topology` (`Port`), and `launcher.go` re-exports `domains/ec2/systeminstance` launch types as aliases. These clear when each edge becomes a consumer-owned capability.
|
||||
- `docs/service-interfaces.yaml` (ECS coverage gap, closed): the traced `ecs.bus.*` family and the ECS AWS-API action subjects are both listed. The action subjects are declared as `ecs.*`, the same wildcard convention as `rds.*`: `domains/ecs/service_nats.go` publishes them from the gateway and `runtime/daemon/daemon.go` subscribes to them. Declaring `ecs.bus.*` alone had made `ecs` a managed prefix, so `manifest-lint` failed CI on all 34 undeclared action subjects until this was added.
|
||||
- `contracts/ecs/v1` (dead routes): four of five bus subjects have no producer. The scheduler leader still subscribes to `instance-register`, `instance-heartbeat` and `task-state`, which can never fire, and `assign` has neither producer nor consumer; only `service-reconcile` is live. Removal condition: an owner decision to retire these routes (a successor contract under the compatibility rules) or to adopt them in the agent.
|
||||
|
||||
The layering test enforcing these rules (`architecture/layering`, a module-root repository governance test, not Spinifex product code; run by `make test`, `test-cover` and `test-race`) is blocking, with a ratchet: the debt recorded in this section is narrowly allow-listed by owner and removal condition, and any new, unrecorded forbidden edge fails local preflight and CI. The allow-list mirrors the edges in this section that break an import rule; same-domain legacy edges, placement residuals and security notes are not import-rule violations and have no allow-list entry.
|
||||
@@ -512,7 +527,7 @@ First-resource ranking:
|
||||
|
||||
Blocked dependencies recorded, not designed around:
|
||||
|
||||
- RDS instance: quota admission, the duplicated expected-node fan-out (`gateway/rds/enginecatalog.go`, `daemon/rds_deps.go`), IAM ensurer and Bluebottle master key.
|
||||
- RDS instance: quota admission, the duplicated expected-node fan-out (`domains/rds/awsapi/enginecatalog.go`, `daemon/rds_deps.go`), IAM ensurer and Bluebottle master key.
|
||||
- RDS `DescribeOrderableDBInstanceOptions`: the EC2 instance-type-availability projection.
|
||||
- ECS task, service and capacity: `iam:PassRole` in the gateway adapter, IAM `ecsInstanceRole`, and EC2/ELBv2/EIP adapters called as internal APIs.
|
||||
- EKS nodegroup and cluster: IAM role and instance-profile creation, Bluebottle `auth`, the NATS CA, and EKS identity (token review, OIDC).
|
||||
@@ -521,7 +536,7 @@ Blocked dependencies recorded, not designed around:
|
||||
Findings to resolve before extraction:
|
||||
|
||||
- `dev` is merged in (`9e77181f2`), including `ModifyDBSubnetGroup` (#1152); the subnet-group inventory is superseded by its lifecycle contract, `docs/package-boundary/rds-db-subnet-group-lifecycle.md`.
|
||||
- The EKS control-plane template persists the OIDC signing key and k3s join token; see the `handlers/eks` potential security risk in the dependency-rule debt.
|
||||
- The EKS control-plane template persists the OIDC signing key and k3s join token; see the `domains/eks/cluster` potential security risk in the dependency-rule debt.
|
||||
|
||||
## ADR-0007 instance-state inventory (slice 0)
|
||||
|
||||
@@ -650,41 +665,41 @@ series above; "needs decision" is open.
|
||||
|
||||
| Legacy path | What remains | What blocks it |
|
||||
|---|---|---|
|
||||
| `handlers/iam` | Private identity state and credential issuance; consumed by the EC2 instance and spot-instance AWS adapters, `handlers/sts`, `gateway/sts`, `gateway/bedrock`, the ELBv2 system-instance role path, ECR/Ochre signing-key crypto and `operator/cli`. | Deferred: the ADR's `handlers/iam` row states a shared IAM contract is not inferred from current KV readers; each cross-domain reader needs an IAM capability first. |
|
||||
| `handlers/sts` | Everything except the EKS OIDC JWKS lookup, which now reaches `handlers/eks` only through the injected `ClusterJWKSResolver`. | Deferred: per-consumer IAM capabilities (dependency-rule debt above); `handlers/sts` still imports `handlers/iam` directly. |
|
||||
| `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/eks` | Nodegroup and cluster resources (`access` and `addon` already extracted). | Deferred: ADR-0004 blocked-dependency list — IAM role/instance-profile creation, Bluebottle `auth`, the NATS CA, and EKS identity (token review, OIDC). |
|
||||
| `handlers/rds` | The RDS instance resource (subnet group, parameter group and engine catalogue already extracted). | Deferred: ADR-0004 blocked-dependency list — quota admission, the duplicated expected-node fan-out, the IAM ensurer and the Bluebottle master key. |
|
||||
| `handlers/elbv2` | Everything; not yet touched. | Deferred: ADR-0001 sequences this after the ELBv2 data-plane boundary is settled. Needs decision: what evidence establishes that boundary is settled. |
|
||||
| `handlers/iam/mock` | A test double for `handlers/iam`. | Moves with `handlers/iam`; it has no independent target. |
|
||||
| `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` | — | 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`. |
|
||||
| `handlers/iam/mock` | — | Done. Moved with the implementation to `domains/iam/mock`. |
|
||||
| `gateway` (root) | Generic HTTP route assembly, SigV4/registry authentication, the policy gate, request-audit middleware, request-context keys, and the legacy per-service dispatch switch for every service not yet registered. | Deferred: the ADR-0002 phases above (auth/IAM and request-context boundary design) gate moving the audit, context-key and legacy-dispatch mechanics. |
|
||||
| `gateway/bedrock` | The Ochre domain core, its stores and the inference engine, together with the Bedrock action layer, in one package with internal cycles. | Deferred: needs the split plan recorded above — domain core and stores to `domains/ochre/<resource>`, inference to an Ochre-owned package, and only the action layer to `domains/ochre/awsapi`. |
|
||||
| `gateway/{ecs,eks,rds,iam,sts,elbv2}` | Each service's AWS action adapters and dispatch, not yet registered through ADR-0002. | Deferred: the ADR-0002 registration contract has only reached ECR and ACM so far; each service needs its own registration slice. |
|
||||
| `gateway/bedrock` | — | Done. The whole package moved into `domains/ochre` in `spinifex#1266` with its package-private core and tests intact and no behaviour change. Separating the action adapters into `domains/ochre/awsapi` remains deferred, recorded as `domains/ochre` (action adapters) debt above. |
|
||||
| `gateway/{iam,sts,eks,rds}` | — | Done. The action adapters moved to `domains/{iam,sts,eks,rds}/awsapi` in `spinifex#1265` with no API, NATS subject, storage or behaviour change. ADR-0001 does not make ADR-0002 registration a prerequisite for relocating a domain's adapters, as the EC2 adapter moves already showed; the root `gateway` dispatch that still imports them is recorded `gateway` (service registration) debt. |
|
||||
| `gateway/{ecs,elbv2}` | — | Done. The action adapters moved to `domains/{ecs,elbv2}/awsapi` in `spinifex#1273` with no API, NATS subject, storage or behaviour change; the root `gateway` dispatch that still imports them is recorded `gateway` (service registration) debt. |
|
||||
| `gateway/spx` | `promote_image.go` and `spx.go` dispatch (`storage.go` already consumes `contracts/operator/v1`). | Needs decision: which package owns operator-plane action dispatch registration — an `ingress/aws`-style seam for the operator plane, or `gateway` stays its home. |
|
||||
| `gateway/tagging` | — | Done. `domains/tagging` owns the read-only cross-resource `GetResources` aggregation and its API semantics (`0d830e901`, `aedd0b2c4`); AWSGW composes the owner listers, the EC2 tag index and ELBv2 (`f3545e3ea`); the root `gateway` keeps only the legacy dispatch in `gateway/tagging.go`. The package is removed, and its two allowlist rows and frozen-legacy entry are gone. Owner tag mutation capabilities remain future direction, so the RDS and ECS tag-writing edges above stay open. |
|
||||
| `services/viperblockd` | — | Done. The adapter is `providers/ebs/viperblock` (with the read-only `dirty` reader), the shared vocabulary is `contracts/viperblockd/legacy/v1`, and the role is `runtime/roles/viperblockd`. `services/` is empty. |
|
||||
| `utils` | — | Done and deleted. `ec2_subject.go` went to `contracts/ec2/v1`, the vpcd protocol to `contracts/network/v1` with the typed client in `domains/network/projection`, and the master-key loader to `domains/ec2/ebs/encryption`. |
|
||||
| `vpcd` | — | Done. The IMDS local-file reader and record-space opener moved to `domains/ec2/instance/state` and are injected through `guestmetadata.LocalStateReader`/`RecordLoader` (recorded above), so `vpcd` no longer imports `daemon` directly; its inter-process boundary now exists (`contracts/network/v1`, with `domains/network/subscribers` as the consumer and `domains/network/projection` as the producer client). The package moved unchanged to `runtime/roles/vpcd` (recorded below), and the legacy directory is removed. |
|
||||
| `daemon` | Reserved as `runtime/daemon`'s home only; not split or moved. | Deferred: Q-28's write-side consolidation is a prerequisite ADR-0001 names explicitly. |
|
||||
| `daemon` | `runtime/daemon` (whole-package move, above) | Done. Q-28's ownership and deployment refactor remains deferred debt; the amended ADR-0001 row makes it not a relocation prerequisite. |
|
||||
| `admin` | — | Done and deleted. The operator use cases moved to `operator/admin`, image promotion to `domains/ec2/image`, the bootstrap account identifiers to `foundation/aws/identifiers`, and the credential-format generators to `foundation/aws/credentials` (recorded above). Operator bootstrap's direct credential generation stays recorded as debt. |
|
||||
| `accountteardown` | The whole package, a frozen legacy root. Its direct cleanup calls into `domains/ec2/awsapi/*` action adapters, other domain and handler services, the object-store provider, raw NATS subjects and a raw KV bucket stay as recorded debt. | Assessed for relocation and not moved. ADR-0001's `accountteardown` row requires conversion to domain-owned cleanup capabilities before moving, and ADR-0008 does not authorise a mechanical rename. It remains a legacy root until its direct cleanup calls are converted to domain-owned capabilities; its destination after that conversion is `operator/accountlifecycle`. |
|
||||
| `lbagent` | The whole package; `handlers/elbv2` imports it as a domain-to-agent implementation edge (recorded above). | Deferred: Q-30 reserves `agents/elbv2` as its destination but forbids moving it as an isolated rename ahead of the common-agent design. |
|
||||
| `accountteardown` | `operator/accountlifecycle` (whole-package move, above) | Done. Its direct cleanup calls into `domains/ec2/awsapi/*` action adapters, other domain services, the object-store provider, raw NATS subjects and a raw KV bucket stay recorded debt; the amended ADR-0001 row makes replacing them with owner capabilities not a relocation prerequisite. |
|
||||
| `lbagent` | `agents/elbv2` (whole-package move, above) | Done. Q-30's data-plane boundary remains deferred debt; the amended ADR-0001 row makes it not a relocation prerequisite. The `domains/elbv2` import stays recorded debt. |
|
||||
| `migrate` | Nothing at the old path. | Split, not an exact ADR row: ADR-0001 maps `migrate` to `foundation/state/migrate`, but the package held two responsibilities. The KV migration engine and its tests are in `foundation/state/migrate` (`8cd9cff75`); the config-file and object-store upgrade workflow moved to `operator/upgrade` (`919d5bfa6`) by owner decision. Needs an ADR-0001 map clarification before this row can be claimed as target conformance. |
|
||||
|
||||
### Remaining legacy roots by blocker class
|
||||
|
||||
Five legacy roots remain: `accountteardown`, `daemon`, `gateway`, `handlers` and `lbagent`.
|
||||
One legacy root remains: `gateway`.
|
||||
The rows above give each path's blocker; this summary groups them so a deferral is not mistaken for unfinished mechanical work.
|
||||
ADR-0001 completion is structural, so none of these is cleared by remediating service lifecycle behaviour.
|
||||
|
||||
| Class | Paths | Status |
|
||||
|---|---|---|
|
||||
| Explicitly deferred by ADR-0001 | `daemon` (Q-28), `lbagent` (Q-30) | Closed for this refactor; the ADR names the prerequisite. |
|
||||
| No missing boundary (candidate moves) | `handlers/ecs/bus` → `contracts/ecs/v1` (`a130f6b8e`); `admin/images_promote.go` → `domains/ec2/image` (`263c58167`); `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/iam` and `handlers/iam/mock` (per-consumer IAM capabilities); `handlers/{ecs,eks,rds}` (ADR-0004 blocked-dependency lists); `handlers/elbv2` (data-plane boundary evidence); `accountteardown` (domain cleanup capabilities); `gateway` root (ADR-0002 auth and request-context phase); `gateway/{iam,sts,ecs,eks,rds,elbv2}` (each follows its `handlers/<svc>`, because a new-path `awsapi` cannot import a legacy handler); `gateway/bedrock` (the recorded Ochre split plan) | Recorded debt with removal conditions above. |
|
||||
| Broader boundary or follows another package | `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 `handlers/iam` itself, STS's IAM reads, Ochre's IAM reads, the ELBv2 role path and the ECS, EKS and RDS ensurers.
|
||||
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.
|
||||
|
||||
`runtime/roles/awsgw` carries two recorded residuals from its own move (`931d86d5c`
|
||||
above) that are not yet re-homed: `runQuotaReconcile`/`quotaKeyFor` belong to
|
||||
|
||||
@@ -17,13 +17,11 @@ tags:
|
||||
|
||||
## Overview
|
||||
|
||||
The platform serves **413 operations** across the AWS APIs below. Every page names the operations its service implements from the pinned model, generated from the dispatch tables on each build rather than written by hand.
|
||||
The platform serves **392 operations** across the AWS APIs below. Every page names the operations its service implements from the pinned model, generated from the dispatch tables on each build rather than written by hand.
|
||||
|
||||
| Service | Operations |
|
||||
|---|---:|
|
||||
| [ACM](/coverage/acm) | 9 |
|
||||
| [Bedrock](/coverage/bedrock) | 16 |
|
||||
| [Bedrock Runtime](/coverage/bedrock-runtime) | 5 |
|
||||
| [EC2](/coverage/ec2) | 127 |
|
||||
| [ECR](/coverage/ecr) | 23 |
|
||||
| [ECS](/coverage/ecs) | 31 |
|
||||
@@ -33,4 +31,4 @@ The platform serves **413 operations** across the AWS APIs below. Every page nam
|
||||
| [RDS](/coverage/rds) | 27 |
|
||||
| [S3](/coverage/s3) | 26 |
|
||||
| [STS](/coverage/sts) | 5 |
|
||||
| **Total** | **413** |
|
||||
| **Total** | **392** |
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
title: "Bedrock Runtime API Coverage"
|
||||
seoTitle: "Amazon Bedrock Runtime API Coverage — Spinifex Docs"
|
||||
description: "The Amazon Bedrock Runtime API operations Spinifex implements, covering model invocation, the Converse API, streamed responses and standalone guardrail checks."
|
||||
category: "Coverage"
|
||||
sections:
|
||||
- overview
|
||||
tags:
|
||||
- aws
|
||||
- compatibility
|
||||
- coverage
|
||||
- bedrock
|
||||
- ai
|
||||
- inference
|
||||
---
|
||||
|
||||
# Bedrock Runtime API Coverage
|
||||
|
||||
## Overview
|
||||
|
||||
Spinifex implements **5 operations** in the Bedrock Runtime `2023-09-30` API model.
|
||||
|
||||
### Operations
|
||||
|
||||
| Operation |
|
||||
|---|
|
||||
| `ApplyGuardrail` |
|
||||
| `Converse` |
|
||||
| `ConverseStream` |
|
||||
| `InvokeModel` |
|
||||
| `InvokeModelWithResponseStream` |
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
title: "Bedrock API Coverage"
|
||||
seoTitle: "Amazon Bedrock API Coverage on Spinifex — Spinifex Docs"
|
||||
description: "The Amazon Bedrock control-plane API operations Spinifex implements, covering foundation models, guardrails, provisioned throughput and invocation logging."
|
||||
category: "Coverage"
|
||||
sections:
|
||||
- overview
|
||||
tags:
|
||||
- aws
|
||||
- compatibility
|
||||
- coverage
|
||||
- bedrock
|
||||
- ai
|
||||
- models
|
||||
---
|
||||
|
||||
# Bedrock API Coverage
|
||||
|
||||
## Overview
|
||||
|
||||
Spinifex implements **16 operations** in the Bedrock `2023-04-20` API model.
|
||||
|
||||
### Operations
|
||||
|
||||
| Operation |
|
||||
|---|
|
||||
| `CreateGuardrail` |
|
||||
| `CreateGuardrailVersion` |
|
||||
| `CreateProvisionedModelThroughput` |
|
||||
| `DeleteGuardrail` |
|
||||
| `DeleteModelInvocationLoggingConfiguration` |
|
||||
| `DeleteProvisionedModelThroughput` |
|
||||
| `GetFoundationModel` |
|
||||
| `GetGuardrail` |
|
||||
| `GetModelInvocationLoggingConfiguration` |
|
||||
| `GetProvisionedModelThroughput` |
|
||||
| `ListFoundationModels` |
|
||||
| `ListGuardrails` |
|
||||
| `ListProvisionedModelThroughputs` |
|
||||
| `PutModelInvocationLoggingConfiguration` |
|
||||
| `UpdateGuardrail` |
|
||||
| `UpdateProvisionedModelThroughput` |
|
||||
@@ -114,303 +114,6 @@
|
||||
"extra": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"service": "bedrock",
|
||||
"api_version": "2023-04-20",
|
||||
"opaque": false,
|
||||
"note": "",
|
||||
"operations": {
|
||||
"modelled": [
|
||||
"BatchDeleteAdvancedPromptOptimizationJob",
|
||||
"BatchDeleteEvaluationJob",
|
||||
"CancelAutomatedReasoningPolicyBuildWorkflow",
|
||||
"CreateAdvancedPromptOptimizationJob",
|
||||
"CreateAutomatedReasoningPolicy",
|
||||
"CreateAutomatedReasoningPolicyTestCase",
|
||||
"CreateAutomatedReasoningPolicyVersion",
|
||||
"CreateCustomModel",
|
||||
"CreateCustomModelDeployment",
|
||||
"CreateEvaluationJob",
|
||||
"CreateFoundationModelAgreement",
|
||||
"CreateGuardrail",
|
||||
"CreateGuardrailVersion",
|
||||
"CreateInferenceProfile",
|
||||
"CreateMarketplaceModelEndpoint",
|
||||
"CreateModelCopyJob",
|
||||
"CreateModelCustomizationJob",
|
||||
"CreateModelImportJob",
|
||||
"CreateModelInvocationJob",
|
||||
"CreatePromptRouter",
|
||||
"CreateProvisionedModelThroughput",
|
||||
"DeleteAutomatedReasoningPolicy",
|
||||
"DeleteAutomatedReasoningPolicyBuildWorkflow",
|
||||
"DeleteAutomatedReasoningPolicyTestCase",
|
||||
"DeleteCustomModel",
|
||||
"DeleteCustomModelDeployment",
|
||||
"DeleteEnforcedGuardrailConfiguration",
|
||||
"DeleteFoundationModelAgreement",
|
||||
"DeleteGuardrail",
|
||||
"DeleteImportedModel",
|
||||
"DeleteInferenceProfile",
|
||||
"DeleteMarketplaceModelEndpoint",
|
||||
"DeleteModelInvocationLoggingConfiguration",
|
||||
"DeletePromptRouter",
|
||||
"DeleteProvisionedModelThroughput",
|
||||
"DeleteResourcePolicy",
|
||||
"DeregisterMarketplaceModelEndpoint",
|
||||
"ExportAutomatedReasoningPolicyVersion",
|
||||
"GetAccountDataRetention",
|
||||
"GetAdvancedPromptOptimizationJob",
|
||||
"GetAutomatedReasoningPolicy",
|
||||
"GetAutomatedReasoningPolicyAnnotations",
|
||||
"GetAutomatedReasoningPolicyBuildWorkflow",
|
||||
"GetAutomatedReasoningPolicyBuildWorkflowResultAssets",
|
||||
"GetAutomatedReasoningPolicyNextScenario",
|
||||
"GetAutomatedReasoningPolicyTestCase",
|
||||
"GetAutomatedReasoningPolicyTestResult",
|
||||
"GetCustomModel",
|
||||
"GetCustomModelDeployment",
|
||||
"GetEvaluationJob",
|
||||
"GetFoundationModel",
|
||||
"GetFoundationModelAvailability",
|
||||
"GetGuardrail",
|
||||
"GetImportedModel",
|
||||
"GetInferenceProfile",
|
||||
"GetMarketplaceModelEndpoint",
|
||||
"GetModelCopyJob",
|
||||
"GetModelCustomizationJob",
|
||||
"GetModelImportJob",
|
||||
"GetModelInvocationJob",
|
||||
"GetModelInvocationLoggingConfiguration",
|
||||
"GetPromptRouter",
|
||||
"GetProvisionedModelThroughput",
|
||||
"GetResourcePolicy",
|
||||
"GetUseCaseForModelAccess",
|
||||
"ListAdvancedPromptOptimizationJobs",
|
||||
"ListAutomatedReasoningPolicies",
|
||||
"ListAutomatedReasoningPolicyBuildWorkflows",
|
||||
"ListAutomatedReasoningPolicyTestCases",
|
||||
"ListAutomatedReasoningPolicyTestResults",
|
||||
"ListCustomModelDeployments",
|
||||
"ListCustomModels",
|
||||
"ListEnforcedGuardrailsConfiguration",
|
||||
"ListEvaluationJobs",
|
||||
"ListFoundationModelAgreementOffers",
|
||||
"ListFoundationModels",
|
||||
"ListGuardrails",
|
||||
"ListImportedModels",
|
||||
"ListInferenceProfiles",
|
||||
"ListMarketplaceModelEndpoints",
|
||||
"ListModelCopyJobs",
|
||||
"ListModelCustomizationJobs",
|
||||
"ListModelImportJobs",
|
||||
"ListModelInvocationJobs",
|
||||
"ListPromptRouters",
|
||||
"ListProvisionedModelThroughputs",
|
||||
"ListTagsForResource",
|
||||
"PutAccountDataRetention",
|
||||
"PutEnforcedGuardrailConfiguration",
|
||||
"PutModelInvocationLoggingConfiguration",
|
||||
"PutResourcePolicy",
|
||||
"PutUseCaseForModelAccess",
|
||||
"RegisterMarketplaceModelEndpoint",
|
||||
"StartAutomatedReasoningPolicyBuildWorkflow",
|
||||
"StartAutomatedReasoningPolicyTestWorkflow",
|
||||
"StopAdvancedPromptOptimizationJob",
|
||||
"StopEvaluationJob",
|
||||
"StopModelCustomizationJob",
|
||||
"StopModelInvocationJob",
|
||||
"TagResource",
|
||||
"UntagResource",
|
||||
"UpdateAutomatedReasoningPolicy",
|
||||
"UpdateAutomatedReasoningPolicyAnnotations",
|
||||
"UpdateAutomatedReasoningPolicyTestCase",
|
||||
"UpdateCustomModelDeployment",
|
||||
"UpdateGuardrail",
|
||||
"UpdateMarketplaceModelEndpoint",
|
||||
"UpdateProvisionedModelThroughput"
|
||||
],
|
||||
"registered": [
|
||||
"CreateGuardrail",
|
||||
"CreateGuardrailVersion",
|
||||
"CreateProvisionedModelThroughput",
|
||||
"DeleteGuardrail",
|
||||
"DeleteModelInvocationLoggingConfiguration",
|
||||
"DeleteProvisionedModelThroughput",
|
||||
"GetFoundationModel",
|
||||
"GetGuardrail",
|
||||
"GetModelInvocationLoggingConfiguration",
|
||||
"GetProvisionedModelThroughput",
|
||||
"ListFoundationModels",
|
||||
"ListGuardrails",
|
||||
"ListProvisionedModelThroughputs",
|
||||
"PutModelInvocationLoggingConfiguration",
|
||||
"UpdateGuardrail",
|
||||
"UpdateProvisionedModelThroughput"
|
||||
],
|
||||
"implemented": [
|
||||
"CreateGuardrail",
|
||||
"CreateGuardrailVersion",
|
||||
"CreateProvisionedModelThroughput",
|
||||
"DeleteGuardrail",
|
||||
"DeleteModelInvocationLoggingConfiguration",
|
||||
"DeleteProvisionedModelThroughput",
|
||||
"GetFoundationModel",
|
||||
"GetGuardrail",
|
||||
"GetModelInvocationLoggingConfiguration",
|
||||
"GetProvisionedModelThroughput",
|
||||
"ListFoundationModels",
|
||||
"ListGuardrails",
|
||||
"ListProvisionedModelThroughputs",
|
||||
"PutModelInvocationLoggingConfiguration",
|
||||
"UpdateGuardrail",
|
||||
"UpdateProvisionedModelThroughput"
|
||||
],
|
||||
"stubbed": [],
|
||||
"unsupported": [],
|
||||
"missing": [
|
||||
"BatchDeleteAdvancedPromptOptimizationJob",
|
||||
"BatchDeleteEvaluationJob",
|
||||
"CancelAutomatedReasoningPolicyBuildWorkflow",
|
||||
"CreateAdvancedPromptOptimizationJob",
|
||||
"CreateAutomatedReasoningPolicy",
|
||||
"CreateAutomatedReasoningPolicyTestCase",
|
||||
"CreateAutomatedReasoningPolicyVersion",
|
||||
"CreateCustomModel",
|
||||
"CreateCustomModelDeployment",
|
||||
"CreateEvaluationJob",
|
||||
"CreateFoundationModelAgreement",
|
||||
"CreateInferenceProfile",
|
||||
"CreateMarketplaceModelEndpoint",
|
||||
"CreateModelCopyJob",
|
||||
"CreateModelCustomizationJob",
|
||||
"CreateModelImportJob",
|
||||
"CreateModelInvocationJob",
|
||||
"CreatePromptRouter",
|
||||
"DeleteAutomatedReasoningPolicy",
|
||||
"DeleteAutomatedReasoningPolicyBuildWorkflow",
|
||||
"DeleteAutomatedReasoningPolicyTestCase",
|
||||
"DeleteCustomModel",
|
||||
"DeleteCustomModelDeployment",
|
||||
"DeleteEnforcedGuardrailConfiguration",
|
||||
"DeleteFoundationModelAgreement",
|
||||
"DeleteImportedModel",
|
||||
"DeleteInferenceProfile",
|
||||
"DeleteMarketplaceModelEndpoint",
|
||||
"DeletePromptRouter",
|
||||
"DeleteResourcePolicy",
|
||||
"DeregisterMarketplaceModelEndpoint",
|
||||
"ExportAutomatedReasoningPolicyVersion",
|
||||
"GetAccountDataRetention",
|
||||
"GetAdvancedPromptOptimizationJob",
|
||||
"GetAutomatedReasoningPolicy",
|
||||
"GetAutomatedReasoningPolicyAnnotations",
|
||||
"GetAutomatedReasoningPolicyBuildWorkflow",
|
||||
"GetAutomatedReasoningPolicyBuildWorkflowResultAssets",
|
||||
"GetAutomatedReasoningPolicyNextScenario",
|
||||
"GetAutomatedReasoningPolicyTestCase",
|
||||
"GetAutomatedReasoningPolicyTestResult",
|
||||
"GetCustomModel",
|
||||
"GetCustomModelDeployment",
|
||||
"GetEvaluationJob",
|
||||
"GetFoundationModelAvailability",
|
||||
"GetImportedModel",
|
||||
"GetInferenceProfile",
|
||||
"GetMarketplaceModelEndpoint",
|
||||
"GetModelCopyJob",
|
||||
"GetModelCustomizationJob",
|
||||
"GetModelImportJob",
|
||||
"GetModelInvocationJob",
|
||||
"GetPromptRouter",
|
||||
"GetResourcePolicy",
|
||||
"GetUseCaseForModelAccess",
|
||||
"ListAdvancedPromptOptimizationJobs",
|
||||
"ListAutomatedReasoningPolicies",
|
||||
"ListAutomatedReasoningPolicyBuildWorkflows",
|
||||
"ListAutomatedReasoningPolicyTestCases",
|
||||
"ListAutomatedReasoningPolicyTestResults",
|
||||
"ListCustomModelDeployments",
|
||||
"ListCustomModels",
|
||||
"ListEnforcedGuardrailsConfiguration",
|
||||
"ListEvaluationJobs",
|
||||
"ListFoundationModelAgreementOffers",
|
||||
"ListImportedModels",
|
||||
"ListInferenceProfiles",
|
||||
"ListMarketplaceModelEndpoints",
|
||||
"ListModelCopyJobs",
|
||||
"ListModelCustomizationJobs",
|
||||
"ListModelImportJobs",
|
||||
"ListModelInvocationJobs",
|
||||
"ListPromptRouters",
|
||||
"ListTagsForResource",
|
||||
"PutAccountDataRetention",
|
||||
"PutEnforcedGuardrailConfiguration",
|
||||
"PutResourcePolicy",
|
||||
"PutUseCaseForModelAccess",
|
||||
"RegisterMarketplaceModelEndpoint",
|
||||
"StartAutomatedReasoningPolicyBuildWorkflow",
|
||||
"StartAutomatedReasoningPolicyTestWorkflow",
|
||||
"StopAdvancedPromptOptimizationJob",
|
||||
"StopEvaluationJob",
|
||||
"StopModelCustomizationJob",
|
||||
"StopModelInvocationJob",
|
||||
"TagResource",
|
||||
"UntagResource",
|
||||
"UpdateAutomatedReasoningPolicy",
|
||||
"UpdateAutomatedReasoningPolicyAnnotations",
|
||||
"UpdateAutomatedReasoningPolicyTestCase",
|
||||
"UpdateCustomModelDeployment",
|
||||
"UpdateMarketplaceModelEndpoint"
|
||||
],
|
||||
"extra": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"service": "bedrockruntime",
|
||||
"api_version": "2023-09-30",
|
||||
"opaque": false,
|
||||
"note": "",
|
||||
"operations": {
|
||||
"modelled": [
|
||||
"ApplyGuardrail",
|
||||
"Converse",
|
||||
"ConverseStream",
|
||||
"CountTokens",
|
||||
"GetAsyncInvoke",
|
||||
"InvokeGuardrailChecks",
|
||||
"InvokeModel",
|
||||
"InvokeModelWithBidirectionalStream",
|
||||
"InvokeModelWithResponseStream",
|
||||
"ListAsyncInvokes",
|
||||
"StartAsyncInvoke"
|
||||
],
|
||||
"registered": [
|
||||
"ApplyGuardrail",
|
||||
"Converse",
|
||||
"ConverseStream",
|
||||
"InvokeModel",
|
||||
"InvokeModelWithResponseStream"
|
||||
],
|
||||
"implemented": [
|
||||
"ApplyGuardrail",
|
||||
"Converse",
|
||||
"ConverseStream",
|
||||
"InvokeModel",
|
||||
"InvokeModelWithResponseStream"
|
||||
],
|
||||
"stubbed": [],
|
||||
"unsupported": [],
|
||||
"missing": [
|
||||
"CountTokens",
|
||||
"GetAsyncInvoke",
|
||||
"InvokeGuardrailChecks",
|
||||
"InvokeModelWithBidirectionalStream",
|
||||
"ListAsyncInvokes",
|
||||
"StartAsyncInvoke"
|
||||
],
|
||||
"extra": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"service": "ec2",
|
||||
"api_version": "2016-11-15",
|
||||
|
||||
@@ -44,118 +44,6 @@ Pinned model source: `github.com/aws/api-models-aws@9b1a49292744, 2026-09-25`.
|
||||
- `UpdateAcmeEndpoint`
|
||||
- `UpdateCertificateOptions`
|
||||
|
||||
## bedrock
|
||||
|
||||
`2023-04-20` — 16 of 108 modelled operations implemented (14.8%), 0 stubbed, 0 not applicable, 0 registered outside the pinned model.
|
||||
|
||||
### Not implemented
|
||||
|
||||
- `BatchDeleteAdvancedPromptOptimizationJob`
|
||||
- `BatchDeleteEvaluationJob`
|
||||
- `CancelAutomatedReasoningPolicyBuildWorkflow`
|
||||
- `CreateAdvancedPromptOptimizationJob`
|
||||
- `CreateAutomatedReasoningPolicy`
|
||||
- `CreateAutomatedReasoningPolicyTestCase`
|
||||
- `CreateAutomatedReasoningPolicyVersion`
|
||||
- `CreateCustomModel`
|
||||
- `CreateCustomModelDeployment`
|
||||
- `CreateEvaluationJob`
|
||||
- `CreateFoundationModelAgreement`
|
||||
- `CreateInferenceProfile`
|
||||
- `CreateMarketplaceModelEndpoint`
|
||||
- `CreateModelCopyJob`
|
||||
- `CreateModelCustomizationJob`
|
||||
- `CreateModelImportJob`
|
||||
- `CreateModelInvocationJob`
|
||||
- `CreatePromptRouter`
|
||||
- `DeleteAutomatedReasoningPolicy`
|
||||
- `DeleteAutomatedReasoningPolicyBuildWorkflow`
|
||||
- `DeleteAutomatedReasoningPolicyTestCase`
|
||||
- `DeleteCustomModel`
|
||||
- `DeleteCustomModelDeployment`
|
||||
- `DeleteEnforcedGuardrailConfiguration`
|
||||
- `DeleteFoundationModelAgreement`
|
||||
- `DeleteImportedModel`
|
||||
- `DeleteInferenceProfile`
|
||||
- `DeleteMarketplaceModelEndpoint`
|
||||
- `DeletePromptRouter`
|
||||
- `DeleteResourcePolicy`
|
||||
- `DeregisterMarketplaceModelEndpoint`
|
||||
- `ExportAutomatedReasoningPolicyVersion`
|
||||
- `GetAccountDataRetention`
|
||||
- `GetAdvancedPromptOptimizationJob`
|
||||
- `GetAutomatedReasoningPolicy`
|
||||
- `GetAutomatedReasoningPolicyAnnotations`
|
||||
- `GetAutomatedReasoningPolicyBuildWorkflow`
|
||||
- `GetAutomatedReasoningPolicyBuildWorkflowResultAssets`
|
||||
- `GetAutomatedReasoningPolicyNextScenario`
|
||||
- `GetAutomatedReasoningPolicyTestCase`
|
||||
- `GetAutomatedReasoningPolicyTestResult`
|
||||
- `GetCustomModel`
|
||||
- `GetCustomModelDeployment`
|
||||
- `GetEvaluationJob`
|
||||
- `GetFoundationModelAvailability`
|
||||
- `GetImportedModel`
|
||||
- `GetInferenceProfile`
|
||||
- `GetMarketplaceModelEndpoint`
|
||||
- `GetModelCopyJob`
|
||||
- `GetModelCustomizationJob`
|
||||
- `GetModelImportJob`
|
||||
- `GetModelInvocationJob`
|
||||
- `GetPromptRouter`
|
||||
- `GetResourcePolicy`
|
||||
- `GetUseCaseForModelAccess`
|
||||
- `ListAdvancedPromptOptimizationJobs`
|
||||
- `ListAutomatedReasoningPolicies`
|
||||
- `ListAutomatedReasoningPolicyBuildWorkflows`
|
||||
- `ListAutomatedReasoningPolicyTestCases`
|
||||
- `ListAutomatedReasoningPolicyTestResults`
|
||||
- `ListCustomModelDeployments`
|
||||
- `ListCustomModels`
|
||||
- `ListEnforcedGuardrailsConfiguration`
|
||||
- `ListEvaluationJobs`
|
||||
- `ListFoundationModelAgreementOffers`
|
||||
- `ListImportedModels`
|
||||
- `ListInferenceProfiles`
|
||||
- `ListMarketplaceModelEndpoints`
|
||||
- `ListModelCopyJobs`
|
||||
- `ListModelCustomizationJobs`
|
||||
- `ListModelImportJobs`
|
||||
- `ListModelInvocationJobs`
|
||||
- `ListPromptRouters`
|
||||
- `ListTagsForResource`
|
||||
- `PutAccountDataRetention`
|
||||
- `PutEnforcedGuardrailConfiguration`
|
||||
- `PutResourcePolicy`
|
||||
- `PutUseCaseForModelAccess`
|
||||
- `RegisterMarketplaceModelEndpoint`
|
||||
- `StartAutomatedReasoningPolicyBuildWorkflow`
|
||||
- `StartAutomatedReasoningPolicyTestWorkflow`
|
||||
- `StopAdvancedPromptOptimizationJob`
|
||||
- `StopEvaluationJob`
|
||||
- `StopModelCustomizationJob`
|
||||
- `StopModelInvocationJob`
|
||||
- `TagResource`
|
||||
- `UntagResource`
|
||||
- `UpdateAutomatedReasoningPolicy`
|
||||
- `UpdateAutomatedReasoningPolicyAnnotations`
|
||||
- `UpdateAutomatedReasoningPolicyTestCase`
|
||||
- `UpdateCustomModelDeployment`
|
||||
- `UpdateMarketplaceModelEndpoint`
|
||||
|
||||
## bedrockruntime
|
||||
|
||||
`2023-09-30` — 5 of 11 modelled operations implemented (45.5%), 0 stubbed, 0 not applicable, 0 registered outside the pinned model.
|
||||
|
||||
### Not implemented
|
||||
|
||||
- `CountTokens`
|
||||
- `GetAsyncInvoke`
|
||||
- `InvokeGuardrailChecks`
|
||||
- `InvokeModelWithBidirectionalStream`
|
||||
- `ListAsyncInvokes`
|
||||
- `StartAsyncInvoke`
|
||||
|
||||
## ec2
|
||||
|
||||
`2016-11-15` — 127 of 804 modelled operations implemented (15.8%), 0 stubbed, 39 not applicable, 0 registered outside the pinned model.
|
||||
|
||||
@@ -30,36 +30,6 @@
|
||||
"tls"
|
||||
]
|
||||
},
|
||||
"bedrock": {
|
||||
"slug": "bedrock",
|
||||
"name": "Bedrock",
|
||||
"title": "Bedrock API Coverage",
|
||||
"seoTitle": "Amazon Bedrock API Coverage on Spinifex — Spinifex Docs",
|
||||
"description": "The Amazon Bedrock control-plane API operations Spinifex implements, covering foundation models, guardrails, provisioned throughput and invocation logging.",
|
||||
"tags": [
|
||||
"aws",
|
||||
"compatibility",
|
||||
"coverage",
|
||||
"bedrock",
|
||||
"ai",
|
||||
"models"
|
||||
]
|
||||
},
|
||||
"bedrockruntime": {
|
||||
"slug": "bedrock-runtime",
|
||||
"name": "Bedrock Runtime",
|
||||
"title": "Bedrock Runtime API Coverage",
|
||||
"seoTitle": "Amazon Bedrock Runtime API Coverage — Spinifex Docs",
|
||||
"description": "The Amazon Bedrock Runtime API operations Spinifex implements, covering model invocation, the Converse API, streamed responses and standalone guardrail checks.",
|
||||
"tags": [
|
||||
"aws",
|
||||
"compatibility",
|
||||
"coverage",
|
||||
"bedrock",
|
||||
"ai",
|
||||
"inference"
|
||||
]
|
||||
},
|
||||
"ec2": {
|
||||
"slug": "ec2",
|
||||
"name": "EC2",
|
||||
|
||||
@@ -286,6 +286,7 @@ Useful background for host-side troubleshooting:
|
||||
- The IMDS HTTP server runs inside the `spinifex-vpcd` service on each host, listening on `169.254.169.254:80` with one listener per instance's primary interface — attribution is structural, with no source-IP trust.
|
||||
- A freshly launched guest's metadata service comes up within a few seconds of launch; cloud-init's built-in retries absorb this window.
|
||||
- Session tokens and cached role credentials are not persisted. A vpcd restart drops them; SDKs and cloud-init transparently reissue. Datapath state on the host survives service restarts.
|
||||
- An Open vSwitch restart, upgrade or crash drops every `br-imds` flow, which takes the guest's whole network down, not only IMDS. `spinifex-daemon` checks each running guest's datapath every 10 seconds and re-installs what was lost.
|
||||
|
||||
## Limits and Defaults
|
||||
|
||||
@@ -362,6 +363,15 @@ ovs-vsctl list-ports br-imds # per-instance endpoint + patch ports
|
||||
ovs-vsctl list-ports br-int | grep imi-
|
||||
```
|
||||
|
||||
3. If the guest has lost all networking, not just IMDS, check whether `br-imds` still has its ports but no flows:
|
||||
|
||||
```bash
|
||||
sudo ovs-ofctl dump-flows br-imds | grep -c cookie= # 7 per running instance
|
||||
journalctl -u spinifex-daemon | grep 'IMDS: per-tap datapath'
|
||||
```
|
||||
|
||||
That is the state an Open vSwitch restart leaves behind. The daemon repairs it within 10 seconds and logs `IMDS: per-tap datapath was lost; re-installed`. A count that stays at 0 with `IMDS: per-tap datapath repair failed` in the log names the step that failed.
|
||||
|
||||
### cloud-init Did Not Apply SSH Key or User Data
|
||||
|
||||
cloud-init sources both from IMDS at first boot. Check `public-keys/0/openssh-key` and `user-data` respond from inside the guest (see [Using IMDS from a Guest](#using-imds-from-a-guest)), and review `/var/log/cloud-init.log` in the guest. A 404 on `public-keys/` means the instance was launched without `--key-name` or the key pair was since deleted.
|
||||
|
||||
+200
-110
@@ -31,12 +31,13 @@ resources:
|
||||
- [1. Clone the Spinifex repository](#1-clone-the-spinifex-repository)
|
||||
- [2. OCI Credentials for Spinifex](#2-oci-credentials-for-spinifex)
|
||||
- [2.1 Instance principal (preferred)](#21-instance-principal-preferred)
|
||||
- [2.2 OCI IAM user with scoped permissions](#22-oci-iam-user-with-scoped-permissions)
|
||||
- [2.2 Advanced: OCI IAM user with scoped permissions](#22-advanced-oci-iam-user-with-scoped-permissions)
|
||||
- [3. Choose deployment method](#3-choose-deployment-method)
|
||||
- [4. Write your Terraform inputs](#4-write-your-terraform-inputs)
|
||||
- [5. Deploy](#5-deploy)
|
||||
- [6. Check it worked](#6-check-it-worked)
|
||||
- [7. Set up your cluster](#7-set-up-your-cluster)
|
||||
- [7. Obtain AWS credentials](#7-obtain-aws-credentials)
|
||||
- [8. Set up your cluster](#8-set-up-your-cluster)
|
||||
- [Harden it before production](#harden-it-before-production)
|
||||
- [Next steps](#next-steps)
|
||||
- [Removing the deployment](#removing-the-deployment)
|
||||
@@ -68,7 +69,7 @@ This guide installs Spinifex on Oracle Cloud Infrastructure, on one instance or
|
||||
|
||||
Nodes run **Ubuntu 26.04**, resolved at plan time as the newest Canonical platform image for your shape and region. Oracle Linux support as the host OS to run Spinifex is under active development.
|
||||
|
||||
**Terraform does the whole deployment:** the VCN, the block volumes, both VNICs per node, the Spinifex install, the cluster formation and the allocator. Six steps below, one command, about ten minutes for a single node.
|
||||
**Terraform creates the OCI infrastructure:** the VCN, block volumes and both VNICs per node. The `validate-topology.sh` driver then waits for cloud-init, installs Spinifex, forms the cluster, configures the allocator and verifies it. That separation matters when a run needs to resume after a host-side failure.
|
||||
|
||||
What sits underneath is in [Architecture and Operations](../oci-architecture/README.md), which also carries the IAM policy, the quotas, every variable and the troubleshooting.
|
||||
|
||||
@@ -83,6 +84,8 @@ What sits underneath is in [Architecture and Operations](../oci-architecture/REA
|
||||
|
||||
Check two quotas before you start, because both refuse at apply time rather than at plan time. **Reserved public IPs** are capped at 50 per region across the whole tenancy, and the **compute limit for your shape** starts at zero for bare metal on many new tenancies. [Quotas](../oci-architecture/README.md#quotas) has the commands.
|
||||
|
||||
An instance-principal deployment also needs capacity for **one additional OCI Dynamic Group**. Dynamic Groups are tenancy-wide Identity objects; the first-run command creates one named `<deployment_name>-nodes` and its policy.
|
||||
|
||||
For the workstation (Linux, MacOS, Windows [WSL]) that will be used to deploy Spinifex on OCI the following prerequisites are required.
|
||||
|
||||
### OCI CLI tool
|
||||
@@ -99,7 +102,7 @@ That writes a `[DEFAULT]` profile, which every command below reads with no flag.
|
||||
export OCI_CLI_PROFILE=my-profile
|
||||
```
|
||||
|
||||
Spinifex's Terraform helper reads the same variable, so this one export covers the CLI commands here and the deployment in step 5.
|
||||
The driver also accepts explicit `--oci-profile` and `--identity-oci-profile` arguments. Prefer those in the commands below when using named profiles: they make the infrastructure profile and tenancy-admin identity profile unambiguous.
|
||||
|
||||
### OCI Compartment
|
||||
|
||||
@@ -194,7 +197,7 @@ There are two ways to provide OCI API access to Spinifex, and they differ in who
|
||||
| **Key material** | None. Each node authenticates as itself | A private key on every node, at `/etc/spinifex/oci/` |
|
||||
| **Who can set up** | A tenancy admin, once per tenancy | Anyone with identity rights in the tenancy |
|
||||
| **Rotation** | Nothing to rotate | Yours to rotate and redistribute |
|
||||
| **What you set** | `export SPX_PRINCIPAL=adopt`, read by step 4 | Nothing extra; step 5 installs the key for you |
|
||||
| **What you set** | Pass `--instance-principal`; add `--setup-instance-principal` on the first deployment | Nothing extra; step 5 installs the key for you |
|
||||
|
||||
**Use 2.1 if you are a tenancy admin.** It is the better of the two because there is no key to leak, rotate or forget about, and a replaced node needs no handoff. Use 2.2 when your compartment was allocated to you inside someone else's tenancy, which is the common case and the reason 2.2 exists at all.
|
||||
|
||||
@@ -204,32 +207,22 @@ Both routes below need `$CID` from the prerequisites step, so confirm it is stil
|
||||
echo "${CID:?set it as shown under OCI CLI tool}"
|
||||
```
|
||||
|
||||
Do one of the two authentication methods below, not both, then continue to step 3.
|
||||
Choose one authentication method below. The deployment commands in step 5 make the choice explicit.
|
||||
|
||||
### 2.1 Instance principal (preferred)
|
||||
|
||||
Each node authenticates with the certificate its own metadata service serves at `/opc/v2/identity/cert.pem`, so no key is written anywhere. Authorising that takes a dynamic group and a policy, and **both are tenancy-root resources** — a compartment-scoped user cannot create them, and the attempt fails with `404-NotAuthorizedOrNotFound` on `CreateDynamicGroup`, which reads like a misconfiguration and is not one.
|
||||
Each node authenticates with the certificate its own metadata service serves at `/opc/v2/identity/cert.pem`, so no key is written to a host. Authorising that needs a Dynamic Group and policy, both of which are tenancy-root resources.
|
||||
|
||||
Create them once, with a tenancy-admin OCI profile, then set one variable:
|
||||
The deployment command in [step 5](#5-deploy) creates these resources on a first run when given `--setup-instance-principal`. It needs a tenancy-admin profile for that one action; later deployments use the existing group with `--instance-principal` only.
|
||||
|
||||
```bash
|
||||
./setup-identity.sh --dry-run # always first
|
||||
./setup-identity.sh
|
||||
The Dynamic Group matches the target compartment, so it covers replacement nodes automatically. Its independent Terraform state is stored in `.identity/`; retain that directory until the Dynamic Group and policy are intentionally removed.
|
||||
|
||||
export SPX_PRINCIPAL=adopt
|
||||
```
|
||||
> [!WARNING]
|
||||
> A tenancy at its Dynamic Group object limit rejects the first-run command with `DynamicResourceGroups` quota errors. Free capacity or request a quota increase before retrying; do not delete an unfamiliar Dynamic Group.
|
||||
|
||||
**That is all of it.** Step 4 writes `SPX_PRINCIPAL` into your Terraform inputs and step 5 reads it back from there, so neither step needs anything extra from you. Keep the same shell for both, as with `$CID`.
|
||||
### 2.2 Advanced: OCI IAM user with scoped permissions
|
||||
|
||||
The dynamic group matches on the compartment, so it covers every node you build there and needs no update when a node is replaced. Its Terraform state goes in `.identity/`, separate from the deployment's — keep that directory, as it is the only record of what was created.
|
||||
|
||||
Only `setup-identity.sh` needs tenancy-admin rights. Every deployment afterwards runs as your ordinary compartment user.
|
||||
|
||||
To remove the group and policy again, `./setup-identity.sh --destroy`. Every node configured for `adopt` loses its credential the moment you do.
|
||||
|
||||
### 2.2 OCI IAM user with scoped permissions
|
||||
|
||||
This route creates a dedicated OCI user holding only the three permissions Spinifex needs, and installs its key on each node. It needs identity rights in the tenancy but no tenancy-root rights, so it works in an allocated compartment.
|
||||
This route creates a dedicated OCI user holding only the four permissions Spinifex needs, and installs its key on each node. It needs identity rights in the tenancy but no tenancy-root rights, so it works in an allocated compartment.
|
||||
|
||||
Skip this section if you have enabled the instance principal authentication path.
|
||||
|
||||
@@ -391,7 +384,7 @@ Choose how to deploy Spinifex on OCI, either as a single node, or at minimum a t
|
||||
|
||||
## 4. Write your Terraform inputs
|
||||
|
||||
The next step is to define a `terraform.auto.tfvars` configuration file with your desired deployment method for Spinifex on OCI.
|
||||
The driver supplies safe defaults for each topology. For a first deployment, provide only the target compartment; add the optional overrides after the cluster works.
|
||||
|
||||
Recommended compute shapes for a VM deployment:
|
||||
|
||||
@@ -406,28 +399,38 @@ Recommended bare metal shapes, preferred for large-scale production workloads:
|
||||
|
||||
```bash
|
||||
cat > terraform.auto.tfvars <<EOF
|
||||
compartment_ocid = "$CID"
|
||||
deployment_name = "spinifex"
|
||||
# Existing OCI compartment where every POC resource is created. Required.
|
||||
compartment_ocid = "$CID"
|
||||
|
||||
# How the nodes authenticate to OCI, from step 2. "off" is an API key, which is
|
||||
# what step 2.2 installs; "adopt" is the instance principal from step 2.1.
|
||||
instance_principal = "${SPX_PRINCIPAL:-off}"
|
||||
# Number of Spinifex nodes. Keep this aligned with --topology: 1 for vm-single
|
||||
# or bm, and 3 for vm-multi. Omit it to use the topology's default.
|
||||
node_count = 3
|
||||
|
||||
# Shape and size.
|
||||
# Bare metal is preferred for large scale production usage.
|
||||
compute_shape = "VM.Standard.E5.Flex"
|
||||
compute_ocpus = 8 # OCI counts an OCPU as a full core, 1 OCPU = 2vCPU
|
||||
compute_memory_in_gbs = 32
|
||||
# OCI Flex VM shape. Use a fixed BM.* shape for a bare-metal deployment.
|
||||
compute_shape = "VM.Standard.E6.Flex"
|
||||
|
||||
# 1 for a single node, 3 required for a cluster.
|
||||
node_count = 1
|
||||
# CPU allocation per Flex VM, measured in OCI OCPUs (one OCPU is two vCPUs).
|
||||
# Ignored by fixed-shape bare-metal instances.
|
||||
compute_ocpus = 8
|
||||
|
||||
# The block volume behind /var/lib/spinifex, attached over iSCSI.
|
||||
data_volume_size_in_gbs = 256
|
||||
data_volume_vpus_per_gb = 120 # 120 = Ultra High Performance
|
||||
# Memory allocation per Flex VM in GiB. Ignored by fixed-shape bare-metal instances.
|
||||
compute_memory_in_gbs = 32
|
||||
|
||||
# Firewall. A list, so it can hold several ranges. Read the note below before
|
||||
# narrowing it.
|
||||
# Persistent block-volume capacity, in GiB, mounted as /var/lib/spinifex on each node.
|
||||
data_volume_size_in_gbs = 1024
|
||||
|
||||
# Block-volume performance setting, in VPUs per GiB. Higher values cost more and
|
||||
# provide more IOPS. Consult OCI volume-performance limits before increasing it.
|
||||
data_volume_vpus_per_gb = 60
|
||||
|
||||
# OCI authentication mode used by the Spinifex public-IP allocator. "adopt" means
|
||||
# the Dynamic Group and policy already exist. The driver's --instance-principal
|
||||
# flag enforces this mode; do not set this to "create" in a normal deployment state.
|
||||
instance_principal = "adopt"
|
||||
|
||||
# OCI security-list sources allowed to reach the public subnet. These rules also
|
||||
# affect guests with public OCI addresses, so keep 0.0.0.0/0 for a public POC and
|
||||
# use the Spinifex host firewall to restrict node SSH/console access instead.
|
||||
node_client_cidr_allow_list = ["0.0.0.0/0"]
|
||||
EOF
|
||||
```
|
||||
@@ -438,7 +441,7 @@ The heredoc delimiter is unquoted, which is what lets `$CID` expand from the var
|
||||
grep compartment_ocid terraform.auto.tfvars
|
||||
```
|
||||
|
||||
**This file is what decides the deployment.** `compute_ocpus` and `compute_memory_in_gbs` size a VM flex shape and are ignored on a fixed bare-metal one, which comes at one fixed size with all available OCPU/memory.
|
||||
`--topology vm-single`, `vm-multi`, and `bm` select the default node count and compute shape. Values in `terraform.auto.tfvars` intentionally override those defaults. `compute_ocpus` and `compute_memory_in_gbs` size a VM flex shape and are ignored on a fixed bare-metal one.
|
||||
|
||||
[Every Terraform variable](../oci-architecture/README.md#every-terraform-variable) has the full list.
|
||||
|
||||
@@ -447,35 +450,100 @@ grep compartment_ocid terraform.auto.tfvars
|
||||
|
||||
## 5. Deploy
|
||||
|
||||
Choose the deployment method below:
|
||||
Choose `vm-single` for an evaluation, `vm-multi` for the recommended three-node cluster, or `bm` for a bare-metal host.
|
||||
|
||||
- `vm-single` - Single node instance (best for testing/development)
|
||||
- `vm-multi` - A recommended 3-node cluster (production)
|
||||
- `bm` - Bare metal for larger scale production use.
|
||||
### First instance-principal deployment
|
||||
|
||||
```bash
|
||||
export TOPOLOGY=vm-single
|
||||
```
|
||||
|
||||
Next deploy Spinifex on OCI:
|
||||
This command creates the tenancy-wide Dynamic Group/policy, provisions the OCI infrastructure, installs Spinifex, forms the cluster, configures OCI public addressing and verifies the allocator:
|
||||
|
||||
```bash
|
||||
./validate-topology.sh \
|
||||
--topology "$TOPOLOGY" \
|
||||
--credential-hook ./spx-oci-config.sh \
|
||||
--topology vm-multi \
|
||||
--instance-principal \
|
||||
--setup-instance-principal \
|
||||
--oci-profile <compartment-deployer-profile> \
|
||||
--identity-oci-profile <tenancy-admin-profile> \
|
||||
--ssh-public-key ~/.ssh/oci-spx.pub \
|
||||
--ssh-private-key ~/.ssh/oci-spx \
|
||||
--skip-workload \
|
||||
--keep
|
||||
```
|
||||
|
||||
The same command covers both credential routes. It reads `instance_principal` from the tfvars you wrote in step 4, so an instance-principal deployment needs no extra argument and `--credential-hook` is simply ignored.
|
||||
If one profile has both tenancy and compartment permissions, use its name for both profile arguments. `--keep` leaves the POC running when validation completes.
|
||||
|
||||
One command builds the infrastructure, installs Spinifex, forms the cluster, configures OCI public addressing and verifies that the address allocator came up.
|
||||
### Later instance-principal deployments
|
||||
|
||||
After the Dynamic Group and policy exist, omit `--setup-instance-principal`:
|
||||
|
||||
```bash
|
||||
./validate-topology.sh \
|
||||
--topology vm-multi \
|
||||
--instance-principal \
|
||||
--oci-profile <compartment-deployer-profile> \
|
||||
--ssh-public-key ~/.ssh/oci-spx.pub \
|
||||
--ssh-private-key ~/.ssh/oci-spx \
|
||||
--skip-workload \
|
||||
--keep
|
||||
```
|
||||
|
||||
### Advanced: OCI API-key deployment
|
||||
|
||||
For the API-key path from [section 2.2](#22-advanced-oci-iam-user-with-scoped-permissions), use the credential hook instead of `--instance-principal`:
|
||||
|
||||
```bash
|
||||
./validate-topology.sh \
|
||||
--topology vm-multi \
|
||||
--oci-profile <compartment-deployer-profile> \
|
||||
--credential-hook ./spx-oci-config.sh \
|
||||
--ssh-public-key ~/.ssh/oci-spx.pub \
|
||||
--ssh-private-key ~/.ssh/oci-spx \
|
||||
--skip-workload \
|
||||
--keep
|
||||
```
|
||||
|
||||
### Install Spinifex on existing OCI infrastructure
|
||||
|
||||
Use this path when your team has already applied the [Terraform-only OCI configuration](../../scripts/terraform/oci-spx/README.md) and wants the validation driver to install and verify Spinifex without creating, changing, or destroying the OCI infrastructure.
|
||||
|
||||
The Terraform apply must have produced its normal `hosts_file`, `compute_shape`, and `instance_principal` outputs. Its default local state file is `scripts/terraform/oci-spx/terraform.tfstate`.
|
||||
|
||||
For existing infrastructure that uses an already-created instance principal (`instance_principal = "adopt"` in the Terraform inputs), run:
|
||||
|
||||
```bash
|
||||
./validate-topology.sh \
|
||||
--topology vm-multi \
|
||||
--existing-state-file "$PWD/terraform.tfstate" \
|
||||
--instance-principal \
|
||||
--oci-profile <compartment-deployer-profile> \
|
||||
--ssh-public-key ~/.ssh/oci-spx.pub \
|
||||
--ssh-private-key ~/.ssh/oci-spx \
|
||||
--skip-workload
|
||||
```
|
||||
|
||||
If the Dynamic Group and policy do not exist yet, add `--setup-instance-principal` and `--identity-oci-profile <tenancy-admin-profile>` to that command. The driver creates only those identity resources; it still leaves the existing OCI infrastructure untouched.
|
||||
|
||||
For existing infrastructure using the API-key path (`instance_principal = "off"`), use the credential hook instead:
|
||||
|
||||
```bash
|
||||
./validate-topology.sh \
|
||||
--topology vm-multi \
|
||||
--existing-state-file "$PWD/terraform.tfstate" \
|
||||
--oci-profile <compartment-deployer-profile> \
|
||||
--credential-hook ./spx-oci-config.sh \
|
||||
--ssh-public-key ~/.ssh/oci-spx.pub \
|
||||
--ssh-private-key ~/.ssh/oci-spx \
|
||||
--skip-workload
|
||||
```
|
||||
|
||||
`--existing-state-file` puts the driver in existing-infrastructure mode: Terraform apply and destroy are skipped, and the hosts are always kept. Pass `--existing-hosts-file /path/to/hosts` instead when Terraform state is unavailable; the file must contain one public SSH IP address per line. With a hosts file, explicitly pass either `--instance-principal` or `--credential-hook` because the driver cannot infer the credential mode.
|
||||
|
||||
| Argument | What to pass |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--topology` | `vm-single` for one VM, `vm-multi` for three, `bm` for one bare-metal host. Required, with no default, so a command cannot be aimed at the wrong one by omission. It supplies a shape and a node count only where your tfvars is silent |
|
||||
| `--topology` | `vm-single` for one VM, `vm-multi` for three, `bm` for one bare-metal host. Required, with no default, so a command cannot be aimed at the wrong one by omission. It supplies a shape and node count only where your tfvars is silent |
|
||||
| `--instance-principal` | Authenticate Spinifex nodes through their OCI instance identity. This is the recommended route. |
|
||||
| `--setup-instance-principal` | First deployment only: create the tenancy-wide Dynamic Group and policy. Needs `--identity-oci-profile` when the tenancy admin uses a different profile. |
|
||||
| `--channel` | Nothing, normally. It defaults to `latest`, the published release. Pass `dev` to install the newest pre-release instead, or `--version <tag>` to pin an exact build |
|
||||
| `--credential-hook` | `./spx-oci-config.sh`, which installs the step 2.2 credential on every node. Not used when your tfvars set `instance_principal`, because there is then no key to install |
|
||||
| `--credential-hook` | `./spx-oci-config.sh`, which installs the step 2.2 credential on every node. Do not pass it with `--instance-principal`, because there is then no key to install. |
|
||||
| `--skip-workload` | Stop once the cluster is verified. Drop it and the driver also runs the published Terraform workbooks on the node, which launches real guests on public addresses and tears each one down again |
|
||||
| `--keep` | **This is what makes it a deployment rather than a test.** Without it the driver destroys everything at the end, which is right for CI and testing a deployment |
|
||||
|
||||
@@ -488,27 +556,14 @@ Add `--dry-run` to print the plan and change nothing.
|
||||
|
||||
## 6. Check it worked
|
||||
|
||||
The stages print as they finish, and all of these have to appear:
|
||||
For an instance-principal deployment, the decisive lines on **each node** are:
|
||||
|
||||
```text
|
||||
[validate-vm-single] building
|
||||
[validate-vm-single] VM.Standard.E5.Flex, 1 node(s), oci_auth from a key file
|
||||
[validate-vm-single] waiting for cloud-init on each node
|
||||
[validate-vm-single] installing Spinifex on each node
|
||||
[validate-vm-single] initializing the single node
|
||||
[validate-vm-single] running the credential hook
|
||||
[validate-vm-single] credential hook ok
|
||||
[validate-vm-single] configuring the IMDS remap and the external pool
|
||||
[validate-vm-single] checking the allocator came up
|
||||
[validate-vm-single] 150.230.13.131 allocator ready
|
||||
[validate-vm-single] cluster membership
|
||||
[validate-vm-single] --skip-workload: stopping before the workbook
|
||||
[validate-vm-single] --keep: leaving the infrastructure up, no verdict recorded
|
||||
[validate-vm-single] ssh in with: ssh -i ~/.ssh/oci-spx ubuntu@public.ip
|
||||
[validate-vm-single] destroy it with: ./validate-topology.sh --topology vm-single --destroy-only
|
||||
ocinet resolved the external VNIC from its interface
|
||||
OCI allocator ready
|
||||
```
|
||||
|
||||
**`allocator ready` is the line to look for.** Every line above it also prints on a node that cannot allocate a public address, and that failure stays silent until a guest asks for one.
|
||||
`OCI allocator ready` is the line to look for. It proves that the node resolved its OCI VNIC and authenticated to OCI, which is what allows a guest to receive a public address.
|
||||
|
||||
`running the credential hook` is `spx-oci-config.sh`, and its own output is captured rather than printed. If the deploy stops there, read `credential-hook.log` in the state directory the driver names at startup — its `credential:` line says which credential went onto the nodes. On the instance-principal route those two lines are absent, because there is no handoff to make.
|
||||
|
||||
@@ -517,51 +572,66 @@ The stages print as they finish, and all of these have to appear:
|
||||
The deploy's last line is the command to get in, so you can paste it straight back. The addresses are also on disk, one per line, which is what to read from a script:
|
||||
|
||||
```bash
|
||||
cat ".validate-$TOPOLOGY/hosts"
|
||||
cat .validate-vm-multi/hosts
|
||||
|
||||
ssh -i ~/.ssh/oci-spx ubuntu@"$(head -1 ".validate-$TOPOLOGY/hosts")"
|
||||
ssh -i ~/.ssh/oci-spx ubuntu@"$(head -1 .validate-vm-multi/hosts)"
|
||||
```
|
||||
|
||||
`$TOPOLOGY` is the variable you exported in step 5, so this is the same command whether you deployed one VM, three, or bare metal. The first line is the node the cluster formed on.
|
||||
|
||||
Then call the AWS surface with the `[spinifex]` profile that `spx admin init` wrote into `~/.aws/credentials`:
|
||||
Replace `vm-multi` with the topology you deployed. The first address is the node the cluster formed on. For a three-node cluster, verify membership directly:
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
aws ec2 describe-instance-types
|
||||
ssh -i ~/.ssh/oci-spx ubuntu@"$(head -1 .validate-vm-multi/hosts)" \
|
||||
'sudo spx get nodes'
|
||||
```
|
||||
|
||||
```text
|
||||
{
|
||||
"InstanceTypes": [
|
||||
{
|
||||
"InstanceType": "c7a.12xlarge",
|
||||
"CurrentGeneration": true,
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
Expect three nodes with `Ready` status.
|
||||
|
||||
## 7. Obtain AWS credentials
|
||||
|
||||
The driver can display the AWS access key and secret generated on node 1. Run this **locally and interactively** after a kept instance-principal deployment:
|
||||
|
||||
```bash
|
||||
./validate-topology.sh \
|
||||
--topology vm-multi \
|
||||
--instance-principal \
|
||||
--oci-profile <compartment-deployer-profile> \
|
||||
--ssh-public-key ~/.ssh/oci-spx.pub \
|
||||
--ssh-private-key ~/.ssh/oci-spx \
|
||||
--skip-workload \
|
||||
--keep \
|
||||
--show-aws-credentials
|
||||
```
|
||||
|
||||
This safely resumes the deployment workflow: it checks the existing OCI infrastructure and cluster, then prints node 1's `~/.aws/credentials` to your terminal after all nodes are Ready. Do **not** include `--setup-instance-principal` here; the Dynamic Group/policy already exists.
|
||||
|
||||
> [!WARNING]
|
||||
> `--show-aws-credentials` reveals an AWS secret. Do not use it in CI, redirect its output, or paste the credentials into chat, tickets, source control, or logs.
|
||||
|
||||
The output has this shape:
|
||||
|
||||
```ini
|
||||
[spinifex]
|
||||
aws_access_key_id = <access-key>
|
||||
aws_secret_access_key = <secret-key>
|
||||
```
|
||||
|
||||
Use the displayed values to sign into the console at:
|
||||
|
||||
```bash
|
||||
echo "https://$(head -1 .validate-vm-multi/hosts):3000/"
|
||||
```
|
||||
|
||||
Replace `vm-multi` if you deployed another topology. The console uses the same access key and secret shown by the deployment command.
|
||||
|
||||
To call the AWS surface from node 1, its generated `[spinifex]` AWS profile is already available:
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/oci-spx ubuntu@"$(head -1 .validate-vm-multi/hosts)" \
|
||||
'export AWS_PROFILE=spinifex; aws ec2 describe-instance-types'
|
||||
```
|
||||
|
||||
If this returns a list of available instance types, your installation is working.
|
||||
|
||||
### Console access
|
||||
|
||||
Next login to the Spinifex console at:
|
||||
|
||||
```
|
||||
https://YOURIP:3000/
|
||||
```
|
||||
|
||||
Fetch your generated AWS credentials via SSH and login to the console using your new Spinifex account.
|
||||
|
||||
```bash
|
||||
ubuntu@spinifexnode01:~$ cat ~/.aws/credentials
|
||||
[spinifex]
|
||||
aws_access_key_id = XXXX
|
||||
aws_secret_access_key = XXXX
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src="../../.github/assets/spinifex-ui.jpg" alt="Spinifex Web UI" width="900">
|
||||
</p>
|
||||
@@ -569,7 +639,7 @@ aws_secret_access_key = XXXX
|
||||
|
||||
**Congratulations! Spinifex is installed.**
|
||||
|
||||
## 7. Set up your cluster
|
||||
## 8. Set up your cluster
|
||||
|
||||
Spinifex is running, but it holds nothing yet — no machine images, no networks, no instances.
|
||||
|
||||
@@ -644,11 +714,31 @@ The four are `oracle-10.1-x86_64`, `oracle-10.1-arm64`, `oracle-9.8-x86_64` and
|
||||
## Removing the deployment
|
||||
|
||||
```bash
|
||||
./validate-topology.sh --topology vm-single --destroy-only
|
||||
./validate-topology.sh \
|
||||
--topology vm-multi \
|
||||
--oci-profile <compartment-deployer-profile> \
|
||||
--ssh-public-key ~/.ssh/oci-spx.pub \
|
||||
--ssh-private-key ~/.ssh/oci-spx \
|
||||
--destroy-only
|
||||
```
|
||||
|
||||
It destroys whatever that topology's Terraform state holds, so it can only remove what this configuration created.
|
||||
|
||||
If this was the only Spinifex cluster using the instance-principal Dynamic Group and policy, remove those tenancy-wide resources as well:
|
||||
|
||||
```bash
|
||||
./validate-topology.sh \
|
||||
--topology vm-multi \
|
||||
--oci-profile <compartment-deployer-profile> \
|
||||
--identity-oci-profile <tenancy-admin-profile> \
|
||||
--ssh-public-key ~/.ssh/oci-spx.pub \
|
||||
--ssh-private-key ~/.ssh/oci-spx \
|
||||
--destroy-only \
|
||||
--destroy-identity
|
||||
```
|
||||
|
||||
Do not use `--destroy-identity` if another Spinifex cluster uses the same Dynamic Group.
|
||||
|
||||
### Check for leftover public IPs afterwards
|
||||
|
||||
**Terraform does not own the addresses Spinifex allocates, so a destroy can leave them behind.**
|
||||
|
||||
@@ -31,6 +31,7 @@ services:
|
||||
- "ec2.*"
|
||||
- "elbv2.*"
|
||||
- "acm.*"
|
||||
- "route53.*"
|
||||
- "iam.*"
|
||||
- "rds.*"
|
||||
- "ecs.*"
|
||||
@@ -39,6 +40,7 @@ services:
|
||||
- "ec2.*"
|
||||
- "elbv2.*"
|
||||
- "acm.*"
|
||||
- "route53.*"
|
||||
- "iam.*"
|
||||
- "rds.*"
|
||||
- "ecs.*"
|
||||
@@ -50,13 +52,13 @@ services:
|
||||
# Translates VPC / EBS / S3 operations into vpc.* / ebs.* / s3.* and
|
||||
# forwards to vpcd / viperblockd / predastore.
|
||||
#
|
||||
# Source layout: spinifex/runtime/roles/spinifex (entry), spinifex/daemon
|
||||
# Source layout: spinifex/runtime/roles/spinifex (entry), spinifex/runtime/daemon
|
||||
# (subscribe registration), spinifex/domains/<domain> and the legacy
|
||||
# spinifex/handlers/{ecs,eks,elbv2,iam,rds} packages (per-subject handler
|
||||
# bodies). Selector treats these paths as belonging to this service via
|
||||
# the `additional_paths` field.
|
||||
additional_paths:
|
||||
- spinifex/daemon
|
||||
- spinifex/runtime/daemon
|
||||
- spinifex/domains/ec2/account
|
||||
- spinifex/domains/ec2/eigw
|
||||
- spinifex/domains/ec2/eip
|
||||
@@ -74,16 +76,17 @@ services:
|
||||
- spinifex/domains/ec2/volume
|
||||
- spinifex/domains/ec2/volumestate
|
||||
- spinifex/domains/ec2/vpc
|
||||
- spinifex/handlers/elbv2
|
||||
- spinifex/domains/elbv2
|
||||
- spinifex/domains/acm
|
||||
- spinifex/handlers/iam
|
||||
- spinifex/handlers/ecs
|
||||
- spinifex/handlers/rds
|
||||
- spinifex/domains/route53
|
||||
- spinifex/domains/iam
|
||||
- spinifex/domains/ecs
|
||||
- spinifex/domains/rds/instance
|
||||
- spinifex/domains/rds/subnetgroup
|
||||
- spinifex/domains/rds/parametergroup
|
||||
- spinifex/domains/rds/engine
|
||||
- spinifex/domains/ecs/taskdefinition
|
||||
- spinifex/handlers/eks
|
||||
- spinifex/domains/eks/cluster
|
||||
- spinifex/domains/eks/access
|
||||
- spinifex/domains/eks/addon
|
||||
# Guest add-on wire contract (subject, report, staged-manifest body).
|
||||
@@ -136,7 +139,7 @@ services:
|
||||
- "spinifex.cluster.shutdown.infra"
|
||||
# Operator-plane Predastore topology contract (contracts/operator/v1).
|
||||
- "spinifex.storage.config"
|
||||
# ELBv2 control plane (handlers/elbv2/service_nats.go topic map).
|
||||
# ELBv2 control plane (domains/elbv2/service_nats.go topic map).
|
||||
- "elbv2.CreateLoadBalancer"
|
||||
- "elbv2.DeleteLoadBalancer"
|
||||
- "elbv2.DescribeLoadBalancers"
|
||||
@@ -154,16 +157,20 @@ services:
|
||||
- "acm.DescribeCertificate"
|
||||
- "acm.ListCertificates"
|
||||
- "acm.DeleteCertificate"
|
||||
# Route 53 hosted zones (domains/route53; subscribed in daemon.go).
|
||||
- "route53.*"
|
||||
# Operator-only zone activation and delegation, sent by spx admin dns.
|
||||
- "route53.admin.*"
|
||||
- "elbv2.AddListenerCertificates"
|
||||
- "elbv2.RemoveListenerCertificates"
|
||||
- "elbv2.DescribeListenerCertificates"
|
||||
- "elbv2.DescribeSSLPolicies"
|
||||
# RDS control plane (handlers/rds/subjects.go). One wildcard rather than
|
||||
# RDS control plane (domains/rds/instance/subjects.go). One wildcard rather than
|
||||
# 30 literals: every action subject is registered from a constant, so a
|
||||
# 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.*"
|
||||
@@ -220,7 +227,7 @@ services:
|
||||
- "vpc.gate-subnet-egress"
|
||||
- "vpc.ungate-subnet-egress"
|
||||
# Egress-only reroute removal for a single system instance
|
||||
# (handlers/eks). Fire-and-forget. vpc.add-system-egress has no
|
||||
# (domains/eks/cluster). Fire-and-forget. vpc.add-system-egress has no
|
||||
# current producer.
|
||||
- "vpc.delete-system-egress"
|
||||
# → viperblockd
|
||||
@@ -403,7 +410,7 @@ suites:
|
||||
e2e-lb:
|
||||
path: tests/e2e/lb
|
||||
covers: [awsgw, spinifex_daemon, vpcd, viperblockd, predastore]
|
||||
# Exercises full ELBv2 control plane (handlers/elbv2) + data plane
|
||||
# Exercises full ELBv2 control plane (domains/elbv2) + data plane
|
||||
# (LB agent on the system VM). ELBv2 is part of spinifex_daemon in
|
||||
# this manifest; revisit if it splits into a standalone service.
|
||||
|
||||
@@ -436,7 +443,7 @@ suites:
|
||||
path: tests/e2e/bedrock
|
||||
covers: [awsgw]
|
||||
# Ochre (AWS-Bedrock) control plane: static catalog list/get served
|
||||
# straight from awsgw (gateway/bedrock.go, gateway/bedrock/) — no NATS
|
||||
# straight from awsgw (gateway/bedrock.go, domains/ochre/) — no NATS
|
||||
# hop to spinifex_daemon. Converse (Anthropic-direct / self-host vLLM)
|
||||
# subtests are gated on backend availability and skip by default.
|
||||
|
||||
@@ -466,6 +473,12 @@ suites:
|
||||
# Exercises the full daemon stack under network failure injection;
|
||||
# any service touching the wire path is in scope.
|
||||
|
||||
e2e-route53:
|
||||
path: tests/e2e/route53
|
||||
covers: [awsgw, spinifex_daemon, predastore]
|
||||
# Hosted zones end to end: delegation check, publish to the northstar zone
|
||||
# store in predastore, and every node answering before a change is INSYNC.
|
||||
|
||||
# Suites below are bash-only today. Entries activate as the Go ports
|
||||
# land (Bead 2 fixtures unblock new-shape ports for multinode etc.).
|
||||
#
|
||||
|
||||
@@ -10,7 +10,7 @@ COPY go.mod ./
|
||||
COPY main.go ./
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/spinifex-demo .
|
||||
|
||||
FROM gcr.io/distroless/static:nonroot@sha256:e2e927ec666bae08560abb3c55d0659eceabb657f56b6782ab500a9fc7f555e3
|
||||
FROM gcr.io/distroless/static:nonroot@sha256:18f4cc26c9ffa7f20a359298be7deadac5c607c8e03a02b8119f1a5fc2f82c99
|
||||
COPY --from=build /out/spinifex-demo /spinifex-demo
|
||||
EXPOSE 8080
|
||||
USER nonroot:nonroot
|
||||
|
||||
@@ -6,8 +6,8 @@ Suites live in `tests/e2e/<name>/` and build to `tests/e2e/_bin/<name>.test`. Ev
|
||||
|
||||
| Set | Suites |
|
||||
| --- | --- |
|
||||
| `E2E_SUITES_SINGLE` | `single iam cert eks ecs storagegrowth partialblock rds quota storagefault` |
|
||||
| `E2E_SUITES_MULTI` | `multinode lb cert quota kvquorum lbrecovery instancerecovery storagefault` |
|
||||
| `E2E_SUITES_SINGLE` | `single iam cert eks ecs storagegrowth partialblock rds quota route53 storagefault` |
|
||||
| `E2E_SUITES_MULTI` | `multinode lb cert quota route53 kvquorum lbrecovery instancerecovery storagefault` |
|
||||
| `E2E_SUITES_NIGHTLY_SINGLE` | `single cert iam` |
|
||||
| `E2E_SUITES_NIGHTLY_MULTI` | `multinode cert lb` |
|
||||
|
||||
@@ -19,7 +19,7 @@ The nightly permutation sets are deliberately narrower than the full ones: those
|
||||
|
||||
### `single` — the broad single-node EC2/EBS/VPC surface
|
||||
|
||||
The largest suite, 25 top-level tests. Notable entry points:
|
||||
The largest suite, 27 top-level tests. Notable entry points:
|
||||
|
||||
- **`TestVPCEgressPaths`** — **the authority on subnet egress semantics.** One VPC with a public and a private subnet, sharing two guests across four stages: `PublicSubnetEgress` (IGW egress works), `RouteBeforeSubnet` (a regression guard for an IGW route installed on the main route table before the subnet exists), the NAT Gateway rounds, and `EIPFlip` (associating an Elastic IP onto a running instance).
|
||||
- **`NATBaseline` hard-fails if a private-subnet guest reaches the internet with no NAT Gateway.** `NATGatewayUp` then proves egress appears and `NATGatewayDown` that it disappears. `SPINIFEX_VPCEGRESS_NAT_ROUNDS` repeats the up/down cycle for a soak run.
|
||||
@@ -27,6 +27,7 @@ The largest suite, 25 top-level tests. Notable entry points:
|
||||
- `TestSGPolicyDatapath`, `TestSGReachabilityPolicy` — security groups as an actual datapath, not just API state.
|
||||
- `TestWANEgressSMTPBlock` — egress port blocking.
|
||||
- `TestInstanceMetadata` — IMDS from inside a guest.
|
||||
- **`TestIMDSDatapathSurvivesVswitchdRestart`** restarts `ovs-vswitchd` under a guest with an Elastic IP, once through systemd and once with SIGKILL. After each, the guest must answer over its EIP and fetch its instance ID through IMDSv2 within 60 seconds, and `br-imds` must hold all 7 of the guest's own flows. It is sequential because it takes the whole node's datapath down.
|
||||
- `TestStopStart`, `TestEarlyRebootLiveness`, `TestGuestChurnDurability`, `TestSnapshotLifecycle`, `TestSnapshotBackedLaunch`, `TestCreateImage`, `TestVolumeLifecycle`, `TestSpotInstanceLifecycle`, `TestLaunchTemplateBoot`.
|
||||
- `TestNegativeErrorPaths` — the `8a`–`8t` AWS error-shape cases (malformed AMI ID, invalid instance type, volume in use, detach-root forbidden, unknown or malformed instance IDs on the lifecycle verbs, and so on).
|
||||
- **`TestKVReplication`** — the single-node half of the KV replication policy, and it is not the multi-node assertion with a smaller number in it. On one node one replica per bucket is the correct answer rather than the defect, so what it proves is the opposite: that nothing declared a replica count this deployment cannot place, that `spx admin kv replicas` exits 0 and reports `WANT 1`, and that `--repair` run twice against a cluster with nothing to repair changes no bucket's replica count and leaves every bucket serving. The multi-node suites cannot cover any of that, because on three nodes the unplaceable count is the right one. It lives here rather than in its own suite so it runs in every single-node nightly cell, across all three host and network permutations.
|
||||
@@ -47,6 +48,8 @@ Two nightly cells run it: `nat-single` (cell 19) and `nat-multi` (cell 30). Cell
|
||||
|
||||
**`TestMultinodeJetStreamReplicas` is a whole-cluster sweep, not a named list, and it lives here on purpose.** It audits every KV bucket the cluster has and fails any that is replicated across fewer nodes than the cluster has, capped at JetStream's ceiling of five. Naming buckets would only ever cover the ones somebody remembered — a replica count is set where a bucket is created, and there is a bucket-creating call in most services — so the sweep is the assertion and the three daemon-owned buckets are named only so that a run enumerating nothing still fails. Because `multinode` is in `E2E_SUITES_NIGHTLY_MULTI`, this runs in every multi-node permutation cell, and no cell can be added that quietly opts out of it.
|
||||
|
||||
**`TestMultinodeDNS` convergence budgets are env-tunable.** `SPINIFEX_MULTINODE_DNS_TOPOLOGY_TIMEOUT` (default `90s`) bounds every node agreeing on the NS topology, `SPINIFEX_MULTINODE_DNS_TIMEOUT` (default `2m`) bounds a record becoming answerable from a guest or a node, and `SPINIFEX_MULTINODE_DNS_POLL` (default `3s`) is the poll interval for both. Values are Go durations; a malformed or non-positive value panics rather than falling back to the default.
|
||||
|
||||
### `kvquorum` — the cluster's own state survives losing a node (multi-node, needs three)
|
||||
|
||||
**The control plane keeps its state in JetStream KV buckets, and this is the suite that proves those buckets are actually replicated.** A bucket on one replica lives on one JetStream-chosen server recorded in no config, so losing that node makes it return `nats: no responders available for request` from *every* node at once — a cluster-wide outage caused by a single-node event. Leader leases are the worst case: acquiring one is a write, so a lease bucket without a quorum cannot be acquired by anyone, and every reconciler sharing it stops cluster-wide at exactly the moment a node has failed and there is repair work to do.
|
||||
@@ -120,6 +123,7 @@ The create/describe/connect path against a live cluster: a DB VM that actually b
|
||||
### Other
|
||||
|
||||
- **`quota`** — every enforced dimension refuses with the same `quotaExceeded` code; a launch is charged for every instance it asks for; one instance wider than the cap is refused.
|
||||
- **`route53`** — hosted zones as northstar serves them, on every node. `TestNorthstarServesZonesBeyondFirstListPage` writes 1,100 zone objects (more than one `ListObjectsV2` page), requires the last one to resolve on every node and the platform base zone to survive the sync, then requires the deleted zones to stop resolving.
|
||||
- **`gpu`** — device visible in the guest, driver access, launch.
|
||||
- **`bedrock`** — `ListFoundationModels`/`GetFoundationModel` against a running cluster. Wire-format parity is unit-tested, not here.
|
||||
- **`ddil`** — Denied, Disrupted, Intermittent, Limited network scenarios.
|
||||
|
||||
@@ -5,10 +5,11 @@ go 1.27.3
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.55.8
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.3
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.23
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.9
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.9
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.4
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/charmbracelet/bubbles v1.0.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
@@ -23,15 +24,15 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.4.1
|
||||
github.com/miekg/dns v1.1.73
|
||||
github.com/minio/highwayhash v1.0.4
|
||||
github.com/mulgadc/bluebottle v1.22.0
|
||||
github.com/mulgadc/northstar v1.22.0
|
||||
github.com/mulgadc/predastore v1.22.0
|
||||
github.com/mulgadc/viperblock v1.22.0
|
||||
github.com/mulgadc/bluebottle v1.23.0
|
||||
github.com/mulgadc/northstar v1.23.0
|
||||
github.com/mulgadc/predastore v1.23.0
|
||||
github.com/mulgadc/viperblock v1.23.0
|
||||
github.com/nats-io/nats-server/v2 v2.15.1
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
github.com/opencontainers/runtime-spec v1.3.0
|
||||
github.com/oracle/oci-go-sdk/v65 v65.127.0
|
||||
github.com/ovn-kubernetes/libovsdb v0.8.1
|
||||
github.com/oracle/oci-go-sdk/v65 v65.128.0
|
||||
github.com/ovn-kubernetes/libovsdb v0.9.0
|
||||
github.com/pelletier/go-toml/v2 v2.4.3
|
||||
github.com/pterm/pterm v0.12.84
|
||||
github.com/spf13/cobra v1.10.2
|
||||
@@ -67,22 +68,21 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 // indirect
|
||||
github.com/aws/smithy-go v1.28.5 // indirect
|
||||
github.com/aws/smithy-go v1.28.6 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/buraksezer/consistent v1.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cenkalti/hub v1.0.2 // indirect
|
||||
github.com/cenkalti/rpc2 v1.0.5 // indirect
|
||||
github.com/cenkalti/rpc2 v1.0.6 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
@@ -111,12 +111,12 @@ require (
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.5 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/gofrs/flock v0.10.0 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
@@ -139,7 +139,7 @@ require (
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/jstemmer/go-junit-report/v2 v2.1.0 // indirect
|
||||
github.com/klauspost/reedsolomon v1.14.2 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/leodido/go-urn v1.5.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
@@ -170,7 +170,7 @@ require (
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.71.0 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/quic-go/quic-go v0.63.0 // indirect
|
||||
github.com/quic-go/quic-go v0.64.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
@@ -184,14 +184,14 @@ require (
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.etcd.io/bbolt v1.5.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.20.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.71.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.21.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.72.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/log v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.22.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v1.47.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
go.uber.org/nilaway v0.0.0-20260721205819-4a0f7653488a // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 // indirect
|
||||
@@ -200,8 +200,8 @@ require (
|
||||
golang.org/x/telemetry v0.0.0-20261009151841-97e8ea77aa2a // indirect
|
||||
golang.org/x/text v0.43.0 // indirect
|
||||
golang.org/x/vuln v1.6.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
tags.cncf.io/container-device-interface v1.1.1 // indirect
|
||||
|
||||
@@ -24,8 +24,8 @@ github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ
|
||||
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.23 h1:/fGPpKrXTjzkhsDXExBgNT2cGwxiF9rLrQXh8RlXMhI=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.23/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.9 h1:Hv6bcQUJVnmuWaKjqD6Ot8sE75NrWsWV20oGT5aWle0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.9/go.mod h1:ivowmrZNfBQLcg4QkaItgi5wae4Bzot9RzIdbY1hfTg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0=
|
||||
@@ -40,14 +40,14 @@ github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5 h1:/TYsZXdA8UTa+WCtCYSAJIr1vwl0+eho6TUgJGwFFO8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5/go.mod h1:qPqp1Uwd/BqdhPufv6oem9j5J7HNsgc2V22dUiDPn+s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 h1:CwvFlKw0uUScfgOMflXSRZa+ayXgOf6wk7lhyMemQYU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7/go.mod h1:4SbD8BB+p+QEnG0aX6DoWwrtk9SufyYXRsUv0nve8LE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4 h1:pPiWfgeNxqluKEph7hvU88kuGKBPOWzO+Dk9t2zqqNs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4/go.mod h1:YlwGoIUDG/3kBQbdNOVs/xKZ9J01G8e/6D1mRBj9uTk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.0 h1:VMAdYqr4Jn/8ATs9BHC5riwrs0d6m1Z2ohFriSwZwm0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.0/go.mod h1:9APRWGLFITKD+xzWSIyT9V7QV4bNlEuIieWlzXgGFlI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.4 h1:MrkWRB58Qw5TEkb51QVooVLjlM3IevO2iVXEz74B8pg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.4/go.mod h1:yfIxaKRHasvfy6tNEm3/LpqKsupMzQjMaWj9Y0jMVbo=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk=
|
||||
@@ -56,8 +56,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEB
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM=
|
||||
github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k=
|
||||
github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/smithy-go v1.28.6 h1:Pj+/sUviNYjow8ELRtNwKHZ9ECeF+7pATX3+shjLIrw=
|
||||
github.com/aws/smithy-go v1.28.6/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -72,8 +72,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cenkalti/hub v1.0.2 h1:Nqv9TNaA9boeO2wQFW8o87BY3zKthtnzXmWGmJqhAV8=
|
||||
github.com/cenkalti/hub v1.0.2/go.mod h1:8LAFAZcCasb83vfxatMUnZHRoQcffho2ELpHb+kaTJU=
|
||||
github.com/cenkalti/rpc2 v1.0.5 h1:T6l4SS3ja3eaJfRyZrn7Oco/PSx/pr3YK5cjCgLVLTk=
|
||||
github.com/cenkalti/rpc2 v1.0.5/go.mod h1:2yfU5b86vOr16+iY1jN3MvT6Kxc9Nf8j5iZWwUf7iaw=
|
||||
github.com/cenkalti/rpc2 v1.0.6 h1:TvFGIbaXGeHDSZeMyhdGEfRWBBpFFnlAIiAeZoRyVX0=
|
||||
github.com/cenkalti/rpc2 v1.0.6/go.mod h1:2yfU5b86vOr16+iY1jN3MvT6Kxc9Nf8j5iZWwUf7iaw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||
@@ -152,8 +152,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
|
||||
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
|
||||
github.com/go-chi/chi/v5 v5.5.0 h1:xdKQ1dJmVJsmgahrXSKhUx8NyVPzpwdlyrrQFpMPJgg=
|
||||
github.com/go-chi/chi/v5 v5.5.0/go.mod h1:d1IG7GtKPSjPyBP365PH8sAGVHk3qfB/IhjFNRH1wWM=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -167,8 +167,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-playground/validator/v10 v10.30.5 h1:YyCXvVShZbs2Sm3Mb53eNOlhRXctSOzW5QJAouCTZL4=
|
||||
github.com/go-playground/validator/v10 v10.30.5/go.mod h1:wEqiaov48pXX1kjhc3Da8y0M0Dtg/BK7gurFBLgwFrQ=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gofrs/flock v0.10.0 h1:SHMXenfaB03KbroETaCMtbBg3Yn29v4w1r+tgy4ff4k=
|
||||
@@ -249,8 +249,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0=
|
||||
github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
@@ -293,14 +293,14 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/mulgadc/bluebottle v1.22.0 h1:pGZ5/Ly3CYem62pPu2zKBRpvMpAG0LHMIMw9Yt/lkIg=
|
||||
github.com/mulgadc/bluebottle v1.22.0/go.mod h1:DwT91bJyUsrpg1gCJmcCOTG+tgw5J124LloHYvGAQu0=
|
||||
github.com/mulgadc/northstar v1.22.0 h1:fTndlk47hK76fcNr2MqsyeKhBNVEDt8+BC6Z30ohokA=
|
||||
github.com/mulgadc/northstar v1.22.0/go.mod h1:yLH8waqfHZx177Ae6dGQGTDt/02rPE9alnUKyxDNhVs=
|
||||
github.com/mulgadc/predastore v1.22.0 h1:mUQJMTxMiRDVvemMP+34BP/+r1U0iCptc9B++u4NJ8o=
|
||||
github.com/mulgadc/predastore v1.22.0/go.mod h1:/cMIoKtnQG/DWyeX/0matEQrG2/abY60tNHWcBKS9Kc=
|
||||
github.com/mulgadc/viperblock v1.22.0 h1:8vS2AtpIDkIhGNOazQPicKXV2y/8PAy8BNrVWIOCP6I=
|
||||
github.com/mulgadc/viperblock v1.22.0/go.mod h1:Hq8yMagAX8c4wZ68oUkwZPyxTNMwvRwB685eHFewisc=
|
||||
github.com/mulgadc/bluebottle v1.23.0 h1:h2BOSBDrNjIHA7QUeWX9xci2zcIHWsB1Ky0Q///QPaI=
|
||||
github.com/mulgadc/bluebottle v1.23.0/go.mod h1:eWfnT7nGgG93oChqMoTlGv2onPVZ2bVGSyg4nKl1s7I=
|
||||
github.com/mulgadc/northstar v1.23.0 h1:5oeFDB3fifzFb+I5nDAXsewt2rYdEbrUsHl7gwxPZ5k=
|
||||
github.com/mulgadc/northstar v1.23.0/go.mod h1:zZtrmR8ITOhZZQrVbCJQUBpOUiblA2kogKh0Oi5xh/8=
|
||||
github.com/mulgadc/predastore v1.23.0 h1:JpjhDyF1CLTLGDmXrYPSFRImDXfu5L2zeXdiHPHwtNw=
|
||||
github.com/mulgadc/predastore v1.23.0/go.mod h1:y5nBVTX+E+6G2z9xkH9gkecglKbUGnCCitd7d8ZZaJ8=
|
||||
github.com/mulgadc/viperblock v1.23.0 h1:/HZDVVw6/tiq497AVjMeEG+dLn4du4pcbBSiwZKCamg=
|
||||
github.com/mulgadc/viperblock v1.23.0/go.mod h1:frYnDTsFF0MTl7dP84fcO4PsyWvHMb8EQQFIPajxai0=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
|
||||
@@ -319,10 +319,10 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
|
||||
github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.127.0 h1:EfV88zZjJtsIdyALgAYJYL6Pj7Iw11Q2LNtnaGZf+sk=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.127.0/go.mod h1:YmvgbsnUfrsiJ410XAGQ1BRsrUYJJ7W3O5wUikmeI8w=
|
||||
github.com/ovn-kubernetes/libovsdb v0.8.1 h1:M2J8bcJt5mXCom0HqzfEtuHkT80CTSQRcYG7acT8gf4=
|
||||
github.com/ovn-kubernetes/libovsdb v0.8.1/go.mod h1:ZlnHLzagmLOSvyd9qfxBIZp6wOSOw0IsRsc+6lNUGbU=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.128.0 h1:QWh0D4VsQ+94XMFy4sapwHVuW5+VOF4dom+h8/cyJsw=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.128.0/go.mod h1:87yiCsoPSaOZkpt5KIEEThoSirgT8WolahE3I5/qz78=
|
||||
github.com/ovn-kubernetes/libovsdb v0.9.0 h1:WNQM8oCmh793mKa7PIIsF5JYoCa91lgXxG1nEEDStFU=
|
||||
github.com/ovn-kubernetes/libovsdb v0.9.0/go.mod h1:mbCZzSSa9FQ0YEFgeO2rXW/OYoMh6vqNrPGpsS2XYAo=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
@@ -345,8 +345,8 @@ github.com/pterm/pterm v0.12.84 h1:DA9943h1RfwlpcGWCIbD6f6nRTLCe09gK7w6GDXaIzE=
|
||||
github.com/pterm/pterm v0.12.84/go.mod h1:LVerZ1tFVFZrTv7lRbLdbrrTLSLAB5oZY8z3IY/z+IQ=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
|
||||
github.com/quic-go/quic-go v0.63.0 h1:LIFGHI4PFUhhw2dDD1ARHdCff143ffMHwZtbnbuJ78A=
|
||||
github.com/quic-go/quic-go v0.63.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w=
|
||||
github.com/quic-go/quic-go v0.64.1 h1:BCNTP+6x6++OPKrGST2FR3f1tycx8O3QOA3QQv09g70=
|
||||
github.com/quic-go/quic-go v0.64.1/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -401,22 +401,22 @@ go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.20.1 h1:5sHc4ToTFjfSZCtGAAM6jPunICAmJX73htv372T4ipc=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.20.1/go.mod h1:oa6kgvyz/3GYW04dohd0++xJIH4xdQY8PAbpeCMaM8M=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.21.0 h1:/CdgE52i1OEPHhsGUqrggYNRaZy4v4YgVenGd52dRBI=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.21.0/go.mod h1:9pBdbVbuehIDzxN4accnzzxEUPT8Nq7grbZMo+J1VN4=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 h1:LxwW/9ctSCv+QkE/cLR7M91ZIkXNMqJtEMi1vCw9U8s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.71.0 h1:v4KkRLVvE1cWqTJDfZZkTCG+Z4aolsa6RVos0FX7vqE=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.71.0/go.mod h1:g/xbuPC0XbgwMdKuyF5sKOUUEsorSkN6APydyFP/H9E=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.72.0 h1:rv8t3kKjyC2/9D99L1MZt4tJSZO4aYUmHus7L//iKKg=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.72.0/go.mod h1:ZzG+JmzLLvKzx6tMeLrw2l7F8ik2GeRZJALoPBqnM0c=
|
||||
go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
|
||||
go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0 h1:Bu39F5tzJct+f2IZbB8989fwyTps3c8e7EsUQsz+vs8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0/go.mod h1:dJUwod88EsFgYCqrDHaSPzhiY9pBUpt0d85/qSfua7k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 h1:qkDYCAFiZXLcs1L4aY+tP2wguQ4kURANqHOQMA2et2s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0/go.mod h1:tkipS4DRzmpAmvg+Gw4++O1IdDq6TVDnvnYU6cmbQVs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0 h1:5Ek4qQ5XX0PSQfY7lMW3CvcR36yy8YFDfGnYnaku8zE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0/go.mod h1:NUKIMOKSexQdTmS1pSxOQBDiVes3DHRier2ePsGW/gQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 h1:LaLVGJtIzHcvHjVs2OftmKcc+M9mdOqS8vZ1fBD1Yes=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0/go.mod h1:vF6H5sNDuQBJWgSX00iZgbIqk2MZZoVMH0ar5t5bOpw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 h1:UFpxOpYPmMNUtOWhdb+nC1WELIgVf8z9higURrbzYU4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0/go.mod h1:YBGjxe3lt0jtXoEXxGrGhv/jM7oUBUDVK7zhAvNyEjc=
|
||||
go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
|
||||
go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
|
||||
go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
|
||||
@@ -425,10 +425,10 @@ go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDR
|
||||
go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k=
|
||||
go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
|
||||
go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
|
||||
go.opentelemetry.io/otel/sdk/log v0.22.0 h1:PRL+s6P63XT4E/bheEflopPUpVxuvANqZwtt89yhoGk=
|
||||
go.opentelemetry.io/otel/sdk/log v0.22.0/go.mod h1:JNp0sBELrjCTcu5W3GzABVypeU6vDJjBS+X0JISuz+g=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.22.0 h1:infPnfNrhCNgOUZRs3gWUg8vhoBUHihq02gwK05gzlg=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.22.0/go.mod h1:gkQZA3z15Bv3KU9vigBTi8dFechSozRP7v94X4VZv+s=
|
||||
go.opentelemetry.io/otel/sdk/log v1.47.0 h1:W4N/ZfgpkRRnmARnqYoQ6BrWA0mrj8IzVDNLlpmUJZY=
|
||||
go.opentelemetry.io/otel/sdk/log v1.47.0/go.mod h1:Wu74Z2EmOKXi2FleSNAb0Hyau2G+r5kubSEMJw5aAMs=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.23.0 h1:j3pm42QCrKYYLLkI6OrV1QHFkUfDA6/4EP+Y1PzggLg=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.23.0/go.mod h1:zt0gEnuTZgDvqvczHwYswz93eqMaIr6upTTsdV/HUlU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
|
||||
go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
|
||||
@@ -517,10 +517,10 @@ golang.org/x/vuln v1.6.0/go.mod h1:bWlG2493/sjR7ksvicBgMrznH3eYQEyK8ifUYBrqUbg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 h1:izFU9hz7aeLI/Mi1J0991ae+xcwRLr7hTqWnB/9aIIU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5/go.mod h1:3LhxRw4YYkf+ylAfgaY9JlVLFKhokkCV8duhLLe7+t0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
|
||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
|
||||
@@ -10,8 +10,6 @@ var modelFS embed.FS
|
||||
|
||||
var modelFiles = map[Service]string{
|
||||
ACM: "models/acm-2015-12-08.json.gz",
|
||||
Bedrock: "models/bedrock-2023-04-20.json.gz",
|
||||
BedrockRuntime: "models/bedrock-runtime-2023-09-30.json.gz",
|
||||
EC2: "models/ec2-2016-11-15.json.gz",
|
||||
ECR: "models/ecr-2015-09-21.json.gz",
|
||||
ECS: "models/ecs-2014-11-13.json.gz",
|
||||
|
||||
@@ -27,8 +27,6 @@ type Service string
|
||||
|
||||
const (
|
||||
ACM Service = "acm"
|
||||
Bedrock Service = "bedrock"
|
||||
BedrockRuntime Service = "bedrockruntime"
|
||||
EC2 Service = "ec2"
|
||||
ECR Service = "ecr"
|
||||
ECS Service = "ecs"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestServices(t *testing.T) {
|
||||
want := []Service{ACM, Bedrock, BedrockRuntime, EC2, ECR, ECS, EKS, ElasticLoadBalancingV2, IAM, RDS, S3, STS}
|
||||
want := []Service{ACM, EC2, ECR, ECS, EKS, ElasticLoadBalancingV2, IAM, RDS, S3, STS}
|
||||
if got := Services(); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Services() = %v, want %v", got, want)
|
||||
}
|
||||
@@ -21,8 +21,6 @@ func TestLoadAllModels(t *testing.T) {
|
||||
shapes int
|
||||
}{
|
||||
ACM: {"2015-12-08", 40, 239},
|
||||
Bedrock: {"2023-04-20", 108, 881},
|
||||
BedrockRuntime: {"2023-09-30", 11, 349},
|
||||
EC2: {"2016-11-15", 804, 4245},
|
||||
ECR: {"2015-09-21", 58, 404},
|
||||
ECS: {"2014-11-13", 77, 643},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AWS models
|
||||
|
||||
The `*.json.gz` files are the Smithy JSON AST service models from [`aws/api-models-aws`](https://github.com/aws/api-models-aws), gzipped and embedded into the binary, so loading them needs no network or Go module cache. All of them come from the one commit recorded in `../model_source.go`.
|
||||
The `*.json.gz` files are the Smithy JSON AST service models from [`aws/api-models-aws`](https://github.com/aws/api-models-aws), gzipped and embedded into the binary, so loading them needs no network or Go module cache. All ten come from the one commit recorded in `../model_source.go`.
|
||||
|
||||
Do not edit them by hand. To move to a newer commit, run `scripts/sync-aws-models.sh <commit-sha>`, then `make generate-aws-model-coverage` and review the regenerated `docs/coverage` and the `awsmodel` test expectations: a newer model can add operations, required members and enum values, and each resulting finding is a real gap to triage rather than a reason to stay on an older commit.
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -322,18 +322,8 @@ func (g *requestGenerator) site(path []pathStep, site constraintSite) {
|
||||
}
|
||||
|
||||
// value generates a valid value for ref at path, reporting false when none
|
||||
// can be generated. A value that is not generated is left out of the request,
|
||||
// so the sites recorded beneath it are dropped with it.
|
||||
// can be generated.
|
||||
func (g *requestGenerator) value(ref ShapeRef, path []pathStep, depth int) (any, bool) {
|
||||
sites := len(g.sites)
|
||||
value, ok := g.generate(ref, path, depth)
|
||||
if !ok {
|
||||
g.sites = g.sites[:sites]
|
||||
}
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func (g *requestGenerator) generate(ref ShapeRef, path []pathStep, depth int) (any, bool) {
|
||||
shape := g.model.shapes[ref.Shape]
|
||||
if ref.Streaming {
|
||||
g.skip(path, "streaming payload")
|
||||
|
||||
@@ -299,17 +299,6 @@ func TestGenerateRequestsBreaksMapKeys(t *testing.T) {
|
||||
require.Equal(t, []Constraint{ConstraintLength, ConstraintLength}, keyRejections, "below min and above max")
|
||||
}
|
||||
|
||||
// A map of documents is left out of the request because document values are
|
||||
// not generated, so its key must not be broken in a map that is not there.
|
||||
func TestGenerateRequestsDropsSitesUnderAnUngeneratedValue(t *testing.T) {
|
||||
plan, err := GenerateRequests(Bedrock, "CreateAdvancedPromptOptimizationJob", testRequestOptions)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, plan.Skipped, Skip{Path: "$.modelConfigurations[0].additionalModelRequestFields.spxa", Reason: "document values are not generated"})
|
||||
for _, request := range plan.Cases {
|
||||
require.NotContains(t, request.Path, "additionalModelRequestFields")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRequestsRecordsUnbrokenConstraints(t *testing.T) {
|
||||
plan, err := GenerateRequests(EKS, "DescribeCluster", testRequestOptions)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/spinifex/internal/gwsign"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
awsxml "github.com/mulgadc/spinifex/spinifex/foundation/aws/xml"
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
)
|
||||
|
||||
// gatewayStub captures the request the client sent and replies with the body a
|
||||
@@ -74,12 +74,12 @@ func xmlResult(t *testing.T, action string, payload any) []byte {
|
||||
|
||||
func TestCall_SignsAndFormEncodes(t *testing.T) {
|
||||
stub := newGatewayStub(t, http.StatusOK, func(url.Values) []byte {
|
||||
return xmlResult(t, "RegisterDBInstance", &handlers_rds.RegisterDBInstanceOutput{
|
||||
return xmlResult(t, "RegisterDBInstance", &rdsinstance.RegisterDBInstanceOutput{
|
||||
DBInstanceIdentifier: "db-1", HeartbeatIntervalSeconds: 30,
|
||||
})
|
||||
})
|
||||
|
||||
var out handlers_rds.RegisterDBInstanceOutput
|
||||
var out rdsinstance.RegisterDBInstanceOutput
|
||||
err := newTestClient(t, stub).Call(context.Background(), "RegisterDBInstance",
|
||||
url.Values{"DBInstanceIdentifier": {"db-1"}, "AgentVersion": {"test"}}, &out)
|
||||
if err != nil {
|
||||
@@ -116,8 +116,8 @@ func TestCall_SignsAndFormEncodes(t *testing.T) {
|
||||
|
||||
func TestCall_DecodesNestedListsAndOptionalFields(t *testing.T) {
|
||||
password := "s3cret"
|
||||
want := &handlers_rds.GetDBBootstrapConfigOutput{
|
||||
Mode: handlers_rds.BootstrapModeInitialize,
|
||||
want := &rdsinstance.GetDBBootstrapConfigOutput{
|
||||
Mode: rdsinstance.BootstrapModeInitialize,
|
||||
DBInstanceIdentifier: "db-1",
|
||||
Engine: "postgres",
|
||||
MasterUsername: "master",
|
||||
@@ -127,7 +127,7 @@ func TestCall_DecodesNestedListsAndOptionalFields(t *testing.T) {
|
||||
DataVolumeSerial: "voldata01",
|
||||
VMGeneration: 3,
|
||||
FormatAuthorized: true,
|
||||
Parameters: []handlers_rds.Parameter{
|
||||
Parameters: []rdsinstance.Parameter{
|
||||
{Name: "max_connections", Value: "100"},
|
||||
{Name: "work_mem", Value: "4MB"},
|
||||
},
|
||||
@@ -137,12 +137,12 @@ func TestCall_DecodesNestedListsAndOptionalFields(t *testing.T) {
|
||||
return xmlResult(t, "GetDBBootstrapConfig", want)
|
||||
})
|
||||
|
||||
var got handlers_rds.GetDBBootstrapConfigOutput
|
||||
var got rdsinstance.GetDBBootstrapConfigOutput
|
||||
if err := newTestClient(t, stub).Call(context.Background(), "GetDBBootstrapConfig", nil, &got); err != nil {
|
||||
t.Fatalf("Call: %v", err)
|
||||
}
|
||||
|
||||
if got.Mode != handlers_rds.BootstrapModeInitialize || got.Port != 5432 {
|
||||
if got.Mode != rdsinstance.BootstrapModeInitialize || got.Port != 5432 {
|
||||
t.Errorf("mode/port = %q/%d, want initialize/5432", got.Mode, got.Port)
|
||||
}
|
||||
if got.MasterUserPassword == nil || *got.MasterUserPassword != password {
|
||||
@@ -166,14 +166,14 @@ func TestCall_DecodesNestedListsAndOptionalFields(t *testing.T) {
|
||||
// mode is defined by the password element being missing entirely.
|
||||
func TestCall_AbsentOptionalStaysNil(t *testing.T) {
|
||||
stub := newGatewayStub(t, http.StatusOK, func(url.Values) []byte {
|
||||
return xmlResult(t, "GetDBBootstrapConfig", &handlers_rds.GetDBBootstrapConfigOutput{
|
||||
Mode: handlers_rds.BootstrapModeAttach,
|
||||
return xmlResult(t, "GetDBBootstrapConfig", &rdsinstance.GetDBBootstrapConfigOutput{
|
||||
Mode: rdsinstance.BootstrapModeAttach,
|
||||
MasterUsername: "master",
|
||||
Port: 5432,
|
||||
})
|
||||
})
|
||||
|
||||
var got handlers_rds.GetDBBootstrapConfigOutput
|
||||
var got rdsinstance.GetDBBootstrapConfigOutput
|
||||
if err := newTestClient(t, stub).Call(context.Background(), "GetDBBootstrapConfig", nil, &got); err != nil {
|
||||
t.Fatalf("Call: %v", err)
|
||||
}
|
||||
@@ -189,15 +189,15 @@ func TestCall_AbsentOptionalStaysNil(t *testing.T) {
|
||||
// the wire: the locationName tags drive the gateway's marshal, the xml tags the
|
||||
// agent's decode.
|
||||
type commandsResult struct {
|
||||
Commands []handlers_rds.Command `locationName:"Commands" locationNameList:"member" xml:"Commands>member"`
|
||||
Commands []rdsinstance.Command `locationName:"Commands" locationNameList:"member" xml:"Commands>member"`
|
||||
}
|
||||
|
||||
func TestCall_RoundTripsCommands(t *testing.T) {
|
||||
issued := time.Date(2026, 7, 28, 4, 5, 6, 0, time.UTC)
|
||||
want := commandsResult{Commands: []handlers_rds.Command{{
|
||||
want := commandsResult{Commands: []rdsinstance.Command{{
|
||||
CommandID: "cmd-1",
|
||||
Type: "set-master-password",
|
||||
Parameters: []handlers_rds.Parameter{{Name: "MasterUserPassword", Value: "s3cret"}},
|
||||
Parameters: []rdsinstance.Parameter{{Name: "MasterUserPassword", Value: "s3cret"}},
|
||||
IssuedAt: &issued,
|
||||
}}}
|
||||
stub := newGatewayStub(t, http.StatusOK, func(url.Values) []byte {
|
||||
|
||||
@@ -583,13 +583,15 @@ else
|
||||
fi
|
||||
|
||||
# Compute nodes: no database of their own, pointed at every database node.
|
||||
for i in $(seq "$DB_NODES" $((N - 1))); do
|
||||
on "${HOSTS[$i]}" "sudo $SETUP_OVN \
|
||||
--node-name=${NODE_NAMES[$i]} \
|
||||
--ovn-remote=$OVN_REMOTE $OVN_UPLINK_ARGS \
|
||||
--encap-ip=${VPC_IPS[$i]}" || fail "${HOSTS[$i]}: setup-ovn.sh failed"
|
||||
log " ${HOSTS[$i]} attached as a compute node"
|
||||
done
|
||||
if [ "$N" -gt "$DB_NODES" ]; then
|
||||
for i in $(seq "$DB_NODES" $((N - 1))); do
|
||||
on "${HOSTS[$i]}" "sudo $SETUP_OVN \
|
||||
--node-name=${NODE_NAMES[$i]} \
|
||||
--ovn-remote=$OVN_REMOTE $OVN_UPLINK_ARGS \
|
||||
--encap-ip=${VPC_IPS[$i]}" || fail "${HOSTS[$i]}: setup-ovn.sh failed"
|
||||
log " ${HOSTS[$i]} attached as a compute node"
|
||||
done
|
||||
fi
|
||||
|
||||
# --- Form the cluster ------------------------------------------------------
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# Debian is the build host regardless of what is being built — mkosi bootstraps
|
||||
# the target distribution from its own archive, so an Ubuntu image builds fine
|
||||
# from here (see ubuntu-keyring below).
|
||||
FROM debian:trixie-slim@sha256:a29215f6a35e51e22adffa17f89e9d2ef06214e64a2bad10d765c46aea49f11f
|
||||
FROM debian:trixie-slim@sha256:62bd423aeee912639ca8ee681c2b762cb3e25137e0a92b20be455898a9f034ad
|
||||
|
||||
# The runner user is uid 1000; matching it keeps bind-mounted build output
|
||||
# owned by the invoking user instead of root.
|
||||
|
||||
@@ -20,8 +20,6 @@ REPO="https://github.com/aws/api-models-aws.git"
|
||||
# <api-models-aws sdk-id>/<api version>
|
||||
MODELS=(
|
||||
acm/2015-12-08
|
||||
bedrock/2023-04-20
|
||||
bedrock-runtime/2023-09-30
|
||||
ec2/2016-11-15
|
||||
ecr/2015-09-21
|
||||
ecs/2014-11-13
|
||||
@@ -36,7 +34,7 @@ MODELS=(
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# A blobless shallow fetch pulls only the files read below.
|
||||
# A blobless shallow fetch pulls only the ten files read below.
|
||||
git -C "$TMP" init --quiet repo
|
||||
git -C "$TMP/repo" fetch --quiet --depth=1 --filter=blob:none "$REPO" "$COMMIT"
|
||||
COMMITTED="$(git -C "$TMP/repo" log -1 --format=%cs "$COMMIT")"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Terraform configuration that builds the OCI infrastructure a Spinifex cluster runs on: a VCN with its gateways and subnets, one to N nodes with two VNICs each, and a data volume per node. No DRG, remote peering, local peering gateway, RZG, or other external network attachment is created.
|
||||
|
||||
**This is a local fork of [`aszynkow/oci_mulgadc`](https://github.com/aszynkow/oci_mulgadc), modified for Spinifex.** It builds infrastructure only; Spinifex itself is installed afterwards by the existing deploy path (`update-nodes.sh`, `install-node.sh`), the same scripts that serve prod and dev-prod. The operator guide that wraps it is [`docs/oci-integration`](../../../docs/oci-integration/README.md); the plan is `docs/development/feature/oci-terraform-provisioning.md` in the mulga monorepo.
|
||||
**This is a local fork of [`aszynkow/oci_mulgadc`](https://github.com/aszynkow/oci_mulgadc), modified for Spinifex.** It builds OCI infrastructure only. Run `validate-topology.sh` afterwards to install Spinifex, form the cluster, configure the OCI allocator, and validate it; the driver can either build new infrastructure itself or use this Terraform state through `--existing-state-file`. It invokes `install-node.sh` internally for multi-node formation. See [Deploy and validate Spinifex on OCI](../../../docs/oci-integration/README.md#5-deploy) for the driver commands, including the existing-infrastructure path. The operator guide is [`docs/oci-integration`](../../../docs/oci-integration/README.md); the plan is `docs/development/feature/oci-terraform-provisioning.md` in the mulga monorepo.
|
||||
|
||||
## What This Repository Adds
|
||||
|
||||
@@ -257,7 +257,7 @@ Either way, Terraform stages the matching pool block at `/etc/spinifex/oci/exter
|
||||
|
||||
## Validating a topology end to end
|
||||
|
||||
`validate-topology.sh` builds one topology from nothing, installs the published Spinifex release, forms the cluster, runs a Terraform workbook against it, proves the workbook serves traffic, and destroys everything. Three topologies, because each breaks differently — bare metal presents VNICs unlike a VM, a single node has no Geneve underlay to get wrong, and only a cluster exercises RAFT, the gateway chassis and cross-node allocation.
|
||||
`validate-topology.sh` has two modes. By default it builds a topology from nothing, installs the published Spinifex release, forms the cluster, runs a Terraform workbook against it, proves the workbook serves traffic, and destroys everything. In existing-infrastructure mode, it reads an already-applied Terraform state and installs/validates Spinifex without applying or destroying OCI infrastructure. Three topologies exist because each breaks differently — bare metal presents VNICs unlike a VM, a single node has no Geneve underlay to get wrong, and only a cluster exercises RAFT, the gateway chassis and cross-node allocation.
|
||||
|
||||
```bash
|
||||
./validate-topology.sh --topology bm --dry-run
|
||||
@@ -277,6 +277,46 @@ The count that later gates read is the number of addresses the `hosts_file` outp
|
||||
|
||||
The workbook runs **on the node** against `127.0.0.1`, because the node certificate carries no SAN for its public address — a workbook driven from outside the VCN is still blocked.
|
||||
|
||||
### Validate infrastructure Terraform already created
|
||||
|
||||
Use this after you have completed the Terraform-only [Quick Start](#quick-start). The default state file contains the `hosts_file`, `compute_shape`, and `instance_principal` outputs the driver needs.
|
||||
|
||||
For infrastructure configured with `instance_principal = "adopt"` and an existing Dynamic Group/policy, run from this directory:
|
||||
|
||||
```bash
|
||||
./validate-topology.sh \
|
||||
--topology vm-multi \
|
||||
--existing-state-file "$PWD/terraform.tfstate" \
|
||||
--instance-principal \
|
||||
--oci-profile <compartment-deployer-profile> \
|
||||
--ssh-public-key ~/.ssh/oci-spx.pub \
|
||||
--ssh-private-key ~/.ssh/oci-spx \
|
||||
--skip-workload
|
||||
```
|
||||
|
||||
If the Dynamic Group and policy do not exist, add both of these options to the same command:
|
||||
|
||||
```text
|
||||
--setup-instance-principal --identity-oci-profile <tenancy-admin-profile>
|
||||
```
|
||||
|
||||
The Terraform-only apply still creates no identity resources. The optional validation-driver flag is the action that creates the Dynamic Group/policy, using a separate `.identity/terraform.tfstate`.
|
||||
|
||||
For infrastructure configured with `instance_principal = "off"`, supply the API-key handoff hook instead of `--instance-principal`:
|
||||
|
||||
```bash
|
||||
./validate-topology.sh \
|
||||
--topology vm-multi \
|
||||
--existing-state-file "$PWD/terraform.tfstate" \
|
||||
--oci-profile <compartment-deployer-profile> \
|
||||
--credential-hook ./spx-oci-config.sh \
|
||||
--ssh-public-key ~/.ssh/oci-spx.pub \
|
||||
--ssh-private-key ~/.ssh/oci-spx \
|
||||
--skip-workload
|
||||
```
|
||||
|
||||
`--existing-state-file` skips Terraform apply and destroy, and automatically keeps the hosts. If state is unavailable, replace it with `--existing-hosts-file /path/to/hosts`, containing one public SSH IP address per line. A hosts file carries no authentication metadata, so pass either `--instance-principal` or `--credential-hook` explicitly.
|
||||
|
||||
## Common Commands
|
||||
|
||||
Show the current outputs:
|
||||
|
||||
@@ -19,6 +19,13 @@ resource "oci_identity_dynamic_group" "nodes" {
|
||||
# Scoped to the compartment, not to instance OCIDs: nodes are replaced, and a
|
||||
# rule listing them by OCID goes stale the first time one is.
|
||||
matching_rule = "ALL {instance.compartment.id = '${var.compartment_ocid}'}"
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = local.tenancy_home_region != ""
|
||||
error_message = "OCI did not report a tenancy home region; cannot create instance-principal IAM resources."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Narrower than it looks. The allocator creates, moves and deletes secondary
|
||||
@@ -41,4 +48,10 @@ resource "oci_identity_policy" "nodes" {
|
||||
# is refused as a 404, which reads as a missing resource rather than a missing grant.
|
||||
"Allow dynamic-group ${oci_identity_dynamic_group.nodes[0].name} to use subnets in compartment id ${var.compartment_ocid}",
|
||||
]
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = local.tenancy_home_region != ""
|
||||
error_message = "OCI did not report a tenancy home region; cannot create instance-principal IAM resources."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,19 @@ provider "oci" {
|
||||
region = var.region
|
||||
}
|
||||
|
||||
# Region subscriptions are readable from the deployment-region provider. OCI
|
||||
# marks exactly one subscription as the tenancy home region; its IAM endpoint is
|
||||
# where dynamic groups and policies must be written.
|
||||
data "oci_identity_region_subscriptions" "tenancy" {
|
||||
count = var.instance_principal == "create" ? 1 : 0
|
||||
tenancy_id = var.tenancy_ocid
|
||||
}
|
||||
locals {
|
||||
tenancy_home_region = var.instance_principal == "create" ? one([
|
||||
for subscription in data.oci_identity_region_subscriptions.tenancy[0].region_subscriptions : subscription.region_name
|
||||
if subscription.is_home_region
|
||||
]) : var.region
|
||||
}
|
||||
# OCI serves IAM writes from the tenancy's home region, so the dynamic group and
|
||||
# policy go through this one. Defaults to var.region, which makes it identical to
|
||||
# the provider above until OCI_HOME_REGION says otherwise -- so a tenancy whose
|
||||
@@ -21,5 +34,5 @@ provider "oci" {
|
||||
fingerprint = var.fingerprint
|
||||
private_key = var.private_key != "" ? var.private_key : null
|
||||
private_key_path = var.private_key == "" ? var.private_key_path : null
|
||||
region = var.home_region != null && var.home_region != "" ? var.home_region : var.region
|
||||
region = local.tenancy_home_region
|
||||
}
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
# Example non-secret overrides. Copy to the untracked terraform.auto.tfvars.
|
||||
|
||||
# Restrict who may reach a node's SSH and Spinifex service ports from the internet.
|
||||
node_client_cidr_allow_list = ["203.0.113.10/32"]
|
||||
# Existing OCI compartment where every POC resource is created. Required.
|
||||
compartment_ocid = "ocid1.compartment.oc1.."
|
||||
|
||||
# 1 for the single-node replica, 3 for a cluster.
|
||||
node_count = 1
|
||||
# Number of Spinifex nodes. Keep this aligned with --topology: 1 for vm-single
|
||||
# or bm, and 3 for vm-multi. Omit it to use the topology's default.
|
||||
node_count = 3
|
||||
|
||||
# Bare metal is preferred; this is what we test on until the quota is raised.
|
||||
compute_shape = "VM.Standard.E6.Flex"
|
||||
compute_ocpus = 8
|
||||
# OCI Flex VM shape. Use a fixed BM.* shape for a bare-metal deployment.
|
||||
compute_shape = "VM.Standard.E6.Flex"
|
||||
|
||||
# CPU allocation per Flex VM, measured in OCI OCPUs (one OCPU is two vCPUs).
|
||||
# Ignored by fixed-shape bare-metal instances.
|
||||
compute_ocpus = 8
|
||||
|
||||
# Memory allocation per Flex VM in GiB. Ignored by fixed-shape bare-metal instances.
|
||||
compute_memory_in_gbs = 32
|
||||
|
||||
# Persistent block-volume capacity, in GiB, mounted as /var/lib/spinifex on each node.
|
||||
data_volume_size_in_gbs = 1024
|
||||
|
||||
# Block-volume performance setting, in VPUs per GiB. Higher values cost more and
|
||||
# provide more IOPS. Consult OCI volume-performance limits before increasing it.
|
||||
data_volume_vpus_per_gb = 60
|
||||
|
||||
# OCI authentication mode used by the Spinifex public-IP allocator. "adopt" means
|
||||
# the Dynamic Group and policy already exist. The driver's --instance-principal
|
||||
# flag enforces this mode; do not set this to "create" in a normal deployment state.
|
||||
instance_principal = "adopt"
|
||||
|
||||
# OCI security-list sources allowed to reach the public subnet. These rules also
|
||||
# affect guests with public OCI addresses, so keep 0.0.0.0/0 for a public POC and
|
||||
# use the Spinifex host firewall to restrict node SSH/console access instead.
|
||||
node_client_cidr_allow_list = ["0.0.0.0/0"]
|
||||
|
||||
@@ -24,11 +24,19 @@ SKIP_WORKLOAD=0
|
||||
SKIP_POOL=0
|
||||
DRY_RUN=0
|
||||
DESTROY_ONLY=0
|
||||
DESTROY_IDENTITY=0
|
||||
INSTANCE_PRINCIPAL=0
|
||||
SETUP_INSTANCE_PRINCIPAL=0
|
||||
REINSTALL=0
|
||||
SHOW_AWS_CREDENTIALS=0
|
||||
CREDENTIAL_HOOK="${OCI_CREDENTIAL_HOOK:-}"
|
||||
# Empty means oci_env.py chooses, which is $OCI_CLI_PROFILE, then the reference
|
||||
# tenancy, then DEFAULT. Named here so a deployment can say which tenancy it is in.
|
||||
OCI_PROFILE="${OCI_PROFILE:-}"
|
||||
IDENTITY_OCI_PROFILE=""
|
||||
EXISTING_HOSTS_FILE=""
|
||||
EXISTING_STATE_FILE=""
|
||||
EXISTING_INFRA=0
|
||||
DISTRO=""
|
||||
SETUP_SH=""
|
||||
# Empty means the driver's own default list. Unset is distinguishable from empty,
|
||||
@@ -80,8 +88,8 @@ usage() {
|
||||
cat >&2 <<EOF
|
||||
usage: ${0##*/} --topology <bm|vm-single|vm-multi|vm-single-principal> [options]
|
||||
|
||||
Builds the topology, installs Spinifex, forms the cluster, runs a Terraform
|
||||
workbook against it, then destroys everything.
|
||||
Builds a topology, or installs on existing OCI hosts, then installs Spinifex,
|
||||
forms the cluster, and verifies the OCI address allocator.
|
||||
|
||||
--topology <name> Required. No default: a command aimed at the wrong
|
||||
topology is the easiest expensive mistake here.
|
||||
@@ -94,6 +102,23 @@ workbook against it, then destroys everything.
|
||||
policy to exist already; see instance-principal.tf.
|
||||
Not needed when instance_principal is set in your
|
||||
tfvars, which this reads; the flag outranks it.
|
||||
--setup-instance-principal
|
||||
Create the dynamic group and policy before deployment.
|
||||
Needs a tenancy-admin profile. Opt-in because these are
|
||||
tenancy-level resources. Use --identity-oci-profile to
|
||||
use a different profile for this one-time setup.
|
||||
--identity-oci-profile NAME
|
||||
Tenancy-admin profile for --setup-instance-principal.
|
||||
Defaults to --oci-profile / OCI_PROFILE.
|
||||
--existing-hosts-file PATH
|
||||
Install and form on existing hosts listed one public SSH
|
||||
IP per line. Skips Terraform apply and never destroys.
|
||||
Specify --instance-principal or --credential-hook.
|
||||
--existing-state-file PATH
|
||||
Read hosts_file, compute_shape and instance_principal
|
||||
from an existing local Terraform state. Skips Terraform
|
||||
apply and never destroys. Mutually exclusive with
|
||||
--existing-hosts-file.
|
||||
--credential-hook PATH Executable run after formation, before the pool, as
|
||||
"hook <ssh-key> <host>...". Where an API-key deployment
|
||||
installs its credential. Default \$OCI_CREDENTIAL_HOOK.
|
||||
@@ -111,13 +136,21 @@ workbook against it, then destroys everything.
|
||||
--distro PATH Install this distro tarball instead of the published
|
||||
release, so what is proved is the ref it was built from.
|
||||
--setup-sh PATH setup.sh to pair with --distro. Both or neither.
|
||||
--reinstall Re-run the package installer on hosts that already
|
||||
have Spinifex. Normally a failed run resumes from the
|
||||
installed package to avoid replacing clustered OVN.
|
||||
--no-external-pool Form without an OCI credential: no external pool and no
|
||||
allocator gate. Implies --skip-workload, because a guest
|
||||
with no public address proves nothing a customer wants.
|
||||
--skip-workload Form and verify only; launch no guests.
|
||||
--show-aws-credentials Print node 1's ~/.aws/credentials after the cluster is
|
||||
Ready. This reveals a secret; do not use it in CI logs.
|
||||
--keep Leave the infrastructure up. Implies no verdict.
|
||||
--keep-on-fail Destroy on success, leave a failure up to inspect.
|
||||
--destroy-only Destroy whatever this topology's state holds, then stop.
|
||||
--destroy-identity With --destroy-only, also destroy the Dynamic Group and
|
||||
IAM policy tracked by setup-identity.sh. These are
|
||||
tenancy-wide and may be shared by another cluster.
|
||||
--dry-run Print the plan and the per-node steps, change nothing.
|
||||
EOF
|
||||
exit 2
|
||||
@@ -133,14 +166,21 @@ while [ $# -gt 0 ]; do
|
||||
--version) INSTALL_VERSION="${2:?}"; shift 2 ;;
|
||||
--distro) DISTRO="${2:?}"; shift 2 ;;
|
||||
--setup-sh) SETUP_SH="${2:?}"; shift 2 ;;
|
||||
--reinstall) REINSTALL=1; shift ;;
|
||||
--instance-principal) INSTANCE_PRINCIPAL=1; shift ;;
|
||||
--setup-instance-principal) SETUP_INSTANCE_PRINCIPAL=1; shift ;;
|
||||
--identity-oci-profile) IDENTITY_OCI_PROFILE="${2:?}"; shift 2 ;;
|
||||
--existing-hosts-file) EXISTING_HOSTS_FILE="${2:?}"; shift 2 ;;
|
||||
--existing-state-file) EXISTING_STATE_FILE="${2:?}"; shift 2 ;;
|
||||
--credential-hook) CREDENTIAL_HOOK="${2:?}"; shift 2 ;;
|
||||
--oci-profile) OCI_PROFILE="${2:?}"; shift 2 ;;
|
||||
--no-external-pool) SKIP_POOL=1; SKIP_WORKLOAD=1; shift ;;
|
||||
--skip-workload) SKIP_WORKLOAD=1; shift ;;
|
||||
--show-aws-credentials) SHOW_AWS_CREDENTIALS=1; shift ;;
|
||||
--keep) KEEP=1; shift ;;
|
||||
--keep-on-fail) KEEP_ON_FAIL=1; shift ;;
|
||||
--destroy-only) DESTROY_ONLY=1; shift ;;
|
||||
--destroy-identity) DESTROY_IDENTITY=1; shift ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
-h | --help) usage ;;
|
||||
*) die "unknown option $1" ;;
|
||||
@@ -167,6 +207,22 @@ if [ "$TOPO_FORCE_PRINCIPAL" = 1 ]; then
|
||||
|| die "--no-external-pool makes $TOPOLOGY meaningless: the allocator is what exercises the instance principal, so a run without it proves nothing"
|
||||
fi
|
||||
|
||||
[ -z "$EXISTING_HOSTS_FILE" ] || [ -z "$EXISTING_STATE_FILE" ] \
|
||||
|| die "--existing-hosts-file and --existing-state-file are mutually exclusive"
|
||||
if [ -n "$EXISTING_HOSTS_FILE" ] || [ -n "$EXISTING_STATE_FILE" ]; then
|
||||
EXISTING_INFRA=1
|
||||
[ "$DESTROY_ONLY" = 0 ] \
|
||||
|| die "--destroy-only is unavailable with existing infrastructure; this driver did not create it"
|
||||
[ "$KEEP_ON_FAIL" = 0 ] \
|
||||
|| die "--keep-on-fail is unnecessary with existing infrastructure, which is always kept"
|
||||
KEEP=1
|
||||
fi
|
||||
|
||||
[ "$SETUP_INSTANCE_PRINCIPAL" = 0 ] || [ "$INSTANCE_PRINCIPAL" = 1 ] \
|
||||
|| die "--setup-instance-principal requires --instance-principal (or vm-single-principal)"
|
||||
[ "$DESTROY_IDENTITY" = 0 ] || [ "$DESTROY_ONLY" = 1 ] \
|
||||
|| die "--destroy-identity is valid only with --destroy-only"
|
||||
|
||||
# Terraform's own order, as far as this script needs it: the last assignment across
|
||||
# terraform.tfvars then *.auto.tfvars alphabetically is the one that applies. Read
|
||||
# because the credential-hook gate has to run before the apply spends anything; the
|
||||
@@ -206,7 +262,9 @@ NODES="$TOPO_DEFAULT_NODES"
|
||||
# sweep could read it, leaving instances running that nothing can find.
|
||||
STATE_ROOT="${OCI_STATE_ROOT:-$HERE}"
|
||||
mkdir -p "$STATE_ROOT"
|
||||
STATE_DIR="$STATE_ROOT/.validate-$TOPOLOGY"
|
||||
STATE_SUFFIX=""
|
||||
[ "$EXISTING_INFRA" = 1 ] && STATE_SUFFIX="-existing"
|
||||
STATE_DIR="$STATE_ROOT/.validate-$TOPOLOGY$STATE_SUFFIX"
|
||||
RESULTS="$STATE_DIR/results.txt"
|
||||
|
||||
# Tab-separated siblings of the human block, for the run-page tables. Written here
|
||||
@@ -239,6 +297,108 @@ if [ "${#tf_principal_var[@]}" -gt 0 ]; then
|
||||
tf_vars=("${tf_principal_var[@]}" "${tf_vars[@]}")
|
||||
fi
|
||||
|
||||
# Keep host discovery separate from provisioning. The downstream install,
|
||||
# formation and allocator checks need only public SSH addresses, so they can be
|
||||
# reused against infrastructure Terraform did not create in this run.
|
||||
load_hosts() {
|
||||
local raw="$1" source="$2" line other
|
||||
HOSTS=()
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
line="${line%%#*}"
|
||||
line="$(printf '%s' "$line" | tr -d '[:space:]')"
|
||||
[ -n "$line" ] || continue
|
||||
[[ "$line" =~ ^[0-9]+(\.[0-9]+){3}$ ]] \
|
||||
|| die "$source contains a non-IPv4 host entry: '$line'"
|
||||
for other in "${HOSTS[@]:-}"; do
|
||||
[ "$line" != "$other" ] || die "$source lists '$line' more than once"
|
||||
done
|
||||
HOSTS+=("$line")
|
||||
done <<EOF
|
||||
$raw
|
||||
EOF
|
||||
[ "${#HOSTS[@]}" -ge 1 ] || die "$source named no hosts"
|
||||
}
|
||||
|
||||
state_output() {
|
||||
local name="$1"
|
||||
terraform -chdir="$HERE" output -state="$EXISTING_STATE_FILE" -raw "$name"
|
||||
}
|
||||
|
||||
load_existing_infrastructure() {
|
||||
local raw state_principal
|
||||
if [ -n "$EXISTING_HOSTS_FILE" ]; then
|
||||
[ -r "$EXISTING_HOSTS_FILE" ] \
|
||||
|| die "--existing-hosts-file is not readable: '$EXISTING_HOSTS_FILE'"
|
||||
raw="$(cat "$EXISTING_HOSTS_FILE")"
|
||||
load_hosts "$raw" "--existing-hosts-file $EXISTING_HOSTS_FILE"
|
||||
SHAPE="existing infrastructure"
|
||||
# A hosts file contains no trustworthy authentication metadata. Demand
|
||||
# an explicit instance-principal flag or an API-key hook rather than
|
||||
# quietly inheriting a stale local tfvars value.
|
||||
if [ "$INSTANCE_PRINCIPAL" != 1 ] && [ -z "$CREDENTIAL_HOOK" ]; then
|
||||
die "--existing-hosts-file needs --instance-principal or --credential-hook; host addresses cannot prove how the nodes authenticate"
|
||||
fi
|
||||
else
|
||||
[ -r "$EXISTING_STATE_FILE" ] \
|
||||
|| die "--existing-state-file is not readable: '$EXISTING_STATE_FILE'"
|
||||
raw="$(state_output hosts_file)" \
|
||||
|| die "could not read hosts_file from existing state '$EXISTING_STATE_FILE'"
|
||||
load_hosts "$raw" "hosts_file in $EXISTING_STATE_FILE"
|
||||
SHAPE="$(state_output compute_shape 2>/dev/null || echo 'existing infrastructure')"
|
||||
state_principal="$(state_output instance_principal)" \
|
||||
|| die "could not read instance_principal from existing state '$EXISTING_STATE_FILE'"
|
||||
if [ "$INSTANCE_PRINCIPAL" = 1 ] && [ "$state_principal" != adopt ]; then
|
||||
die "--instance-principal was requested but existing state says instance_principal='$state_principal'"
|
||||
fi
|
||||
if [ "$INSTANCE_PRINCIPAL" != 1 ]; then
|
||||
PRINCIPAL_MODE="$state_principal"
|
||||
fi
|
||||
fi
|
||||
NODES="${#HOSTS[@]}"
|
||||
log "using existing infrastructure: ${HOSTS[*]}"
|
||||
}
|
||||
|
||||
identity_resources() {
|
||||
local action="$1" identity_profile
|
||||
case "$action" in
|
||||
create)
|
||||
[ "$SETUP_INSTANCE_PRINCIPAL" = 1 ] || return 0
|
||||
if [ "$DRY_RUN" = 1 ]; then
|
||||
log "dry run: planning the instance-principal dynamic group and policy"
|
||||
else
|
||||
log "creating/verifying the instance-principal dynamic group and policy"
|
||||
fi
|
||||
;;
|
||||
destroy)
|
||||
[ "$DESTROY_IDENTITY" = 1 ] || return 0
|
||||
log "destroying the instance-principal Dynamic Group and IAM policy"
|
||||
;;
|
||||
*) die "internal error: unknown identity action '$action'" ;;
|
||||
esac
|
||||
identity_profile="$IDENTITY_OCI_PROFILE"
|
||||
[ -n "$identity_profile" ] || identity_profile="$OCI_PROFILE"
|
||||
if [ -n "$identity_profile" ]; then
|
||||
if [ "$action" = destroy ]; then
|
||||
OCI_CLI_PROFILE="$identity_profile" OCI_SSH_PUBLIC_KEY="$SSH_PUBLIC_KEY" \
|
||||
"$HERE/setup-identity.sh" --destroy
|
||||
elif [ "$DRY_RUN" = 1 ]; then
|
||||
OCI_CLI_PROFILE="$identity_profile" OCI_SSH_PUBLIC_KEY="$SSH_PUBLIC_KEY" \
|
||||
"$HERE/setup-identity.sh" --dry-run
|
||||
else
|
||||
OCI_CLI_PROFILE="$identity_profile" OCI_SSH_PUBLIC_KEY="$SSH_PUBLIC_KEY" \
|
||||
"$HERE/setup-identity.sh"
|
||||
fi
|
||||
else
|
||||
if [ "$action" = destroy ]; then
|
||||
OCI_SSH_PUBLIC_KEY="$SSH_PUBLIC_KEY" "$HERE/setup-identity.sh" --destroy
|
||||
elif [ "$DRY_RUN" = 1 ]; then
|
||||
OCI_SSH_PUBLIC_KEY="$SSH_PUBLIC_KEY" "$HERE/setup-identity.sh" --dry-run
|
||||
else
|
||||
OCI_SSH_PUBLIC_KEY="$SSH_PUBLIC_KEY" "$HERE/setup-identity.sh"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
ssh_node() {
|
||||
local host="$1"
|
||||
shift
|
||||
@@ -274,6 +434,31 @@ record() {
|
||||
printf '%s\t%s\t%s\n' "$gate" "$status" "$detail" >> "$RESULTS_TSV"
|
||||
}
|
||||
|
||||
# Checkpoints are deliberately local to the validation state directory. They
|
||||
# let a retry continue after a transient failure without treating a marker by
|
||||
# itself as proof: the corresponding host state is checked before it is reused.
|
||||
stage_done() { [ -f "$STATE_DIR/stage-$1.done" ]; }
|
||||
mark_stage() { : > "$STATE_DIR/stage-$1.done"; }
|
||||
|
||||
formation_complete() {
|
||||
local host
|
||||
stage_done formation || return 1
|
||||
for host in "${HOSTS[@]}"; do
|
||||
ssh_node "$host" 'test -s /etc/spinifex/spinifex.toml' || return 1
|
||||
done
|
||||
}
|
||||
|
||||
pool_complete() {
|
||||
local host
|
||||
stage_done pool || return 1
|
||||
for host in "${HOSTS[@]}"; do
|
||||
ssh_node "$host" 'grep -q imds_host_meta_ip /etc/spinifex/spinifex.toml' || return 1
|
||||
if [ "$SKIP_POOL" != 1 ]; then
|
||||
ssh_node "$host" "grep -q 'name = \"oci-public\"' /etc/spinifex/spinifex.toml" || return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# The driver's per-workbook lines, which are go test's own shape because
|
||||
# go-junit-report consumes them downstream. A workbook the driver never reached
|
||||
# is absent from its log and so absent here, which the summary renders as a row
|
||||
@@ -303,7 +488,11 @@ destroy_topology() {
|
||||
cleanup() {
|
||||
local rc=$?
|
||||
if [ "$KEEP" = 1 ]; then
|
||||
log "--keep: leaving the infrastructure up, no verdict recorded"
|
||||
if [ "$EXISTING_INFRA" = 1 ]; then
|
||||
log "existing infrastructure mode: leaving infrastructure untouched"
|
||||
else
|
||||
log "--keep: leaving the infrastructure up, no verdict recorded"
|
||||
fi
|
||||
# The command to get in, printed rather than left to be assembled from an
|
||||
# address elsewhere in the log. HOSTS is empty if we never got as far as
|
||||
# the apply, which is why this is guarded rather than assumed.
|
||||
@@ -313,7 +502,8 @@ cleanup() {
|
||||
local key="$SSH_PRIVATE_KEY"
|
||||
case "$key" in "$HOME"/*) key="~${key#"$HOME"}" ;; esac
|
||||
log "ssh in with: ssh -i $key ubuntu@${HOSTS[0]}"
|
||||
log "destroy it with: $0 --topology $TOPOLOGY --destroy-only"
|
||||
[ "$EXISTING_INFRA" = 1 ] || \
|
||||
log "destroy it with: $0 --topology $TOPOLOGY --destroy-only"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
@@ -348,6 +538,7 @@ if [ "$DESTROY_ONLY" = 1 ]; then
|
||||
log "destroy only: whatever $STATE_DIR/terraform.tfstate holds"
|
||||
destroy_topology
|
||||
[ "$DESTROY_RC" = 0 ] || exit 1
|
||||
identity_resources destroy
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -368,7 +559,7 @@ log "$SHAPE, $NODES node(s), state in $STATE_DIR"
|
||||
# Checked here, not where the hook is run: nothing about it depends on the apply, and
|
||||
# the run is otherwise forty minutes and a bare-metal bill from discovering that the
|
||||
# path in a CI variable does not exist on this runner.
|
||||
if [ "$SKIP_POOL" != 1 ] && [ "$PRINCIPAL_MODE" = off ]; then
|
||||
if [ "$EXISTING_INFRA" != 1 ] && [ "$SKIP_POOL" != 1 ] && [ "$PRINCIPAL_MODE" = off ]; then
|
||||
[ -n "$CREDENTIAL_HOOK" ] \
|
||||
|| die "this deployment authenticates with an API key and no credential hook is set, so no node could reach the OCI API; set --credential-hook or OCI_CREDENTIAL_HOOK, or use an instance principal"
|
||||
[ -x "$CREDENTIAL_HOOK" ] || die "credential hook is not executable: $CREDENTIAL_HOOK"
|
||||
@@ -409,7 +600,25 @@ assert_no_tfvars_override() {
|
||||
}
|
||||
assert_no_tfvars_override
|
||||
|
||||
if [ "$EXISTING_INFRA" = 1 ]; then
|
||||
load_existing_infrastructure
|
||||
fi
|
||||
identity_resources create
|
||||
|
||||
if [ "$SKIP_POOL" != 1 ] && [ "$PRINCIPAL_MODE" = off ]; then
|
||||
[ -n "$CREDENTIAL_HOOK" ] \
|
||||
|| die "this deployment authenticates with an API key and no credential hook is set, so no node could reach the OCI API; set --credential-hook or OCI_CREDENTIAL_HOOK, or use an instance principal"
|
||||
[ -x "$CREDENTIAL_HOOK" ] || die "credential hook is not executable: $CREDENTIAL_HOOK"
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" = 1 ]; then
|
||||
if [ "$EXISTING_INFRA" = 1 ]; then
|
||||
plan="install Spinifex on $NODES existing node(s), form the cluster"
|
||||
[ "$SKIP_POOL" = 1 ] && plan="$plan, configure no external pool"
|
||||
[ "$SKIP_WORKLOAD" = 1 ] && plan="$plan, launch no guests"
|
||||
log "dry run: would $plan; Terraform apply and destroy are skipped"
|
||||
exit 0
|
||||
fi
|
||||
# Whole plan to a file, summary to the terminal. A dry run is here to catch a
|
||||
# plan aimed at the wrong compartment, and `tail` dropped the resource count
|
||||
# and every compartment and shape line along with it, leaving only the outputs
|
||||
@@ -439,29 +648,26 @@ fi
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
tf init -input=false > "$STATE_DIR/init.log" 2>&1 || die "terraform init failed; see $STATE_DIR/init.log"
|
||||
if [ "$EXISTING_INFRA" = 1 ]; then
|
||||
record build SKIP "existing infrastructure"
|
||||
else
|
||||
tf init -input=false > "$STATE_DIR/init.log" 2>&1 || die "terraform init failed; see $STATE_DIR/init.log"
|
||||
|
||||
log "building"
|
||||
tf apply -auto-approve -no-color "${tf_vars[@]}" > "$STATE_DIR/apply.log" 2>&1 \
|
||||
|| die "terraform apply failed; see $STATE_DIR/apply.log"
|
||||
record build PASS
|
||||
log "building"
|
||||
tf apply -auto-approve -no-color "${tf_vars[@]}" > "$STATE_DIR/apply.log" 2>&1 \
|
||||
|| die "terraform apply failed; see $STATE_DIR/apply.log"
|
||||
record build PASS
|
||||
|
||||
# A read loop rather than mapfile, which bash 3.2 on macOS does not have. The
|
||||
# output is captured first so a failed terraform fails here, not silently as an
|
||||
# empty host list.
|
||||
hosts_raw=$(tf output -raw "${tf_state[@]}" hosts_file) \
|
||||
|| die "could not read the hosts_file output from $STATE_DIR/terraform.tfstate"
|
||||
HOSTS=()
|
||||
while IFS= read -r line; do
|
||||
if [ -n "$line" ]; then HOSTS+=("$line"); fi
|
||||
done <<EOF
|
||||
$hosts_raw
|
||||
EOF
|
||||
[ "${#HOSTS[@]}" -ge 1 ] || die "the hosts_file output named no hosts, so the apply built nothing to install on"
|
||||
for host in "${HOSTS[@]}"; do
|
||||
[[ "$host" =~ ^[0-9]+(\.[0-9]+){3}$ ]] \
|
||||
|| die "the hosts_file output is not an address: '$host' -- something on the wrapper's stdout is in the capture"
|
||||
done
|
||||
hosts_raw=$(tf output -raw "${tf_state[@]}" hosts_file) \
|
||||
|| die "could not read hosts_file from $STATE_DIR/terraform.tfstate"
|
||||
load_hosts "$hosts_raw" "hosts_file from $STATE_DIR/terraform.tfstate"
|
||||
SHAPE=$(tf output -raw "${tf_state[@]}" compute_shape) \
|
||||
|| die "could not read compute_shape from $STATE_DIR/terraform.tfstate"
|
||||
built_principal=$(tf output -raw "${tf_state[@]}" instance_principal) \
|
||||
|| die "could not read instance_principal from $STATE_DIR/terraform.tfstate"
|
||||
[ "$built_principal" = "$PRINCIPAL_MODE" ] \
|
||||
|| die "read instance_principal='$PRINCIPAL_MODE' from the tfvars and the apply built '$built_principal'; set it in one place"
|
||||
fi
|
||||
|
||||
# Written for every topology, not just the multi-node one that passes it to
|
||||
# install-node.sh: it is also how an operator gets the addresses afterwards
|
||||
@@ -474,18 +680,6 @@ if [ "${#HOSTS[@]}" != "$NODES" ]; then
|
||||
log "note: $NODES node(s) is the $TOPOLOGY default, and the apply built ${#HOSTS[@]}; taking the apply's"
|
||||
NODES="${#HOSTS[@]}"
|
||||
fi
|
||||
SHAPE=$(tf output -raw "${tf_state[@]}" compute_shape) \
|
||||
|| die "could not read the compute_shape output from $STATE_DIR/terraform.tfstate"
|
||||
|
||||
# Terraform is the authority on which credential was built, and the reader above is
|
||||
# a guess made before the apply. They have to agree: the hook is skipped on the
|
||||
# strength of that guess, and a node left with no credential at all would otherwise
|
||||
# fail much later as an allocator that cannot resolve its VNIC.
|
||||
built_principal=$(tf output -raw "${tf_state[@]}" instance_principal) \
|
||||
|| die "could not read the instance_principal output from $STATE_DIR/terraform.tfstate"
|
||||
[ "$built_principal" = "$PRINCIPAL_MODE" ] \
|
||||
|| die "read instance_principal='$PRINCIPAL_MODE' from the tfvars and the apply built '$built_principal'; set it in one place"
|
||||
|
||||
log "hosts: ${HOSTS[*]}"
|
||||
log "$SHAPE, $NODES node(s), oci_auth $([ "$PRINCIPAL_MODE" = off ] && echo "from a key file" || echo "as the instance principal")"
|
||||
|
||||
@@ -519,6 +713,15 @@ record "cloud-init units" PASS
|
||||
# guide gets today. A nightly judging a branch must pass --distro.
|
||||
log "installing Spinifex on each node ($([ -n "$DISTRO" ] && echo "$(basename "$DISTRO")" || echo "published release"))"
|
||||
for host in "${HOSTS[@]}"; do
|
||||
# A retry after OVN formation must not re-run setup.sh: the installer invokes
|
||||
# setup-ovn.sh in standalone mode, which is correct on a new host but would
|
||||
# replace the clustered NB/SB client configuration we are trying to resume.
|
||||
# An explicit --reinstall remains available for a deliberate package upgrade.
|
||||
if [ "$REINSTALL" = 0 ] && ssh_node "$host" 'command -v spx >/dev/null && test -x /usr/local/share/spinifex/setup-ovn.sh'; then
|
||||
INSTALLED_VERSION="$(ssh_node "$host" 'spx version' 2>&1 | head -1)"
|
||||
log "$host already installed: $INSTALLED_VERSION (resuming; installer skipped)"
|
||||
continue
|
||||
fi
|
||||
if [ -n "$DISTRO" ]; then
|
||||
scp -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
-o LogLevel=ERROR -q "$DISTRO" "$SETUP_SH" "ubuntu@$host:/tmp/" \
|
||||
@@ -568,7 +771,10 @@ record install PASS "$INSTALLED_VERSION"
|
||||
# A single node is its own documented path: install-node.sh refuses fewer than two
|
||||
# hosts, because there is nothing to join. Both branches use the flags the guide
|
||||
# names, so what is validated is what the guide tells a customer to type.
|
||||
if [ "$NODES" = 1 ]; then
|
||||
if formation_complete; then
|
||||
log "cluster formation already completed (resuming; formation skipped)"
|
||||
record formation SKIP "checkpoint verified"
|
||||
elif [ "$NODES" = 1 ]; then
|
||||
log "initializing the single node"
|
||||
ssh_node "${HOSTS[0]}" '
|
||||
set -e
|
||||
@@ -578,6 +784,8 @@ if [ "$NODES" = 1 ]; then
|
||||
sudo systemctl start spinifex.target
|
||||
' > "$STATE_DIR/form.log" 2>&1 \
|
||||
|| die "single-node init failed; see $STATE_DIR/form.log"
|
||||
mark_stage formation
|
||||
record formation PASS
|
||||
else
|
||||
log "forming the cluster"
|
||||
"$REPO_ROOT/scripts/install-node.sh" \
|
||||
@@ -588,29 +796,34 @@ else
|
||||
--ipsec off \
|
||||
--yes > "$STATE_DIR/form.log" 2>&1 \
|
||||
|| die "formation failed; see $STATE_DIR/form.log"
|
||||
mark_stage formation
|
||||
record formation PASS
|
||||
fi
|
||||
record formation PASS
|
||||
|
||||
# Runs after formation and before the pool is configured, which is the only window
|
||||
# where a node has /etc/spinifex but has not yet started the allocator. Nothing in
|
||||
# this repository knows what it does: an API-key deployment needs a credential on
|
||||
# each node, and a credential belongs to the operator, not to a checked-in script.
|
||||
# Skipped under instance principal, which needs no handoff at all.
|
||||
if [ "$SKIP_POOL" != 1 ] && [ "$PRINCIPAL_MODE" = off ]; then
|
||||
log "running the credential hook"
|
||||
# Arguments, not a file: the hook is told where the nodes are and how to reach
|
||||
# them, and decides for itself what to put there.
|
||||
"$CREDENTIAL_HOOK" "$SSH_PRIVATE_KEY" "${HOSTS[@]}" > "$STATE_DIR/credential-hook.log" 2>&1 \
|
||||
|| die "the credential hook failed; see $STATE_DIR/credential-hook.log"
|
||||
log "credential hook ok"
|
||||
fi
|
||||
if pool_complete; then
|
||||
log "OCI pool configuration already completed (resuming; pool setup skipped)"
|
||||
record pool SKIP "checkpoint verified"
|
||||
else
|
||||
if [ "$SKIP_POOL" != 1 ] && [ "$PRINCIPAL_MODE" = off ]; then
|
||||
log "running the credential hook"
|
||||
# Arguments, not a file: the hook is told where the nodes are and how to reach
|
||||
# them, and decides for itself what to put there.
|
||||
"$CREDENTIAL_HOOK" "$SSH_PRIVATE_KEY" "${HOSTS[@]}" > "$STATE_DIR/credential-hook.log" 2>&1 \
|
||||
|| die "the credential hook failed; see $STATE_DIR/credential-hook.log"
|
||||
log "credential hook ok"
|
||||
fi
|
||||
|
||||
# The IMDS remap and the pool are both set-before-first-start, and the remap is the
|
||||
# half that is invisible when missing: without it the cloud's metadata service is
|
||||
# shadowed by Spinifex's own endpoints and the allocator cannot resolve its VNIC.
|
||||
log "configuring the IMDS remap$([ "$SKIP_POOL" = 1 ] && echo " (no external pool)" || echo " and the external pool")"
|
||||
for host in "${HOSTS[@]}"; do
|
||||
ssh_node "$host" "SKIP_POOL=$SKIP_POOL bash -s" > "$STATE_DIR/pool-$host.log" 2>&1 <<'REMOTE' \
|
||||
# The IMDS remap and the pool are both set-before-first-start, and the remap is the
|
||||
# half that is invisible when missing: without it the cloud's metadata service is
|
||||
# shadowed by Spinifex's own endpoints and the allocator cannot resolve its VNIC.
|
||||
log "configuring the IMDS remap$([ "$SKIP_POOL" = 1 ] && echo " (no external pool)" || echo " and the external pool")"
|
||||
for host in "${HOSTS[@]}"; do
|
||||
ssh_node "$host" "SKIP_POOL=$SKIP_POOL PRINCIPAL_MODE=$PRINCIPAL_MODE bash -s" > "$STATE_DIR/pool-$host.log" 2>&1 <<'REMOTE' \
|
||||
|| die "pool configuration failed on $host; see $STATE_DIR/pool-$host.log"
|
||||
set -e
|
||||
sudo cp /etc/spinifex/spinifex.toml /etc/spinifex/spinifex.toml.bak-prepool
|
||||
@@ -628,6 +841,11 @@ if n != 1:
|
||||
open(path, "w").write(text)
|
||||
PY
|
||||
if [ "${SKIP_POOL:-0}" != 1 ]; then
|
||||
if [ "${PRINCIPAL_MODE:-off}" = adopt ]; then
|
||||
grep -q 'oci_auth[[:space:]]*=[[:space:]]*"instance_principal"' \
|
||||
/etc/spinifex/oci/external-pool.toml \
|
||||
|| { echo "instance-principal mode needs oci_auth = \\"instance_principal\\" in /etc/spinifex/oci/external-pool.toml"; exit 1; }
|
||||
fi
|
||||
grep -q 'name = "oci-public"' /etc/spinifex/spinifex.toml \
|
||||
|| sudo tee -a /etc/spinifex/spinifex.toml < /etc/spinifex/oci/external-pool.toml >/dev/null
|
||||
fi
|
||||
@@ -640,7 +858,10 @@ sudo python3 -c 'import sys, tomllib; tomllib.load(open(sys.argv[1], "rb"))' \
|
||||
|| { echo "spinifex.toml is not valid TOML after the IMDS remap and the pool append"; exit 1; }
|
||||
sudo systemctl restart spinifex.target
|
||||
REMOTE
|
||||
done
|
||||
done
|
||||
mark_stage pool
|
||||
record pool PASS
|
||||
fi
|
||||
|
||||
# Everything needed to tell the three allocator faults apart, and deliberately no
|
||||
# credential: the config file and the PEM are reported as mode and size only, which
|
||||
@@ -733,6 +954,12 @@ cat "$STATE_DIR/nodes.txt"
|
||||
[ "$ready" = "$NODES" ] || die "expected $NODES Ready node(s), got $ready; see $STATE_DIR/nodes.txt"
|
||||
record membership PASS "$ready Ready"
|
||||
|
||||
if [ "$SHOW_AWS_CREDENTIALS" = 1 ]; then
|
||||
log "AWS credentials from node 1 (${HOSTS[0]}) — SENSITIVE: do not paste into logs or chat"
|
||||
ssh_node "${HOSTS[0]}" 'test -r ~/.aws/credentials && cat ~/.aws/credentials' \
|
||||
|| die "node 1 has no readable ~/.aws/credentials"
|
||||
fi
|
||||
|
||||
if [ "$SKIP_WORKLOAD" = 1 ]; then
|
||||
log "--skip-workload: stopping before the workbook"
|
||||
record workbooks SKIPPED "--skip-workload"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/spinifex/internal/eksgw"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
)
|
||||
|
||||
// Config is the webhook's runtime configuration; the binary sources it from
|
||||
@@ -103,20 +103,20 @@ type tokenReviewRequest struct {
|
||||
// the gateway and decode the resolved identity. A non-2xx / transport error is
|
||||
// surfaced (handle() turns it into a 5xx so the apiserver retries) rather than a
|
||||
// silent deny, distinguishing "webhook broker down" from "token rejected".
|
||||
func gatewayReviewer(client *eksgw.Client, clusterName, accountID string) func(string) (handlers_eks.WebhookTokenReviewResult, error) {
|
||||
func gatewayReviewer(client *eksgw.Client, clusterName, accountID string) func(string) (ekscluster.WebhookTokenReviewResult, error) {
|
||||
path := "/clusters/" + clusterName + "/token-review"
|
||||
return func(token string) (handlers_eks.WebhookTokenReviewResult, error) {
|
||||
return func(token string) (ekscluster.WebhookTokenReviewResult, error) {
|
||||
body, err := json.Marshal(tokenReviewRequest{AccountID: accountID, Token: token})
|
||||
if err != nil {
|
||||
return handlers_eks.WebhookTokenReviewResult{}, fmt.Errorf("marshal request: %w", err)
|
||||
return ekscluster.WebhookTokenReviewResult{}, fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
respBody, err := client.Post(path, body)
|
||||
if err != nil {
|
||||
return handlers_eks.WebhookTokenReviewResult{}, err
|
||||
return ekscluster.WebhookTokenReviewResult{}, err
|
||||
}
|
||||
var res handlers_eks.WebhookTokenReviewResult
|
||||
var res ekscluster.WebhookTokenReviewResult
|
||||
if err := json.Unmarshal(respBody, &res); err != nil {
|
||||
return handlers_eks.WebhookTokenReviewResult{}, fmt.Errorf("decode response: %w", err)
|
||||
return ekscluster.WebhookTokenReviewResult{}, fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func gatewayReviewer(client *eksgw.Client, clusterName, accountID string) func(s
|
||||
type authenticator struct {
|
||||
accountID string
|
||||
clusterName string
|
||||
review func(token string) (handlers_eks.WebhookTokenReviewResult, error)
|
||||
review func(token string) (ekscluster.WebhookTokenReviewResult, error)
|
||||
}
|
||||
|
||||
func (a *authenticator) handle(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -43,8 +43,8 @@ func TestHandle_GrantsAndStampsAudiences(t *testing.T) {
|
||||
a := &authenticator{
|
||||
accountID: "111122223333",
|
||||
clusterName: "alpha",
|
||||
review: func(string) (handlers_eks.WebhookTokenReviewResult, error) {
|
||||
return handlers_eks.WebhookTokenReviewResult{
|
||||
review: func(string) (ekscluster.WebhookTokenReviewResult, error) {
|
||||
return ekscluster.WebhookTokenReviewResult{
|
||||
Authenticated: true,
|
||||
Username: "arn:aws:iam::111122223333:role/admin",
|
||||
UID: "AROAEXAMPLE:session",
|
||||
@@ -65,8 +65,8 @@ func TestHandle_GrantsAndStampsAudiences(t *testing.T) {
|
||||
func TestHandle_DeniesWithoutStampingAudiences(t *testing.T) {
|
||||
a := &authenticator{
|
||||
clusterName: "alpha",
|
||||
review: func(string) (handlers_eks.WebhookTokenReviewResult, error) {
|
||||
return handlers_eks.WebhookTokenReviewResult{Authenticated: false}, nil
|
||||
review: func(string) (ekscluster.WebhookTokenReviewResult, error) {
|
||||
return ekscluster.WebhookTokenReviewResult{Authenticated: false}, nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ func TestHandle_DeniesWithoutStampingAudiences(t *testing.T) {
|
||||
func TestHandle_BrokerFaultReturns5xx(t *testing.T) {
|
||||
a := &authenticator{
|
||||
clusterName: "alpha",
|
||||
review: func(string) (handlers_eks.WebhookTokenReviewResult, error) {
|
||||
return handlers_eks.WebhookTokenReviewResult{}, errors.New("gateway returned 503")
|
||||
review: func(string) (ekscluster.WebhookTokenReviewResult, error) {
|
||||
return ekscluster.WebhookTokenReviewResult{}, errors.New("gateway returned 503")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package lbagent implements the LB config agent that runs inside load balancer VMs.
|
||||
// Package elbv2 implements the LB config agent that runs inside load balancer VMs.
|
||||
// It polls the AWS gateway for config updates and reports health via heartbeats.
|
||||
// All communication uses SigV4-signed HTTP requests to the gateway.
|
||||
package lbagent
|
||||
package elbv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -57,7 +57,7 @@ const (
|
||||
registrationGrace = 60 * time.Second
|
||||
)
|
||||
|
||||
// Data-plane engine names (duplicated from handlers/elbv2 to avoid an import cycle).
|
||||
// Data-plane engine names (duplicated from domains/elbv2 to avoid an import cycle).
|
||||
const (
|
||||
EngineHAProxy = "haproxy"
|
||||
EngineNginx = "nginx"
|
||||
@@ -1,4 +1,4 @@
|
||||
package lbagent
|
||||
package elbv2
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -1,4 +1,4 @@
|
||||
package lbagent
|
||||
package elbv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package lbagent
|
||||
package elbv2
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -1,4 +1,4 @@
|
||||
package lbagent
|
||||
package elbv2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package lbagent
|
||||
package elbv2
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@@ -1,4 +1,4 @@
|
||||
package lbagent
|
||||
package elbv2
|
||||
|
||||
import (
|
||||
"net"
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
|
||||
"github.com/mulgadc/spinifex/internal/gwsign"
|
||||
"github.com/mulgadc/spinifex/internal/rdsgw"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/telemetry"
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
)
|
||||
|
||||
// The gateway resolves the authoritative DB instance from the request's
|
||||
@@ -30,15 +30,15 @@ type identity struct {
|
||||
// Every method is a SigV4-signed Query request, so the NATS bus stays
|
||||
// host-internal.
|
||||
type controlPlane interface {
|
||||
Register(ctx context.Context, id identity) (*handlers_rds.RegisterDBInstanceOutput, error)
|
||||
SubmitState(ctx context.Context, id identity, health handlers_rds.EngineHealth, message string) (*handlers_rds.SubmitDBStateChangeOutput, error)
|
||||
Register(ctx context.Context, id identity) (*rdsinstance.RegisterDBInstanceOutput, error)
|
||||
SubmitState(ctx context.Context, id identity, health rdsinstance.EngineHealth, message string) (*rdsinstance.SubmitDBStateChangeOutput, error)
|
||||
// Returns Mode=initialize with the master password for as long as a payload
|
||||
// staged for this VM generation is unacknowledged, and Mode=attach after.
|
||||
GetBootstrapConfig(ctx context.Context, id identity) (*handlers_rds.GetDBBootstrapConfigOutput, error)
|
||||
GetBootstrapConfig(ctx context.Context, id identity) (*rdsinstance.GetDBBootstrapConfigOutput, error)
|
||||
// Reports that PostgreSQL durably applied the staged password, which is what
|
||||
// lets the control plane destroy it.
|
||||
AcknowledgeBootstrap(ctx context.Context, id identity, pending *pendingBootstrap) error
|
||||
PollCommands(ctx context.Context, id identity, replies []handlers_rds.CommandReply, wait time.Duration) ([]handlers_rds.Command, error)
|
||||
PollCommands(ctx context.Context, id identity, replies []rdsinstance.CommandReply, wait time.Duration) ([]rdsinstance.Command, error)
|
||||
}
|
||||
|
||||
// Bounds a register, heartbeat or bootstrap request. The long poll sets its own
|
||||
@@ -71,7 +71,7 @@ func newGatewayControlPlane(cfg Config) (*gatewayControlPlane, error) {
|
||||
return &gatewayControlPlane{client: client}, nil
|
||||
}
|
||||
|
||||
func (g *gatewayControlPlane) Register(ctx context.Context, id identity) (*handlers_rds.RegisterDBInstanceOutput, error) {
|
||||
func (g *gatewayControlPlane) Register(ctx context.Context, id identity) (*rdsinstance.RegisterDBInstanceOutput, error) {
|
||||
params := url.Values{}
|
||||
setIfPresent(params, "DBInstanceIdentifier", id.DBInstanceIdentifier)
|
||||
setIfPresent(params, "AgentVersion", id.AgentVersion)
|
||||
@@ -80,14 +80,14 @@ func (g *gatewayControlPlane) Register(ctx context.Context, id identity) (*handl
|
||||
ctx, cancel := context.WithTimeout(ctx, callTimeout)
|
||||
defer cancel()
|
||||
|
||||
var out handlers_rds.RegisterDBInstanceOutput
|
||||
var out rdsinstance.RegisterDBInstanceOutput
|
||||
if err := g.client.Call(ctx, "RegisterDBInstance", params, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (g *gatewayControlPlane) SubmitState(ctx context.Context, id identity, health handlers_rds.EngineHealth, message string) (*handlers_rds.SubmitDBStateChangeOutput, error) {
|
||||
func (g *gatewayControlPlane) SubmitState(ctx context.Context, id identity, health rdsinstance.EngineHealth, message string) (*rdsinstance.SubmitDBStateChangeOutput, error) {
|
||||
params := url.Values{"EngineHealth": {string(health)}}
|
||||
setIfPresent(params, "DBInstanceIdentifier", id.DBInstanceIdentifier)
|
||||
setIfPresent(params, "EngineVersion", id.EngineVersion)
|
||||
@@ -96,21 +96,21 @@ func (g *gatewayControlPlane) SubmitState(ctx context.Context, id identity, heal
|
||||
ctx, cancel := context.WithTimeout(ctx, callTimeout)
|
||||
defer cancel()
|
||||
|
||||
var out handlers_rds.SubmitDBStateChangeOutput
|
||||
var out rdsinstance.SubmitDBStateChangeOutput
|
||||
if err := g.client.Call(ctx, "SubmitDBStateChange", params, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (g *gatewayControlPlane) GetBootstrapConfig(ctx context.Context, id identity) (*handlers_rds.GetDBBootstrapConfigOutput, error) {
|
||||
func (g *gatewayControlPlane) GetBootstrapConfig(ctx context.Context, id identity) (*rdsinstance.GetDBBootstrapConfigOutput, error) {
|
||||
params := url.Values{}
|
||||
setIfPresent(params, "DBInstanceIdentifier", id.DBInstanceIdentifier)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, callTimeout)
|
||||
defer cancel()
|
||||
|
||||
var out handlers_rds.GetDBBootstrapConfigOutput
|
||||
var out rdsinstance.GetDBBootstrapConfigOutput
|
||||
if err := g.client.Call(ctx, "GetDBBootstrapConfig", params, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -128,7 +128,7 @@ func (g *gatewayControlPlane) AcknowledgeBootstrap(ctx context.Context, id ident
|
||||
ctx, cancel := context.WithTimeout(ctx, callTimeout)
|
||||
defer cancel()
|
||||
|
||||
var out handlers_rds.AcknowledgeDBBootstrapOutput
|
||||
var out rdsinstance.AcknowledgeDBBootstrapOutput
|
||||
if err := g.client.Call(ctx, "AcknowledgeDBBootstrap", params, &out); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -140,10 +140,10 @@ func (g *gatewayControlPlane) AcknowledgeBootstrap(ctx context.Context, id ident
|
||||
|
||||
// Declared here so the in-guest binary does not link the server side of the API.
|
||||
type pollDBCommandsOutput struct {
|
||||
Commands []handlers_rds.Command `xml:"Commands>member"`
|
||||
Commands []rdsinstance.Command `xml:"Commands>member"`
|
||||
}
|
||||
|
||||
func (g *gatewayControlPlane) PollCommands(ctx context.Context, id identity, replies []handlers_rds.CommandReply, wait time.Duration) ([]handlers_rds.Command, error) {
|
||||
func (g *gatewayControlPlane) PollCommands(ctx context.Context, id identity, replies []rdsinstance.CommandReply, wait time.Duration) ([]rdsinstance.Command, error) {
|
||||
params := url.Values{"WaitTimeSeconds": {strconv.FormatInt(int64(wait.Seconds()), 10)}}
|
||||
setIfPresent(params, "DBInstanceIdentifier", id.DBInstanceIdentifier)
|
||||
// Replies ride the poll rather than a separate action, so replying and
|
||||
@@ -179,7 +179,7 @@ type Agent struct {
|
||||
cp controlPlane
|
||||
probe *engineProbe
|
||||
engine engine
|
||||
handoffWriter func(string, *handlers_rds.GetDBBootstrapConfigOutput) error
|
||||
handoffWriter func(string, *rdsinstance.GetDBBootstrapConfigOutput) error
|
||||
dataMountWaiter func(context.Context, string, string) error
|
||||
|
||||
// Set by bootstrap when the fetch replayed a staged payload; nil leaves the
|
||||
@@ -231,13 +231,13 @@ func newAgent(cfg Config, cp controlPlane, probe *engineProbe) (*Agent, error) {
|
||||
}
|
||||
a := &Agent{
|
||||
cfg: cfg, id: id, cp: cp, probe: probe,
|
||||
handoffWriter: func(dir string, bootCfg *handlers_rds.GetDBBootstrapConfigOutput) error {
|
||||
handoffWriter: func(dir string, bootCfg *rdsinstance.GetDBBootstrapConfigOutput) error {
|
||||
return writeHandoff(dir, bootCfg, catalog.OptionFileName)
|
||||
},
|
||||
dataMountWaiter: waitForDataMount,
|
||||
}
|
||||
a.engine = eng
|
||||
a.hb = newHeartbeater(cp, probe, a.engine, handlers_rds.HeartbeatInterval)
|
||||
a.hb = newHeartbeater(cp, probe, a.engine, rdsinstance.HeartbeatInterval)
|
||||
a.cmd = newCommander(cp, newCommandRegistry(a.engine, newGuestStorage(cfg, execCommandRunner)), cfg.PollWait)
|
||||
a.guard = newParamGuard(a.engine, probe, cp)
|
||||
return a, nil
|
||||
@@ -329,7 +329,7 @@ func (a *Agent) refuseEngineMismatch(ctx context.Context) error {
|
||||
slog.ErrorContext(ctx, "rds-agent: "+message)
|
||||
// Reported before returning, so the control plane learns why the instance
|
||||
// never came up rather than only that it never did.
|
||||
if _, err := a.cp.SubmitState(ctx, a.id, handlers_rds.EngineHealthUnhealthy, message); err != nil {
|
||||
if _, err := a.cp.SubmitState(ctx, a.id, rdsinstance.EngineHealthUnhealthy, message); err != nil {
|
||||
slog.ErrorContext(ctx, "rds-agent: reporting the engine mismatch failed", "err", err)
|
||||
}
|
||||
return errors.New(message)
|
||||
|
||||
@@ -16,10 +16,10 @@ import (
|
||||
|
||||
"github.com/mulgadc/spinifex/internal/gwsign"
|
||||
"github.com/mulgadc/spinifex/internal/rdsgw"
|
||||
rdsawsapi "github.com/mulgadc/spinifex/spinifex/domains/rds/awsapi"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
awsxml "github.com/mulgadc/spinifex/spinifex/foundation/aws/xml"
|
||||
gateway_rds "github.com/mulgadc/spinifex/spinifex/gateway/rds"
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -29,33 +29,33 @@ import (
|
||||
type fakeControlPlane struct {
|
||||
mu sync.Mutex
|
||||
|
||||
registerOut *handlers_rds.RegisterDBInstanceOutput
|
||||
registerOut *rdsinstance.RegisterDBInstanceOutput
|
||||
registerErrs []error
|
||||
registerReqs []identity
|
||||
|
||||
bootstrapOut *handlers_rds.GetDBBootstrapConfigOutput
|
||||
bootstrapOut *rdsinstance.GetDBBootstrapConfigOutput
|
||||
bootstrapErrs []error
|
||||
// Returned once bootstrapErrs is drained, for a failure no retry heals.
|
||||
bootstrapErr error
|
||||
bootstrapReqs []identity
|
||||
// Scripted responses ahead of bootstrapOut, so a test can answer the first
|
||||
// fetch differently from the ones the agent retries with.
|
||||
bootstrapQueue []*handlers_rds.GetDBBootstrapConfigOutput
|
||||
bootstrapQueue []*rdsinstance.GetDBBootstrapConfigOutput
|
||||
|
||||
ackErrs []error
|
||||
ackReqs []pendingBootstrap
|
||||
|
||||
states []submittedState
|
||||
|
||||
pollReplies [][]handlers_rds.CommandReply
|
||||
pollQueue [][]handlers_rds.Command
|
||||
pollReplies [][]rdsinstance.CommandReply
|
||||
pollQueue [][]rdsinstance.Command
|
||||
pollErr error
|
||||
polled chan struct{}
|
||||
}
|
||||
|
||||
type submittedState struct {
|
||||
id identity
|
||||
health handlers_rds.EngineHealth
|
||||
health rdsinstance.EngineHealth
|
||||
message string
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ var _ controlPlane = (*fakeControlPlane)(nil)
|
||||
|
||||
func newFakeControlPlane() *fakeControlPlane {
|
||||
return &fakeControlPlane{
|
||||
registerOut: &handlers_rds.RegisterDBInstanceOutput{
|
||||
registerOut: &rdsinstance.RegisterDBInstanceOutput{
|
||||
DBInstanceIdentifier: "db-resolved", HeartbeatIntervalSeconds: 30,
|
||||
},
|
||||
polled: make(chan struct{}, 64),
|
||||
@@ -81,7 +81,7 @@ func nextErr(errs *[]error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *fakeControlPlane) Register(_ context.Context, id identity) (*handlers_rds.RegisterDBInstanceOutput, error) {
|
||||
func (f *fakeControlPlane) Register(_ context.Context, id identity) (*rdsinstance.RegisterDBInstanceOutput, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.registerReqs = append(f.registerReqs, id)
|
||||
@@ -91,16 +91,16 @@ func (f *fakeControlPlane) Register(_ context.Context, id identity) (*handlers_r
|
||||
return f.registerOut, nil
|
||||
}
|
||||
|
||||
func (f *fakeControlPlane) SubmitState(_ context.Context, id identity, health handlers_rds.EngineHealth, message string) (*handlers_rds.SubmitDBStateChangeOutput, error) {
|
||||
func (f *fakeControlPlane) SubmitState(_ context.Context, id identity, health rdsinstance.EngineHealth, message string) (*rdsinstance.SubmitDBStateChangeOutput, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.states = append(f.states, submittedState{id: id, health: health, message: message})
|
||||
// Zero means "keep the cadence you have", so a test's fast interval is not
|
||||
// reset by every beat.
|
||||
return &handlers_rds.SubmitDBStateChangeOutput{Acknowledged: true}, nil
|
||||
return &rdsinstance.SubmitDBStateChangeOutput{Acknowledged: true}, nil
|
||||
}
|
||||
|
||||
func (f *fakeControlPlane) GetBootstrapConfig(_ context.Context, id identity) (*handlers_rds.GetDBBootstrapConfigOutput, error) {
|
||||
func (f *fakeControlPlane) GetBootstrapConfig(_ context.Context, id identity) (*rdsinstance.GetDBBootstrapConfigOutput, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.bootstrapReqs = append(f.bootstrapReqs, id)
|
||||
@@ -125,11 +125,11 @@ func (f *fakeControlPlane) AcknowledgeBootstrap(_ context.Context, _ identity, p
|
||||
return nextErr(&f.ackErrs)
|
||||
}
|
||||
|
||||
func (f *fakeControlPlane) PollCommands(ctx context.Context, _ identity, replies []handlers_rds.CommandReply, _ time.Duration) ([]handlers_rds.Command, error) {
|
||||
func (f *fakeControlPlane) PollCommands(ctx context.Context, _ identity, replies []rdsinstance.CommandReply, _ time.Duration) ([]rdsinstance.Command, error) {
|
||||
f.mu.Lock()
|
||||
f.pollReplies = append(f.pollReplies, slices.Clone(replies))
|
||||
err := f.pollErr
|
||||
var out []handlers_rds.Command
|
||||
var out []rdsinstance.Command
|
||||
if len(f.pollQueue) > 0 {
|
||||
out, f.pollQueue = f.pollQueue[0], f.pollQueue[1:]
|
||||
}
|
||||
@@ -163,10 +163,10 @@ func (f *fakeControlPlane) snapshotStates() []submittedState {
|
||||
return append([]submittedState(nil), f.states...)
|
||||
}
|
||||
|
||||
func (f *fakeControlPlane) snapshotReplies() [][]handlers_rds.CommandReply {
|
||||
func (f *fakeControlPlane) snapshotReplies() [][]rdsinstance.CommandReply {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
out := make([][]handlers_rds.CommandReply, len(f.pollReplies))
|
||||
out := make([][]rdsinstance.CommandReply, len(f.pollReplies))
|
||||
for i := range f.pollReplies {
|
||||
out[i] = slices.Clone(f.pollReplies[i])
|
||||
}
|
||||
@@ -200,12 +200,12 @@ func staticProbe(code int) probeRunner {
|
||||
return func(context.Context, string, ...string) (int, string, error) { return code, "", nil }
|
||||
}
|
||||
|
||||
func bootstrapOutput(password *string) *handlers_rds.GetDBBootstrapConfigOutput {
|
||||
mode := handlers_rds.BootstrapModeAttach
|
||||
func bootstrapOutput(password *string) *rdsinstance.GetDBBootstrapConfigOutput {
|
||||
mode := rdsinstance.BootstrapModeAttach
|
||||
if password != nil {
|
||||
mode = handlers_rds.BootstrapModeInitialize
|
||||
mode = rdsinstance.BootstrapModeInitialize
|
||||
}
|
||||
return &handlers_rds.GetDBBootstrapConfigOutput{
|
||||
return &rdsinstance.GetDBBootstrapConfigOutput{
|
||||
Mode: mode,
|
||||
DBInstanceIdentifier: "db-resolved",
|
||||
Engine: "postgres",
|
||||
@@ -369,7 +369,7 @@ func TestBootstrap_RetriesHandoffWithoutRefetching(t *testing.T) {
|
||||
a := newTestAgent(t, cfg, cp, staticProbe(0))
|
||||
|
||||
writes := 0
|
||||
a.handoffWriter = func(_ string, got *handlers_rds.GetDBBootstrapConfigOutput) error {
|
||||
a.handoffWriter = func(_ string, got *rdsinstance.GetDBBootstrapConfigOutput) error {
|
||||
writes++
|
||||
if got.MasterUserPassword == nil || *got.MasterUserPassword != password {
|
||||
t.Fatalf("handoff attempt %d lost the initialize password", writes)
|
||||
@@ -401,16 +401,16 @@ func TestBootstrap_RetriesInitializeWithNoPassword(t *testing.T) {
|
||||
cp := newFakeControlPlane()
|
||||
empty := ""
|
||||
password := "staged-secret"
|
||||
cp.bootstrapQueue = []*handlers_rds.GetDBBootstrapConfigOutput{{
|
||||
Mode: handlers_rds.BootstrapModeInitialize, DBInstanceIdentifier: "db-resolved",
|
||||
cp.bootstrapQueue = []*rdsinstance.GetDBBootstrapConfigOutput{{
|
||||
Mode: rdsinstance.BootstrapModeInitialize, DBInstanceIdentifier: "db-resolved",
|
||||
MasterUsername: "master", MasterUserPassword: &empty,
|
||||
}}
|
||||
cp.bootstrapOut = bootstrapOutput(&password)
|
||||
cfg := testConfig(t)
|
||||
a := newTestAgent(t, cfg, cp, staticProbe(0))
|
||||
|
||||
var handed []*handlers_rds.GetDBBootstrapConfigOutput
|
||||
a.handoffWriter = func(_ string, got *handlers_rds.GetDBBootstrapConfigOutput) error {
|
||||
var handed []*rdsinstance.GetDBBootstrapConfigOutput
|
||||
a.handoffWriter = func(_ string, got *rdsinstance.GetDBBootstrapConfigOutput) error {
|
||||
handed = append(handed, got)
|
||||
return nil
|
||||
}
|
||||
@@ -530,7 +530,7 @@ func TestRun_RefusesAnEngineTheImageDoesNotBake(t *testing.T) {
|
||||
// Reported rather than only logged, so the control plane learns why the
|
||||
// instance never came up rather than only that it never did.
|
||||
states := cp.snapshotStates()
|
||||
if len(states) != 1 || states[0].health != handlers_rds.EngineHealthUnhealthy {
|
||||
if len(states) != 1 || states[0].health != rdsinstance.EngineHealthUnhealthy {
|
||||
t.Fatalf("submitted states = %+v, want a single unhealthy report", states)
|
||||
}
|
||||
}
|
||||
@@ -692,7 +692,7 @@ func TestRun_HeartbeatsEngineHealth(t *testing.T) {
|
||||
<-done
|
||||
|
||||
beat := cp.snapshotStates()[0]
|
||||
if beat.health != handlers_rds.EngineHealthHealthy {
|
||||
if beat.health != rdsinstance.EngineHealthHealthy {
|
||||
t.Errorf("heartbeat health = %q, want healthy", beat.health)
|
||||
}
|
||||
if beat.id.DBInstanceIdentifier != "db-resolved" {
|
||||
@@ -747,7 +747,7 @@ func TestRun_HeartbeatReportsDownEngine(t *testing.T) {
|
||||
<-done
|
||||
|
||||
beat := cp.snapshotStates()[0]
|
||||
if beat.health != handlers_rds.EngineHealthUnhealthy {
|
||||
if beat.health != rdsinstance.EngineHealthUnhealthy {
|
||||
t.Errorf("heartbeat health = %q, want unhealthy while the agent is up", beat.health)
|
||||
}
|
||||
if beat.message == "" {
|
||||
@@ -761,11 +761,11 @@ func TestPollDBCommandsOutput_MatchesGatewayShape(t *testing.T) {
|
||||
issued := time.Date(2026, 7, 28, 1, 2, 3, 0, time.UTC)
|
||||
// Rendered exactly as the gateway's typed adapter renders it, so a change to
|
||||
// either side's tags fails here.
|
||||
payload := awsxml.QueryResponsePayload("PollDBCommands", &gateway_rds.PollDBCommandsOutput{
|
||||
Commands: []handlers_rds.Command{{
|
||||
payload := awsxml.QueryResponsePayload("PollDBCommands", &rdsawsapi.PollDBCommandsOutput{
|
||||
Commands: []rdsinstance.Command{{
|
||||
CommandID: "cmd-1",
|
||||
Type: "reload-parameters",
|
||||
Parameters: []handlers_rds.Parameter{{Name: "work_mem", Value: "8MB"}},
|
||||
Parameters: []rdsinstance.Parameter{{Name: "work_mem", Value: "8MB"}},
|
||||
IssuedAt: &issued,
|
||||
}},
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// The staged payload this boot has to acknowledge once PostgreSQL has applied
|
||||
@@ -23,7 +23,7 @@ type pendingBootstrap struct {
|
||||
// Fetches the boot material and writes the handoff rds-init is blocked waiting
|
||||
// for, then points the health probe at the assigned port.
|
||||
func (a *Agent) bootstrap(ctx context.Context) error {
|
||||
var cfg *handlers_rds.GetDBBootstrapConfigOutput
|
||||
var cfg *rdsinstance.GetDBBootstrapConfigOutput
|
||||
if err := retryObserved(ctx, "bootstrap fetch", func(ctx context.Context) error {
|
||||
fetched, err := a.cp.GetBootstrapConfig(ctx, a.id)
|
||||
if err != nil {
|
||||
@@ -35,7 +35,7 @@ func (a *Agent) bootstrap(ctx context.Context) error {
|
||||
// A control plane predating the encrypted payload answers initialize with
|
||||
// nothing to initialise with. Retrying re-dials the queue group until an
|
||||
// upgraded node answers, so a mixed-version fleet heals within one boot.
|
||||
if fetched.Mode == handlers_rds.BootstrapModeInitialize &&
|
||||
if fetched.Mode == rdsinstance.BootstrapModeInitialize &&
|
||||
(fetched.MasterUserPassword == nil || *fetched.MasterUserPassword == "") {
|
||||
return fmt.Errorf("bootstrap fetch returned mode=%s with no master password", fetched.Mode)
|
||||
}
|
||||
@@ -92,7 +92,7 @@ const (
|
||||
|
||||
// bootstrap.env is written last and renamed into place, so its appearance —
|
||||
// which rds-init waits on — means the whole handoff is complete.
|
||||
func writeHandoff(dir string, cfg *handlers_rds.GetDBBootstrapConfigOutput, optionFileName func(name string) string) error {
|
||||
func writeHandoff(dir string, cfg *rdsinstance.GetDBBootstrapConfigOutput, optionFileName func(name string) string) error {
|
||||
if cfg.MasterUsername == "" {
|
||||
return fmt.Errorf("bootstrap config carries no master username")
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func writeHandoff(dir string, cfg *handlers_rds.GetDBBootstrapConfigOutput, opti
|
||||
|
||||
// Every value is single-quoted because rds-init sources this with `.`: an
|
||||
// unquoted password holding a space, `$` or `;` would be word-split or executed.
|
||||
func renderBootstrapEnv(cfg *handlers_rds.GetDBBootstrapConfigOutput) string {
|
||||
func renderBootstrapEnv(cfg *rdsinstance.GetDBBootstrapConfigOutput) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("# Written by rds-agent. Regenerated on every boot; edits are lost.\n")
|
||||
writeEnvLine(&b, "RDS_MODE", cfg.Mode)
|
||||
@@ -193,7 +193,7 @@ func shellQuote(s string) string {
|
||||
// postgresql.conf syntax, which MariaDB's option files also parse. Values are
|
||||
// quoted so a space or unit suffix survives. Names are the engine's startup
|
||||
// spellings, not the customer's: the wrong one leaves the engine unable to boot.
|
||||
func renderParameters(optionFileName func(name string) string, params []handlers_rds.Parameter) (string, error) {
|
||||
func renderParameters(optionFileName func(name string) string, params []rdsinstance.Parameter) (string, error) {
|
||||
if optionFileName == nil {
|
||||
return "", fmt.Errorf("render the resolved parameters: no engine catalog resolved")
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// testPostgresOptionFileName resolves the real catalog's startup spellings,
|
||||
@@ -25,8 +25,8 @@ func TestWriteHandoff_WritesEveryFileRootOnly(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "handoff")
|
||||
password := "s3cret"
|
||||
|
||||
err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
||||
Mode: handlers_rds.BootstrapModeInitialize,
|
||||
err := writeHandoff(dir, &rdsinstance.GetDBBootstrapConfigOutput{
|
||||
Mode: rdsinstance.BootstrapModeInitialize,
|
||||
Engine: "postgres",
|
||||
MasterUsername: "master",
|
||||
ListenAddress: "10.0.1.20",
|
||||
@@ -38,7 +38,7 @@ func TestWriteHandoff_WritesEveryFileRootOnly(t *testing.T) {
|
||||
DataVolumeSerial: "voldata01",
|
||||
VMGeneration: 4,
|
||||
FormatAuthorized: true,
|
||||
Parameters: []handlers_rds.Parameter{
|
||||
Parameters: []rdsinstance.Parameter{
|
||||
{Name: "max_connections", Value: "200"},
|
||||
{Name: "shared_buffers", Value: "256MB"},
|
||||
},
|
||||
@@ -94,8 +94,8 @@ func TestWriteHandoff_WritesEveryFileRootOnly(t *testing.T) {
|
||||
// accident, and a later boot would inherit it.
|
||||
func TestWriteHandoff_AttachOmitsPassword(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "handoff")
|
||||
if err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
||||
Mode: handlers_rds.BootstrapModeAttach,
|
||||
if err := writeHandoff(dir, &rdsinstance.GetDBBootstrapConfigOutput{
|
||||
Mode: rdsinstance.BootstrapModeAttach,
|
||||
Engine: "postgres",
|
||||
MasterUsername: "master",
|
||||
ListenAddress: "10.0.1.20",
|
||||
@@ -127,8 +127,8 @@ func TestWriteHandoff_QuotesShellMetacharacters(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "handoff")
|
||||
password := `a b$(id);'"\` + "`whoami`"
|
||||
|
||||
if err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
||||
Mode: handlers_rds.BootstrapModeInitialize,
|
||||
if err := writeHandoff(dir, &rdsinstance.GetDBBootstrapConfigOutput{
|
||||
Mode: rdsinstance.BootstrapModeInitialize,
|
||||
Engine: "postgres",
|
||||
MasterUsername: "master",
|
||||
ListenAddress: "10.0.1.20",
|
||||
@@ -150,8 +150,8 @@ func TestWriteHandoff_QuotesShellMetacharacters(t *testing.T) {
|
||||
// that is not there; the pair is delivered together or not at all.
|
||||
func TestWriteHandoff_SkipsHalfATLSPair(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "handoff")
|
||||
if err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
||||
Mode: handlers_rds.BootstrapModeAttach,
|
||||
if err := writeHandoff(dir, &rdsinstance.GetDBBootstrapConfigOutput{
|
||||
Mode: rdsinstance.BootstrapModeAttach,
|
||||
Engine: "postgres",
|
||||
MasterUsername: "master",
|
||||
ListenAddress: "10.0.1.20",
|
||||
@@ -171,8 +171,8 @@ func TestWriteHandoff_SkipsHalfATLSPair(t *testing.T) {
|
||||
// would only move the failure into rds-init after the boot has committed to it.
|
||||
func TestWriteHandoff_RejectsConfigWithNoMasterUser(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "handoff")
|
||||
err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
||||
Mode: handlers_rds.BootstrapModeAttach,
|
||||
err := writeHandoff(dir, &rdsinstance.GetDBBootstrapConfigOutput{
|
||||
Mode: rdsinstance.BootstrapModeAttach,
|
||||
Engine: "postgres",
|
||||
Port: 5432,
|
||||
}, testPostgresOptionFileName(t))
|
||||
@@ -192,8 +192,8 @@ func TestWriteHandoff_TightensExistingDirectory(t *testing.T) {
|
||||
t.Fatalf("seed dir: %v", err)
|
||||
}
|
||||
|
||||
if err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
||||
Mode: handlers_rds.BootstrapModeAttach,
|
||||
if err := writeHandoff(dir, &rdsinstance.GetDBBootstrapConfigOutput{
|
||||
Mode: rdsinstance.BootstrapModeAttach,
|
||||
Engine: "postgres",
|
||||
MasterUsername: "master",
|
||||
ListenAddress: "10.0.1.20",
|
||||
@@ -250,7 +250,7 @@ func findLine(t *testing.T, body, prefix string) string {
|
||||
// The include rds-init installs is what the engine starts on, so the spelling
|
||||
// written here is the one that has to boot.
|
||||
func TestRenderParameters_UsesTheEngineStartupSpelling(t *testing.T) {
|
||||
params := []handlers_rds.Parameter{
|
||||
params := []rdsinstance.Parameter{
|
||||
{Name: "time_zone", Value: "SYSTEM"},
|
||||
{Name: "max_connections", Value: "85"},
|
||||
}
|
||||
@@ -275,7 +275,7 @@ func TestRenderParameters_UsesTheEngineStartupSpelling(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRenderParameters_PostgresKeepsEveryName(t *testing.T) {
|
||||
rendered, err := renderParameters(testPostgresOptionFileName(t), []handlers_rds.Parameter{
|
||||
rendered, err := renderParameters(testPostgresOptionFileName(t), []rdsinstance.Parameter{
|
||||
{Name: "shared_buffers", Value: "262144"},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// Executes one control-plane directive inside the guest and returns the message
|
||||
// reported back to the issuer.
|
||||
type commandHandler func(ctx context.Context, cmd handlers_rds.Command) (string, error)
|
||||
type commandHandler func(ctx context.Context, cmd rdsinstance.Command) (string, error)
|
||||
|
||||
// A registry rather than a dispatch switch, so guest operations — password
|
||||
// apply, parameter reload, storage grow, snapshot quiesce — add an entry each.
|
||||
@@ -23,13 +23,13 @@ type commandRegistry map[string]commandHandler
|
||||
// of the guest gets a clear answer rather than a timeout.
|
||||
func newCommandRegistry(engine engineOps, storage storageOps) commandRegistry {
|
||||
return commandRegistry{
|
||||
handlers_rds.CommandSetPassword: func(ctx context.Context, cmd handlers_rds.Command) (string, error) {
|
||||
rdsinstance.CommandSetPassword: func(ctx context.Context, cmd rdsinstance.Command) (string, error) {
|
||||
params := commandParams(cmd)
|
||||
return "", engine.SetPassword(ctx,
|
||||
params[handlers_rds.CommandParamMasterUsername],
|
||||
params[handlers_rds.CommandParamMasterUserPassword])
|
||||
params[rdsinstance.CommandParamMasterUsername],
|
||||
params[rdsinstance.CommandParamMasterUserPassword])
|
||||
},
|
||||
handlers_rds.CommandApplyParams: func(ctx context.Context, cmd handlers_rds.Command) (string, error) {
|
||||
rdsinstance.CommandApplyParams: func(ctx context.Context, cmd rdsinstance.Command) (string, error) {
|
||||
pending, err := engine.ApplyParameters(ctx, cmd.Parameters)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -38,19 +38,19 @@ func newCommandRegistry(engine engineOps, storage storageOps) commandRegistry {
|
||||
// awaiting a restart come back as the message itself.
|
||||
return strings.Join(pending, ","), nil
|
||||
},
|
||||
handlers_rds.CommandStopEngine: func(ctx context.Context, cmd handlers_rds.Command) (string, error) {
|
||||
rdsinstance.CommandStopEngine: func(ctx context.Context, cmd rdsinstance.Command) (string, error) {
|
||||
return "", engine.Stop(ctx)
|
||||
},
|
||||
handlers_rds.CommandGrowFilesystem: func(ctx context.Context, cmd handlers_rds.Command) (string, error) {
|
||||
rdsinstance.CommandGrowFilesystem: func(ctx context.Context, cmd rdsinstance.Command) (string, error) {
|
||||
return storage.GrowFilesystem(ctx)
|
||||
},
|
||||
handlers_rds.CommandQuiesce: func(ctx context.Context, cmd handlers_rds.Command) (string, error) {
|
||||
rdsinstance.CommandQuiesce: func(ctx context.Context, cmd rdsinstance.Command) (string, error) {
|
||||
params := commandParams(cmd)
|
||||
hold, err := quiesceHoldFrom(params[handlers_rds.CommandParamQuiesceDeadlineSeconds])
|
||||
hold, err := quiesceHoldFrom(params[rdsinstance.CommandParamQuiesceDeadlineSeconds])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := engine.Quiesce(ctx, params[handlers_rds.CommandParamQuiesceLabel], hold); err != nil {
|
||||
if err := engine.Quiesce(ctx, params[rdsinstance.CommandParamQuiesceLabel], hold); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// After the hold, so what is flushed is the engine's checkpointed
|
||||
@@ -58,7 +58,7 @@ func newCommandRegistry(engine engineOps, storage storageOps) commandRegistry {
|
||||
// WAL — but the parameter file beside them it never opens.
|
||||
return "", storage.SyncDataMount(ctx)
|
||||
},
|
||||
handlers_rds.CommandUnquiesce: func(ctx context.Context, cmd handlers_rds.Command) (string, error) {
|
||||
rdsinstance.CommandUnquiesce: func(ctx context.Context, cmd rdsinstance.Command) (string, error) {
|
||||
return "", engine.Unquiesce(ctx)
|
||||
},
|
||||
}
|
||||
@@ -71,14 +71,14 @@ func quiesceHoldFrom(value string) (time.Duration, error) {
|
||||
seconds, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil || seconds <= 0 {
|
||||
return 0, fmt.Errorf("%s must be a positive number of seconds, got %q",
|
||||
handlers_rds.CommandParamQuiesceDeadlineSeconds, value)
|
||||
rdsinstance.CommandParamQuiesceDeadlineSeconds, value)
|
||||
}
|
||||
return time.Duration(seconds) * time.Second, nil
|
||||
}
|
||||
|
||||
// A later value wins, so a malformed command carrying a parameter twice cannot
|
||||
// leave the handler acting on the first of two conflicting values.
|
||||
func commandParams(cmd handlers_rds.Command) map[string]string {
|
||||
func commandParams(cmd rdsinstance.Command) map[string]string {
|
||||
params := make(map[string]string, len(cmd.Parameters))
|
||||
for _, p := range cmd.Parameters {
|
||||
params[p.Name] = p.Value
|
||||
@@ -99,7 +99,7 @@ type commander struct {
|
||||
wait time.Duration
|
||||
// Cleared only once a poll has carried them, so a failed poll re-delivers
|
||||
// rather than dropping the result of work the guest actually did.
|
||||
pending []handlers_rds.CommandReply
|
||||
pending []rdsinstance.CommandReply
|
||||
}
|
||||
|
||||
func newCommander(cp controlPlane, registry commandRegistry, wait time.Duration) *commander {
|
||||
@@ -137,7 +137,7 @@ func (c *commander) Run(ctx context.Context) {
|
||||
|
||||
// Every command gets a reply: the issuer is blocked on this command ID, so a
|
||||
// silent drop costs it a full timeout.
|
||||
func (c *commander) execute(ctx context.Context, cmd handlers_rds.Command) handlers_rds.CommandReply {
|
||||
func (c *commander) execute(ctx context.Context, cmd rdsinstance.Command) rdsinstance.CommandReply {
|
||||
handler, ok := c.registry[cmd.Type]
|
||||
if !ok {
|
||||
slog.Warn("rds-agent: unsupported command", "commandID", cmd.CommandID, "type", cmd.Type)
|
||||
@@ -150,17 +150,17 @@ func (c *commander) execute(ctx context.Context, cmd handlers_rds.Command) handl
|
||||
slog.Error("rds-agent: command failed", "commandID", cmd.CommandID, "type", cmd.Type, "err", err)
|
||||
return failedReply(cmd, err.Error())
|
||||
}
|
||||
return handlers_rds.CommandReply{
|
||||
return rdsinstance.CommandReply{
|
||||
CommandID: cmd.CommandID,
|
||||
Status: handlers_rds.CommandStatusSucceeded,
|
||||
Status: rdsinstance.CommandStatusSucceeded,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func failedReply(cmd handlers_rds.Command, message string) handlers_rds.CommandReply {
|
||||
return handlers_rds.CommandReply{
|
||||
func failedReply(cmd rdsinstance.Command, message string) rdsinstance.CommandReply {
|
||||
return rdsinstance.CommandReply{
|
||||
CommandID: cmd.CommandID,
|
||||
Status: handlers_rds.CommandStatusFailed,
|
||||
Status: rdsinstance.CommandStatusFailed,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,30 +6,30 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCommander_DispatchesByNameAndReplies(t *testing.T) {
|
||||
var ran handlers_rds.Command
|
||||
var ran rdsinstance.Command
|
||||
registry := commandRegistry{
|
||||
"reload-parameters": func(_ context.Context, cmd handlers_rds.Command) (string, error) {
|
||||
"reload-parameters": func(_ context.Context, cmd rdsinstance.Command) (string, error) {
|
||||
ran = cmd
|
||||
return "reloaded", nil
|
||||
},
|
||||
}
|
||||
|
||||
reply := newCommander(nil, registry, time.Second).execute(context.Background(), handlers_rds.Command{
|
||||
reply := newCommander(nil, registry, time.Second).execute(context.Background(), rdsinstance.Command{
|
||||
CommandID: "cmd-1",
|
||||
Type: "reload-parameters",
|
||||
Parameters: []handlers_rds.Parameter{{Name: "work_mem", Value: "8MB"}},
|
||||
Parameters: []rdsinstance.Parameter{{Name: "work_mem", Value: "8MB"}},
|
||||
})
|
||||
|
||||
if ran.CommandID != "cmd-1" || len(ran.Parameters) != 1 {
|
||||
t.Errorf("handler received %+v, want the command with its parameters", ran)
|
||||
}
|
||||
if reply.Status != handlers_rds.CommandStatusSucceeded || reply.Message != "reloaded" {
|
||||
if reply.Status != rdsinstance.CommandStatusSucceeded || reply.Message != "reloaded" {
|
||||
t.Errorf("reply = %+v, want succeeded/reloaded", reply)
|
||||
}
|
||||
if reply.CommandID != "cmd-1" {
|
||||
@@ -39,15 +39,15 @@ func TestCommander_DispatchesByNameAndReplies(t *testing.T) {
|
||||
|
||||
func TestCommander_FailedHandlerRepliesWithItsError(t *testing.T) {
|
||||
registry := commandRegistry{
|
||||
"grow-storage": func(context.Context, handlers_rds.Command) (string, error) {
|
||||
"grow-storage": func(context.Context, rdsinstance.Command) (string, error) {
|
||||
return "", errors.New("no space left on device")
|
||||
},
|
||||
}
|
||||
|
||||
reply := newCommander(nil, registry, time.Second).execute(context.Background(),
|
||||
handlers_rds.Command{CommandID: "cmd-2", Type: "grow-storage"})
|
||||
rdsinstance.Command{CommandID: "cmd-2", Type: "grow-storage"})
|
||||
|
||||
if reply.Status != handlers_rds.CommandStatusFailed {
|
||||
if reply.Status != rdsinstance.CommandStatusFailed {
|
||||
t.Errorf("reply status = %q, want failed", reply.Status)
|
||||
}
|
||||
if reply.Message != "no space left on device" {
|
||||
@@ -60,9 +60,9 @@ func TestCommander_FailedHandlerRepliesWithItsError(t *testing.T) {
|
||||
// than a timeout.
|
||||
func TestCommander_UnknownTypeRepliesFailedNotDropped(t *testing.T) {
|
||||
reply := newCommander(nil, newCommandRegistry(&fakeEngine{}, &fakeStorage{}), time.Second).execute(context.Background(),
|
||||
handlers_rds.Command{CommandID: "cmd-3", Type: "quiesce"})
|
||||
rdsinstance.Command{CommandID: "cmd-3", Type: "quiesce"})
|
||||
|
||||
if reply.CommandID != "cmd-3" || reply.Status != handlers_rds.CommandStatusFailed {
|
||||
if reply.CommandID != "cmd-3" || reply.Status != rdsinstance.CommandStatusFailed {
|
||||
t.Errorf("reply = %+v, want a failed reply for cmd-3", reply)
|
||||
}
|
||||
if reply.Message == "" {
|
||||
@@ -74,11 +74,11 @@ func TestCommander_UnknownTypeRepliesFailedNotDropped(t *testing.T) {
|
||||
// fails must not consume it, or the issuer never learns the outcome.
|
||||
func TestCommander_RunCarriesRepliesOnTheNextPoll(t *testing.T) {
|
||||
cp := newFakeControlPlane()
|
||||
cp.pollQueue = [][]handlers_rds.Command{
|
||||
cp.pollQueue = [][]rdsinstance.Command{
|
||||
{{CommandID: "cmd-1", Type: "reload-parameters"}},
|
||||
}
|
||||
registry := commandRegistry{
|
||||
"reload-parameters": func(context.Context, handlers_rds.Command) (string, error) { return "done", nil },
|
||||
"reload-parameters": func(context.Context, rdsinstance.Command) (string, error) { return "done", nil },
|
||||
}
|
||||
|
||||
c := newCommander(cp, registry, 10*time.Millisecond)
|
||||
@@ -98,23 +98,23 @@ func TestCommander_RunCarriesRepliesOnTheNextPoll(t *testing.T) {
|
||||
if len(replies[0]) != 0 {
|
||||
t.Errorf("first poll carried replies %+v, want none", replies[0])
|
||||
}
|
||||
if got := replies[1][0]; got.CommandID != "cmd-1" || got.Status != handlers_rds.CommandStatusSucceeded {
|
||||
if got := replies[1][0]; got.CommandID != "cmd-1" || got.Status != rdsinstance.CommandStatusSucceeded {
|
||||
t.Errorf("second poll carried %+v, want the succeeded reply for cmd-1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommander_RetainsAndRetriesTheCompletePendingBatch(t *testing.T) {
|
||||
cp := newFakeControlPlane()
|
||||
pending := []handlers_rds.CommandReply{
|
||||
{CommandID: "cmd-1", Status: handlers_rds.CommandStatusSucceeded},
|
||||
{CommandID: "cmd-2", Status: handlers_rds.CommandStatusFailed, Message: "disk full"},
|
||||
pending := []rdsinstance.CommandReply{
|
||||
{CommandID: "cmd-1", Status: rdsinstance.CommandStatusSucceeded},
|
||||
{CommandID: "cmd-2", Status: rdsinstance.CommandStatusFailed, Message: "disk full"},
|
||||
}
|
||||
c := newCommander(cp, commandRegistry{
|
||||
"reload-parameters": func(context.Context, handlers_rds.Command) (string, error) {
|
||||
"reload-parameters": func(context.Context, rdsinstance.Command) (string, error) {
|
||||
return "reloaded", nil
|
||||
},
|
||||
}, 10*time.Millisecond)
|
||||
c.pending = append([]handlers_rds.CommandReply(nil), pending...)
|
||||
c.pending = append([]rdsinstance.CommandReply(nil), pending...)
|
||||
cp.pollErr = errors.New("reply publication failed")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -131,7 +131,7 @@ func TestCommander_RetainsAndRetriesTheCompletePendingBatch(t *testing.T) {
|
||||
|
||||
cp.mu.Lock()
|
||||
cp.pollErr = nil
|
||||
cp.pollQueue = [][]handlers_rds.Command{{{
|
||||
cp.pollQueue = [][]rdsinstance.Command{{{
|
||||
CommandID: "cmd-3",
|
||||
Type: "reload-parameters",
|
||||
}}}
|
||||
@@ -150,7 +150,7 @@ func TestCommander_RetainsAndRetriesTheCompletePendingBatch(t *testing.T) {
|
||||
assert.Equal(t, pending, replies[1], "the complete failed batch must be retried in order")
|
||||
require.Len(t, replies[2], 1)
|
||||
assert.Equal(t, "cmd-3", replies[2][0].CommandID)
|
||||
assert.Equal(t, handlers_rds.CommandStatusSucceeded, replies[2][0].Status)
|
||||
assert.Equal(t, rdsinstance.CommandStatusSucceeded, replies[2][0].Status)
|
||||
}
|
||||
|
||||
// A poll error must not spin the loop: without a backoff a gateway that is down
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"testing"
|
||||
|
||||
rdsengine "github.com/mulgadc/spinifex/spinifex/domains/rds/engine"
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// The include the image build validates against a real mariadbd. It is checked
|
||||
@@ -30,7 +30,7 @@ func TestMariaDBDefaultParametersFixture_MatchesTheCatalog(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LookupEngine: %v", err)
|
||||
}
|
||||
sizing := handlers_rds.InstanceSizing()
|
||||
sizing := rdsinstance.InstanceSizing()
|
||||
class := sizing.SmallestInstanceClass()
|
||||
settings, err := engine.ResolveEffectiveParameters(sizing, class, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// The in-guest half of the engine seam. Everything engine-shaped that a
|
||||
@@ -26,7 +26,7 @@ type engineOps interface {
|
||||
SetPassword(ctx context.Context, username, password string) error
|
||||
// Installs the resolved parameter set and reloads. Returns the settings the
|
||||
// engine accepted but will not honour until it restarts.
|
||||
ApplyParameters(ctx context.Context, params []handlers_rds.Parameter) ([]string, error)
|
||||
ApplyParameters(ctx context.Context, params []rdsinstance.Parameter) ([]string, error)
|
||||
// Shuts the engine down cleanly, so the data volume is checkpointed before
|
||||
// the VM is stopped or snapshotted.
|
||||
Stop(ctx context.Context) error
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// A probe the test drives directly, in place of whichever binaries the engine's
|
||||
@@ -40,7 +40,7 @@ type engineHarness struct {
|
||||
useSession func(*fakeSession) *[]command
|
||||
// A setting this engine's catalog really carries, so a shared case installs
|
||||
// something the implementation would genuinely be handed.
|
||||
parameter handlers_rds.Parameter
|
||||
parameter rdsinstance.Parameter
|
||||
// What the engine's own include directive globs. Neither the rollback copy
|
||||
// nor the serving copy may end in it.
|
||||
includeSuffix string
|
||||
@@ -83,7 +83,7 @@ func buildPostgresHarness(t *testing.T) *engineHarness {
|
||||
}
|
||||
return started
|
||||
},
|
||||
parameter: handlers_rds.Parameter{Name: "work_mem", Value: "4096"},
|
||||
parameter: rdsinstance.Parameter{Name: "work_mem", Value: "4096"},
|
||||
includeSuffix: ".conf",
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func buildMariaDBHarness(t *testing.T) *engineHarness {
|
||||
}
|
||||
return started
|
||||
},
|
||||
parameter: handlers_rds.Parameter{Name: "max_connections", Value: "200"},
|
||||
parameter: rdsinstance.Parameter{Name: "max_connections", Value: "200"},
|
||||
includeSuffix: ".cnf",
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func buildMariaDBHarness(t *testing.T) *engineHarness {
|
||||
// the file rather than shadowing it with a second copy.
|
||||
func TestEngineContract_ApplyParametersInstallsWhereRdsInitDoes(t *testing.T) {
|
||||
forEachEngine(t, func(t *testing.T, h *engineHarness) {
|
||||
if _, err := h.engine.ApplyParameters(t.Context(), []handlers_rds.Parameter{h.parameter}); err != nil {
|
||||
if _, err := h.engine.ApplyParameters(t.Context(), []rdsinstance.Parameter{h.parameter}); err != nil {
|
||||
t.Fatalf("ApplyParameters: %v", err)
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ func TestEngineContract_ApplyParametersRefusesWhileTheEngineIsRecovering(t *test
|
||||
forEachEngine(t, func(t *testing.T, h *engineHarness) {
|
||||
*h.state = engineRecovering
|
||||
|
||||
if _, err := h.engine.ApplyParameters(t.Context(), []handlers_rds.Parameter{h.parameter}); err == nil {
|
||||
if _, err := h.engine.ApplyParameters(t.Context(), []rdsinstance.Parameter{h.parameter}); err == nil {
|
||||
t.Fatal("ApplyParameters succeeded against an engine that was still coming up")
|
||||
}
|
||||
if _, err := os.Stat(h.params.installedPath()); !os.IsNotExist(err) {
|
||||
@@ -163,14 +163,14 @@ func TestEngineContract_ApplyParametersRefusesWhileTheEngineIsRecovering(t *test
|
||||
func TestEngineContract_RestoreLastKnownGoodOnlyRollsBackADownEngine(t *testing.T) {
|
||||
forEachEngine(t, func(t *testing.T, h *engineHarness) {
|
||||
accepted := h.parameter
|
||||
if _, err := h.engine.ApplyParameters(t.Context(), []handlers_rds.Parameter{accepted}); err != nil {
|
||||
if _, err := h.engine.ApplyParameters(t.Context(), []rdsinstance.Parameter{accepted}); err != nil {
|
||||
t.Fatalf("ApplyParameters: %v", err)
|
||||
}
|
||||
if err := h.engine.RecordServingParameters(t.Context()); err != nil {
|
||||
t.Fatalf("RecordServingParameters: %v", err)
|
||||
}
|
||||
if _, err := h.engine.ApplyParameters(t.Context(),
|
||||
[]handlers_rds.Parameter{{Name: accepted.Name, Value: accepted.Value + "0"}}); err != nil {
|
||||
[]rdsinstance.Parameter{{Name: accepted.Name, Value: accepted.Value + "0"}}); err != nil {
|
||||
t.Fatalf("second ApplyParameters: %v", err)
|
||||
}
|
||||
|
||||
@@ -322,8 +322,8 @@ func TestEngineContract_ServesEveryRegisteredCommand(t *testing.T) {
|
||||
forEachEngine(t, func(t *testing.T, h *engineHarness) {
|
||||
registry := newCommandRegistry(h.engine, &fakeStorage{})
|
||||
for _, name := range []string{
|
||||
handlers_rds.CommandSetPassword, handlers_rds.CommandApplyParams, handlers_rds.CommandStopEngine,
|
||||
handlers_rds.CommandGrowFilesystem, handlers_rds.CommandQuiesce, handlers_rds.CommandUnquiesce,
|
||||
rdsinstance.CommandSetPassword, rdsinstance.CommandApplyParams, rdsinstance.CommandStopEngine,
|
||||
rdsinstance.CommandGrowFilesystem, rdsinstance.CommandQuiesce, rdsinstance.CommandUnquiesce,
|
||||
} {
|
||||
if _, ok := registry[name]; !ok {
|
||||
t.Errorf("command %q has no handler", name)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// The control plane's own statements about this engine, resolved once at
|
||||
@@ -386,7 +386,7 @@ SET SESSION sql_mode = 'NO_ENGINE_SUBSTITUTION';
|
||||
func (e *mariadbEngine) SetPassword(ctx context.Context, username, password string) error {
|
||||
if username == "" || password == "" {
|
||||
return fmt.Errorf("set-password requires both %s and %s",
|
||||
handlers_rds.CommandParamMasterUsername, handlers_rds.CommandParamMasterUserPassword)
|
||||
rdsinstance.CommandParamMasterUsername, rdsinstance.CommandParamMasterUserPassword)
|
||||
}
|
||||
// This runs as the server's own superuser, so a reserved name in the payload
|
||||
// would rotate the platform's account. The whole rule matters here, not just
|
||||
@@ -410,7 +410,7 @@ func (e *mariadbEngine) SetPassword(ctx context.Context, username, password stri
|
||||
// Installs the resolved set and applies the half a running server can adopt.
|
||||
// MariaDB re-reads no file while up, so SET GLOBAL is the only immediate path.
|
||||
// It has no offline parser, so the safety net is the boot-time rollback.
|
||||
func (e *mariadbEngine) ApplyParameters(ctx context.Context, params []handlers_rds.Parameter) ([]string, error) {
|
||||
func (e *mariadbEngine) ApplyParameters(ctx context.Context, params []rdsinstance.Parameter) ([]string, error) {
|
||||
e.paramMu.Lock()
|
||||
defer e.paramMu.Unlock()
|
||||
|
||||
@@ -473,7 +473,7 @@ func (e *mariadbEngine) ApplyParameters(ctx context.Context, params []handlers_r
|
||||
//
|
||||
// TLS enforcement is excluded: applyTLSEnforcement issues it after this batch
|
||||
// has succeeded.
|
||||
func (e *mariadbEngine) applyDynamicParameters(ctx context.Context, params []handlers_rds.Parameter) error {
|
||||
func (e *mariadbEngine) applyDynamicParameters(ctx context.Context, params []rdsinstance.Parameter) error {
|
||||
var b strings.Builder
|
||||
b.WriteString("SET SESSION sql_mode = 'NO_ENGINE_SUBSTITUTION';\n")
|
||||
applied := 0
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
rdsengine "github.com/mulgadc/spinifex/spinifex/domains/rds/engine"
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// Stands in for the control plane's own definition of the engine. It mirrors the
|
||||
@@ -335,7 +335,7 @@ func TestMariaDBEngine_ApplyParametersSetsOnlyTheDynamicHalfLive(t *testing.T) {
|
||||
runner := &recordingRunner{}
|
||||
engine := newTestMariaDBEngine(t, runner.run)
|
||||
|
||||
pending, err := engine.ApplyParameters(context.Background(), []handlers_rds.Parameter{
|
||||
pending, err := engine.ApplyParameters(context.Background(), []rdsinstance.Parameter{
|
||||
{Name: "max_connections", Value: "200"},
|
||||
{Name: "innodb_buffer_pool_size", Value: "1073741824"},
|
||||
})
|
||||
@@ -378,7 +378,7 @@ func TestMariaDBEngine_ApplyParametersRendersEachValueByItsCatalogType(t *testin
|
||||
runner := &recordingRunner{}
|
||||
engine := newTestMariaDBEngine(t, runner.run)
|
||||
|
||||
if _, err := engine.ApplyParameters(context.Background(), []handlers_rds.Parameter{
|
||||
if _, err := engine.ApplyParameters(context.Background(), []rdsinstance.Parameter{
|
||||
{Name: "max_connections", Value: "200"},
|
||||
{Name: "long_query_time", Value: "2.5"},
|
||||
{Name: "innodb_adaptive_hash_index", Value: "on"},
|
||||
@@ -410,7 +410,7 @@ func TestMariaDBEngine_ApplyParametersRefusesANonNumericNumber(t *testing.T) {
|
||||
engine := newTestMariaDBEngine(t, runner.run)
|
||||
|
||||
_, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "max_connections", Value: "200; DROP DATABASE orders"}})
|
||||
[]rdsinstance.Parameter{{Name: "max_connections", Value: "200; DROP DATABASE orders"}})
|
||||
if err == nil {
|
||||
t.Fatal("ApplyParameters accepted a non-numeric value for an integer parameter")
|
||||
}
|
||||
@@ -426,7 +426,7 @@ func TestMariaDBEngine_ApplyParametersRunsNoStatementForAStaticOnlySet(t *testin
|
||||
engine := newTestMariaDBEngine(t, runner.run)
|
||||
|
||||
if _, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "innodb_log_file_size", Value: "268435456"}}); err != nil {
|
||||
[]rdsinstance.Parameter{{Name: "innodb_log_file_size", Value: "268435456"}}); err != nil {
|
||||
t.Fatalf("ApplyParameters: %v", err)
|
||||
}
|
||||
if len(runner.calls) != 0 {
|
||||
@@ -441,13 +441,13 @@ func TestMariaDBEngine_ApplyParametersRollsBackAValueTheServerRefuses(t *testing
|
||||
engine := newTestMariaDBEngine(t, runner.run)
|
||||
|
||||
if _, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "max_connections", Value: "200"}}); err != nil {
|
||||
[]rdsinstance.Parameter{{Name: "max_connections", Value: "200"}}); err != nil {
|
||||
t.Fatalf("first ApplyParameters: %v", err)
|
||||
}
|
||||
|
||||
runner.err = errors.New("ERROR 1231 (42000): Variable 'max_connections' can't be set to the value of '0'")
|
||||
if _, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "max_connections", Value: "0"}}); err == nil {
|
||||
[]rdsinstance.Parameter{{Name: "max_connections", Value: "0"}}); err == nil {
|
||||
t.Fatal("ApplyParameters succeeded against a value the server refused")
|
||||
}
|
||||
|
||||
@@ -496,8 +496,8 @@ func (r *mariadbTLSReadBackRunner) run(ctx context.Context, c command) (string,
|
||||
return r.recordingRunner.run(ctx, c)
|
||||
}
|
||||
|
||||
func requireSecureTransport(value string) []handlers_rds.Parameter {
|
||||
return []handlers_rds.Parameter{
|
||||
func requireSecureTransport(value string) []rdsinstance.Parameter {
|
||||
return []rdsinstance.Parameter{
|
||||
{Name: "max_connections", Value: "200"},
|
||||
{Name: "require_secure_transport", Value: value},
|
||||
}
|
||||
@@ -554,7 +554,7 @@ func TestMariaDBEngine_ApplyParametersReadsAnAbsentEnforcementKeyAsOn(t *testing
|
||||
engine := newScriptedTestMariaDBEngine(t, runner.run)
|
||||
|
||||
_, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "max_connections", Value: "200"}})
|
||||
[]rdsinstance.Parameter{{Name: "max_connections", Value: "200"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "serving without TLS") {
|
||||
t.Fatalf("ApplyParameters error = %v, want a set predating the parameter to read as enforcing", err)
|
||||
}
|
||||
@@ -568,7 +568,7 @@ func TestMariaDBEngine_ApplyParametersEnforcesOnAnAbsentKey(t *testing.T) {
|
||||
engine := newScriptedTestMariaDBEngine(t, runner.run)
|
||||
|
||||
if _, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "max_connections", Value: "200"}}); err != nil {
|
||||
[]rdsinstance.Parameter{{Name: "max_connections", Value: "200"}}); err != nil {
|
||||
t.Fatalf("ApplyParameters: %v", err)
|
||||
}
|
||||
sql := runner.calls[len(runner.calls)-1].Stdin
|
||||
@@ -585,7 +585,7 @@ func TestMariaDBEngine_ApplyParametersLeavesTLSAloneWhenTheBatchFails(t *testing
|
||||
engine := newScriptedTestMariaDBEngine(t, runner.run)
|
||||
runner.err = errors.New("ERROR 1231 (42000): Variable 'max_connections' can't be set to the value of '0'")
|
||||
|
||||
if _, err := engine.ApplyParameters(context.Background(), []handlers_rds.Parameter{
|
||||
if _, err := engine.ApplyParameters(context.Background(), []rdsinstance.Parameter{
|
||||
{Name: "max_connections", Value: "0"},
|
||||
{Name: "require_secure_transport", Value: "0"},
|
||||
}); err == nil {
|
||||
@@ -762,7 +762,7 @@ func TestMariaDBEngine_LastKnownGoodWaitsForARestartToAdoptAStaticSet(t *testing
|
||||
engine := newTestMariaDBEngine(t, runner.run)
|
||||
|
||||
if _, err := engine.ApplyParameters(t.Context(),
|
||||
[]handlers_rds.Parameter{{Name: "innodb_buffer_pool_size", Value: "1073741824"}}); err != nil {
|
||||
[]rdsinstance.Parameter{{Name: "innodb_buffer_pool_size", Value: "1073741824"}}); err != nil {
|
||||
t.Fatalf("ApplyParameters: %v", err)
|
||||
}
|
||||
if err := engine.RecordServingParameters(t.Context()); err != nil {
|
||||
@@ -797,7 +797,7 @@ func TestMariaDBEngine_LastKnownGoodAdvancesOnADynamicApply(t *testing.T) {
|
||||
}
|
||||
|
||||
if _, err := engine.ApplyParameters(t.Context(),
|
||||
[]handlers_rds.Parameter{{Name: "max_connections", Value: "200"}}); err != nil {
|
||||
[]rdsinstance.Parameter{{Name: "max_connections", Value: "200"}}); err != nil {
|
||||
t.Fatalf("ApplyParameters: %v", err)
|
||||
}
|
||||
if err := engine.RecordServingParameters(t.Context()); err != nil {
|
||||
@@ -928,15 +928,15 @@ func TestMariaDBProbe_MapsToTheHealthTheControlPlaneReads(t *testing.T) {
|
||||
},
|
||||
func(int) bool { return alive }))
|
||||
|
||||
if got, _ := probe.Check(context.Background()); got != handlers_rds.EngineHealthHealthy {
|
||||
if got, _ := probe.Check(context.Background()); got != rdsinstance.EngineHealthHealthy {
|
||||
t.Fatalf("health = %q, want healthy", got)
|
||||
}
|
||||
serving = false
|
||||
if got, _ := probe.Check(context.Background()); got != handlers_rds.EngineHealthStarting {
|
||||
if got, _ := probe.Check(context.Background()); got != rdsinstance.EngineHealthStarting {
|
||||
t.Errorf("health while recovering = %q, want starting", got)
|
||||
}
|
||||
alive = false
|
||||
if got, _ := probe.Check(context.Background()); got != handlers_rds.EngineHealthUnhealthy {
|
||||
if got, _ := probe.Check(context.Background()); got != rdsinstance.EngineHealthUnhealthy {
|
||||
t.Errorf("health after the engine went away = %q, want unhealthy", got)
|
||||
}
|
||||
}
|
||||
@@ -1051,7 +1051,7 @@ func TestMariaDBEngine_TimeZoneAppliesLiveByItsCustomerFacingName(t *testing.T)
|
||||
runner := &recordingRunner{}
|
||||
engine := newTestMariaDBEngine(t, runner.run)
|
||||
|
||||
if _, err := engine.ApplyParameters(context.Background(), []handlers_rds.Parameter{
|
||||
if _, err := engine.ApplyParameters(context.Background(), []rdsinstance.Parameter{
|
||||
{Name: "time_zone", Value: "+10:00"},
|
||||
}); err != nil {
|
||||
t.Fatalf("ApplyParameters: %v", err)
|
||||
@@ -1090,7 +1090,7 @@ func TestMariaDBEngine_ALiveTimeZoneChangeLeavesNothingPendingRestart(t *testing
|
||||
t.Fatalf("seed the serving copy: %v", err)
|
||||
}
|
||||
|
||||
pending, err := engine.ApplyParameters(context.Background(), []handlers_rds.Parameter{
|
||||
pending, err := engine.ApplyParameters(context.Background(), []rdsinstance.Parameter{
|
||||
{Name: "time_zone", Value: "+10:00"},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -1112,7 +1112,7 @@ func TestMariaDBEngine_PendingRestartReportsCatalogNames(t *testing.T) {
|
||||
t.Fatalf("seed the serving copy: %v", err)
|
||||
}
|
||||
|
||||
pending, err := engine.ApplyParameters(context.Background(), []handlers_rds.Parameter{
|
||||
pending, err := engine.ApplyParameters(context.Background(), []rdsinstance.Parameter{
|
||||
{Name: "innodb_buffer_pool_size", Value: "1073741824"},
|
||||
{Name: "time_zone", Value: "SYSTEM"},
|
||||
})
|
||||
@@ -1134,7 +1134,7 @@ func TestMariaDBEngine_EveryResolvedDynamicValueRenders(t *testing.T) {
|
||||
t.Fatalf("LookupEngine: %v", err)
|
||||
}
|
||||
rules := controlPlaneRulesFrom(catalogFromEngine{engine: meta})
|
||||
sizing := handlers_rds.InstanceSizing()
|
||||
sizing := rdsinstance.InstanceSizing()
|
||||
|
||||
for _, class := range rdsengine.SupportedInstanceClasses() {
|
||||
params, err := meta.ResolveEffectiveParameters(sizing, class, nil)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// The engine is reached over its unix socket under peer authentication, so the
|
||||
@@ -149,7 +149,7 @@ func newPostgresEngine(cfg Config, meta EngineCatalog, run commandRunner, startS
|
||||
func (e *postgresEngine) SetPassword(ctx context.Context, username, password string) error {
|
||||
if username == "" || password == "" {
|
||||
return fmt.Errorf("set-password requires both %s and %s",
|
||||
handlers_rds.CommandParamMasterUsername, handlers_rds.CommandParamMasterUserPassword)
|
||||
rdsinstance.CommandParamMasterUsername, rdsinstance.CommandParamMasterUserPassword)
|
||||
}
|
||||
// The statement below runs as the cluster superuser over the socket under
|
||||
// peer auth, so a reserved name in the command payload would hand the
|
||||
@@ -182,7 +182,7 @@ ALTER ROLE :"master" WITH LOGIN PASSWORD :'password';
|
||||
// Installs the resolved set, validates it with the engine's own parser before
|
||||
// the engine adopts it, and reloads. A refused value is rolled back here rather
|
||||
// than left on the data volume, where it would survive every VM replace.
|
||||
func (e *postgresEngine) ApplyParameters(ctx context.Context, params []handlers_rds.Parameter) ([]string, error) {
|
||||
func (e *postgresEngine) ApplyParameters(ctx context.Context, params []rdsinstance.Parameter) ([]string, error) {
|
||||
e.paramMu.Lock()
|
||||
defer e.paramMu.Unlock()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// Stands in for the engine in the registry tests, where what matters is that a
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
type fakeEngine struct {
|
||||
username string
|
||||
password string
|
||||
params []handlers_rds.Parameter
|
||||
params []rdsinstance.Parameter
|
||||
pending []string
|
||||
stopped bool
|
||||
label string
|
||||
@@ -36,7 +36,7 @@ func (f *fakeEngine) SetPassword(_ context.Context, username, password string) e
|
||||
return f.err
|
||||
}
|
||||
|
||||
func (f *fakeEngine) ApplyParameters(_ context.Context, params []handlers_rds.Parameter) ([]string, error) {
|
||||
func (f *fakeEngine) ApplyParameters(_ context.Context, params []rdsinstance.Parameter) ([]string, error) {
|
||||
f.params = params
|
||||
return f.pending, f.err
|
||||
}
|
||||
@@ -58,16 +58,16 @@ func (f *fakeEngine) Unquiesce(context.Context) error {
|
||||
|
||||
func TestCommandRegistry_SetPasswordCarriesBothParameters(t *testing.T) {
|
||||
engine := &fakeEngine{}
|
||||
reply := newCommander(nil, newCommandRegistry(engine, &fakeStorage{}), 0).execute(context.Background(), handlers_rds.Command{
|
||||
reply := newCommander(nil, newCommandRegistry(engine, &fakeStorage{}), 0).execute(context.Background(), rdsinstance.Command{
|
||||
CommandID: "cmd-1",
|
||||
Type: handlers_rds.CommandSetPassword,
|
||||
Parameters: []handlers_rds.Parameter{
|
||||
{Name: handlers_rds.CommandParamMasterUsername, Value: "mulgamaster"},
|
||||
{Name: handlers_rds.CommandParamMasterUserPassword, Value: "n3w-pw"},
|
||||
Type: rdsinstance.CommandSetPassword,
|
||||
Parameters: []rdsinstance.Parameter{
|
||||
{Name: rdsinstance.CommandParamMasterUsername, Value: "mulgamaster"},
|
||||
{Name: rdsinstance.CommandParamMasterUserPassword, Value: "n3w-pw"},
|
||||
},
|
||||
})
|
||||
|
||||
if reply.Status != handlers_rds.CommandStatusSucceeded {
|
||||
if reply.Status != rdsinstance.CommandStatusSucceeded {
|
||||
t.Fatalf("reply = %+v, want succeeded", reply)
|
||||
}
|
||||
if engine.username != "mulgamaster" || engine.password != "n3w-pw" {
|
||||
@@ -84,13 +84,13 @@ func TestCommandRegistry_SetPasswordCarriesBothParameters(t *testing.T) {
|
||||
// restart come back as the message the issuer parses.
|
||||
func TestCommandRegistry_ApplyParamsReportsPendingRestart(t *testing.T) {
|
||||
engine := &fakeEngine{pending: []string{"shared_buffers", "max_connections"}}
|
||||
reply := newCommander(nil, newCommandRegistry(engine, &fakeStorage{}), 0).execute(context.Background(), handlers_rds.Command{
|
||||
reply := newCommander(nil, newCommandRegistry(engine, &fakeStorage{}), 0).execute(context.Background(), rdsinstance.Command{
|
||||
CommandID: "cmd-2",
|
||||
Type: handlers_rds.CommandApplyParams,
|
||||
Parameters: []handlers_rds.Parameter{{Name: "shared_buffers", Value: "256MB"}},
|
||||
Type: rdsinstance.CommandApplyParams,
|
||||
Parameters: []rdsinstance.Parameter{{Name: "shared_buffers", Value: "256MB"}},
|
||||
})
|
||||
|
||||
if reply.Status != handlers_rds.CommandStatusSucceeded {
|
||||
if reply.Status != rdsinstance.CommandStatusSucceeded {
|
||||
t.Fatalf("reply = %+v, want succeeded", reply)
|
||||
}
|
||||
if reply.Message != "shared_buffers,max_connections" {
|
||||
@@ -104,9 +104,9 @@ func TestCommandRegistry_ApplyParamsReportsPendingRestart(t *testing.T) {
|
||||
func TestCommandRegistry_StopEngineReachesTheEngine(t *testing.T) {
|
||||
engine := &fakeEngine{}
|
||||
reply := newCommander(nil, newCommandRegistry(engine, &fakeStorage{}), 0).execute(context.Background(),
|
||||
handlers_rds.Command{CommandID: "cmd-3", Type: handlers_rds.CommandStopEngine})
|
||||
rdsinstance.Command{CommandID: "cmd-3", Type: rdsinstance.CommandStopEngine})
|
||||
|
||||
if reply.Status != handlers_rds.CommandStatusSucceeded {
|
||||
if reply.Status != rdsinstance.CommandStatusSucceeded {
|
||||
t.Fatalf("reply = %+v, want succeeded", reply)
|
||||
}
|
||||
if !engine.stopped {
|
||||
@@ -117,9 +117,9 @@ func TestCommandRegistry_StopEngineReachesTheEngine(t *testing.T) {
|
||||
func TestCommandRegistry_FailureRepliesFailed(t *testing.T) {
|
||||
engine := &fakeEngine{err: errors.New("the engine did not stop")}
|
||||
reply := newCommander(nil, newCommandRegistry(engine, &fakeStorage{}), 0).execute(context.Background(),
|
||||
handlers_rds.Command{CommandID: "cmd-4", Type: handlers_rds.CommandStopEngine})
|
||||
rdsinstance.Command{CommandID: "cmd-4", Type: rdsinstance.CommandStopEngine})
|
||||
|
||||
if reply.Status != handlers_rds.CommandStatusFailed {
|
||||
if reply.Status != rdsinstance.CommandStatusFailed {
|
||||
t.Fatalf("reply = %+v, want failed", reply)
|
||||
}
|
||||
if !strings.Contains(reply.Message, "the engine did not stop") {
|
||||
@@ -329,7 +329,7 @@ func TestPostgresEngine_ApplyParametersInstallsAndReloads(t *testing.T) {
|
||||
engine := newTestEngine(t, runner.run)
|
||||
|
||||
pending, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "shared_buffers", Value: "256MB"}})
|
||||
[]rdsinstance.Parameter{{Name: "shared_buffers", Value: "256MB"}})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParameters: %v", err)
|
||||
}
|
||||
@@ -375,7 +375,7 @@ func TestPostgresEngine_ApplyParametersSurfacesAFailedReload(t *testing.T) {
|
||||
engine := newTestEngine(t, runner.run)
|
||||
|
||||
if _, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "work_mem", Value: "8MB"}}); err == nil {
|
||||
[]rdsinstance.Parameter{{Name: "work_mem", Value: "8MB"}}); err == nil {
|
||||
t.Fatal("ApplyParameters succeeded against a failing reload")
|
||||
}
|
||||
}
|
||||
@@ -389,7 +389,7 @@ func TestPostgresEngine_ApplyParametersSurfacesAnUnsignalledReload(t *testing.T)
|
||||
engine := newScriptedTestEngine(t, runner.run)
|
||||
|
||||
_, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "work_mem", Value: "8MB"}})
|
||||
[]rdsinstance.Parameter{{Name: "work_mem", Value: "8MB"}})
|
||||
if err == nil || !strings.Contains(err.Error(), `pg_reload_conf() returned "f"`) {
|
||||
t.Fatalf("ApplyParameters error = %v, want a refusal naming the answer the postmaster gave", err)
|
||||
}
|
||||
@@ -424,7 +424,7 @@ func TestPostgresEngine_ApplyParametersRestartsOnARepairSetWhileEngineIsDown(t *
|
||||
})
|
||||
engine.repairTimeout = 100 * time.Millisecond
|
||||
engine.repairPoll = time.Millisecond
|
||||
params := []handlers_rds.Parameter{
|
||||
params := []rdsinstance.Parameter{
|
||||
{Name: "shared_buffers", Value: "32768"},
|
||||
{Name: "work_mem", Value: "8192"},
|
||||
}
|
||||
@@ -488,9 +488,9 @@ func (r *tlsReadBackRunner) run(ctx context.Context, c command) (string, error)
|
||||
return r.recordingRunner.run(ctx, c)
|
||||
}
|
||||
|
||||
func forceSSLParameter(t *testing.T, value string) []handlers_rds.Parameter {
|
||||
func forceSSLParameter(t *testing.T, value string) []rdsinstance.Parameter {
|
||||
t.Helper()
|
||||
return []handlers_rds.Parameter{
|
||||
return []rdsinstance.Parameter{
|
||||
{Name: "work_mem", Value: "4096"},
|
||||
{Name: testPostgresEngineMeta(t).TLSEnforcementParameter(), Value: value},
|
||||
}
|
||||
@@ -542,7 +542,7 @@ func TestPostgresEngine_ApplyParametersEnforcesOnAnAbsentKey(t *testing.T) {
|
||||
engine := newScriptedTestEngine(t, runner.run)
|
||||
|
||||
if _, err := engine.ApplyParameters(t.Context(),
|
||||
[]handlers_rds.Parameter{{Name: "work_mem", Value: "4096"}}); err != nil {
|
||||
[]rdsinstance.Parameter{{Name: "work_mem", Value: "4096"}}); err != nil {
|
||||
t.Fatalf("ApplyParameters: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(engine.forceSSLRulePath()); err != nil {
|
||||
@@ -683,7 +683,7 @@ func TestPostgresEngine_ApplyParametersKeepsRepairSetWhenRestartTimesOut(t *test
|
||||
engine.repairTimeout = 10 * time.Millisecond
|
||||
engine.repairPoll = time.Millisecond
|
||||
|
||||
_, err := engine.ApplyParameters(t.Context(), []handlers_rds.Parameter{{Name: "work_mem", Value: "8192"}})
|
||||
_, err := engine.ApplyParameters(t.Context(), []rdsinstance.Parameter{{Name: "work_mem", Value: "8192"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "wait for the engine") {
|
||||
t.Fatalf("ApplyParameters error = %v, want the repair wait failure", err)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ package agent
|
||||
|
||||
import (
|
||||
rdsengine "github.com/mulgadc/spinifex/spinifex/domains/rds/engine"
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// catalogFromEngine adapts a real engine-package Engine to this package's
|
||||
@@ -59,10 +59,10 @@ func testEngineCatalogLookup(name string) (EngineCatalog, error) {
|
||||
// settingsToParameters converts the engine package's own result type to the
|
||||
// wire type the agent's command handlers carry, at the boundary test code
|
||||
// crosses it.
|
||||
func settingsToParameters(settings []rdsengine.Setting) []handlers_rds.Parameter {
|
||||
out := make([]handlers_rds.Parameter, 0, len(settings))
|
||||
func settingsToParameters(settings []rdsengine.Setting) []rdsinstance.Parameter {
|
||||
out := make([]rdsinstance.Parameter, 0, len(settings))
|
||||
for _, s := range settings {
|
||||
out = append(out, handlers_rds.Parameter{Name: s.Name, Value: s.Value})
|
||||
out = append(out, rdsinstance.Parameter{Name: s.Name, Value: s.Value})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// A non-zero exit is a result, not a fault, so it comes back as a code; err is
|
||||
@@ -77,14 +77,14 @@ const (
|
||||
|
||||
// Only the heartbeat calls this, so the seenHealthy latch is single-goroutine
|
||||
// state.
|
||||
func (p *engineProbe) Check(ctx context.Context) (handlers_rds.EngineHealth, string) {
|
||||
func (p *engineProbe) Check(ctx context.Context) (rdsinstance.EngineHealth, string) {
|
||||
state, message := p.state(ctx)
|
||||
switch state {
|
||||
case engineServing:
|
||||
p.seenHealthy = true
|
||||
return handlers_rds.EngineHealthHealthy, ""
|
||||
return rdsinstance.EngineHealthHealthy, ""
|
||||
case engineRecovering:
|
||||
return handlers_rds.EngineHealthStarting, message
|
||||
return rdsinstance.EngineHealthStarting, message
|
||||
default:
|
||||
return p.degraded(), message
|
||||
}
|
||||
@@ -97,9 +97,9 @@ func (p *engineProbe) state(ctx context.Context) (engineState, string) {
|
||||
}
|
||||
|
||||
// A non-answering engine: starting until it has answered once, unhealthy after.
|
||||
func (p *engineProbe) degraded() handlers_rds.EngineHealth {
|
||||
func (p *engineProbe) degraded() rdsinstance.EngineHealth {
|
||||
if p.seenHealthy {
|
||||
return handlers_rds.EngineHealthUnhealthy
|
||||
return rdsinstance.EngineHealthUnhealthy
|
||||
}
|
||||
return handlers_rds.EngineHealthStarting
|
||||
return rdsinstance.EngineHealthStarting
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
func TestEngineProbe_MapsExitCodesToHealth(t *testing.T) {
|
||||
@@ -16,15 +16,15 @@ func TestEngineProbe_MapsExitCodesToHealth(t *testing.T) {
|
||||
code int
|
||||
runErr error
|
||||
seenHealthy bool
|
||||
want handlers_rds.EngineHealth
|
||||
want rdsinstance.EngineHealth
|
||||
}{
|
||||
{name: "accepting connections", code: 0, want: handlers_rds.EngineHealthHealthy},
|
||||
{name: "rejecting during startup", code: 1, want: handlers_rds.EngineHealthStarting},
|
||||
{name: "silent before first success", code: 2, want: handlers_rds.EngineHealthStarting},
|
||||
{name: "silent after first success", code: 2, seenHealthy: true, want: handlers_rds.EngineHealthUnhealthy},
|
||||
{name: "no attempt made", code: 3, want: handlers_rds.EngineHealthStarting},
|
||||
{name: "probe missing before first success", runErr: errors.New("executable not found"), want: handlers_rds.EngineHealthStarting},
|
||||
{name: "probe missing after first success", runErr: errors.New("executable not found"), seenHealthy: true, want: handlers_rds.EngineHealthUnhealthy},
|
||||
{name: "accepting connections", code: 0, want: rdsinstance.EngineHealthHealthy},
|
||||
{name: "rejecting during startup", code: 1, want: rdsinstance.EngineHealthStarting},
|
||||
{name: "silent before first success", code: 2, want: rdsinstance.EngineHealthStarting},
|
||||
{name: "silent after first success", code: 2, seenHealthy: true, want: rdsinstance.EngineHealthUnhealthy},
|
||||
{name: "no attempt made", code: 3, want: rdsinstance.EngineHealthStarting},
|
||||
{name: "probe missing before first success", runErr: errors.New("executable not found"), want: rdsinstance.EngineHealthStarting},
|
||||
{name: "probe missing after first success", runErr: errors.New("executable not found"), seenHealthy: true, want: rdsinstance.EngineHealthUnhealthy},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -38,10 +38,10 @@ func TestEngineProbe_MapsExitCodesToHealth(t *testing.T) {
|
||||
if got != tt.want {
|
||||
t.Errorf("health = %q, want %q", got, tt.want)
|
||||
}
|
||||
if got != handlers_rds.EngineHealthHealthy && message == "" {
|
||||
if got != rdsinstance.EngineHealthHealthy && message == "" {
|
||||
t.Error("a non-healthy result carried no message explaining it")
|
||||
}
|
||||
if got == handlers_rds.EngineHealthHealthy && message != "" {
|
||||
if got == rdsinstance.EngineHealthHealthy && message != "" {
|
||||
t.Errorf("a healthy result carried a message: %q", message)
|
||||
}
|
||||
})
|
||||
@@ -56,11 +56,11 @@ func TestEngineProbe_LatchesAfterFirstHealthy(t *testing.T) {
|
||||
return code, "", nil
|
||||
})
|
||||
|
||||
if got, _ := probe.Check(context.Background()); got != handlers_rds.EngineHealthHealthy {
|
||||
if got, _ := probe.Check(context.Background()); got != rdsinstance.EngineHealthHealthy {
|
||||
t.Fatalf("first check = %q, want healthy", got)
|
||||
}
|
||||
code = 2
|
||||
if got, _ := probe.Check(context.Background()); got != handlers_rds.EngineHealthUnhealthy {
|
||||
if got, _ := probe.Check(context.Background()); got != rdsinstance.EngineHealthUnhealthy {
|
||||
t.Errorf("check after the engine went away = %q, want unhealthy", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,7 +31,7 @@ type heartbeater struct {
|
||||
|
||||
func newHeartbeater(cp controlPlane, probe *engineProbe, recorder servingParameterRecorder, interval time.Duration) *heartbeater {
|
||||
if interval <= 0 {
|
||||
interval = handlers_rds.HeartbeatInterval
|
||||
interval = rdsinstance.HeartbeatInterval
|
||||
}
|
||||
return &heartbeater{cp: cp, probe: probe, recorder: recorder, interval: interval}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func (h *heartbeater) beat(ctx context.Context) {
|
||||
}
|
||||
message += *failure
|
||||
}
|
||||
if health == handlers_rds.EngineHealthHealthy && h.recorder != nil {
|
||||
if health == rdsinstance.EngineHealthHealthy && h.recorder != nil {
|
||||
// Checking every healthy beat also observes a restart that completed
|
||||
// between probes. The recorder skips unchanged and pending-restart sets.
|
||||
recordCtx, cancel := context.WithTimeout(ctx, servingParameterRecordTimeout)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// The first healthy probe is the first proof that the installed include can
|
||||
@@ -33,7 +33,7 @@ func TestHeartbeater_FirstServingProbeSeedsLastKnownGood(t *testing.T) {
|
||||
t.Errorf("last known good = %q, want %q", lastGood, content)
|
||||
}
|
||||
states := cp.snapshotStates()
|
||||
if len(states) != 1 || states[0].health != handlers_rds.EngineHealthHealthy {
|
||||
if len(states) != 1 || states[0].health != rdsinstance.EngineHealthHealthy {
|
||||
t.Errorf("states = %+v, want one healthy heartbeat", states)
|
||||
}
|
||||
}
|
||||
@@ -109,8 +109,8 @@ func TestHeartbeater_ReportsPromptlyUntilTheEngineHasServed(t *testing.T) {
|
||||
waitFor(t, func() bool { return len(cp.snapshotStates()) > 0 }, "the first heartbeat")
|
||||
cancel()
|
||||
|
||||
if states := cp.snapshotStates(); states[0].health != handlers_rds.EngineHealthStarting {
|
||||
t.Errorf("first beat health = %q, want %q", states[0].health, handlers_rds.EngineHealthStarting)
|
||||
if states := cp.snapshotStates(); states[0].health != rdsinstance.EngineHealthStarting {
|
||||
t.Errorf("first beat health = %q, want %q", states[0].health, rdsinstance.EngineHealthStarting)
|
||||
}
|
||||
|
||||
code = 0
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// The generated parameter files and what it takes to install one. They live on
|
||||
@@ -43,7 +43,7 @@ func (s parameterStore) servingPath() string { return filepath.Join(s.dir, s.s
|
||||
|
||||
// Byte-for-byte what rds-init writes for the same set, so the copies compared
|
||||
// against it are comparing values rather than formatting.
|
||||
func (s parameterStore) render(params []handlers_rds.Parameter) ([]byte, error) {
|
||||
func (s parameterStore) render(params []rdsinstance.Parameter) ([]byte, error) {
|
||||
body, err := renderParameters(s.optionFileName, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -54,7 +54,7 @@ func (s parameterStore) render(params []handlers_rds.Parameter) ([]byte, error)
|
||||
// Installs the resolved set at the path rds-init uses, so a later boot
|
||||
// overwrites rather than shadows it. Returns a func putting back whatever was
|
||||
// there, so the caller can undo the install without re-rendering anything.
|
||||
func (s parameterStore) install(params []handlers_rds.Parameter) (restore func() error, err error) {
|
||||
func (s parameterStore) install(params []rdsinstance.Parameter) (restore func() error, err error) {
|
||||
path := s.installedPath()
|
||||
// A missing file is the first-ever apply, and its rollback is a removal.
|
||||
previous, readErr := os.ReadFile(path)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// How long the engine is given to come up after a boot before the installed
|
||||
@@ -66,10 +66,10 @@ func (g *paramGuard) Run(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
message := handlers_rds.ParameterRollbackMessage
|
||||
message := rdsinstance.ParameterRollbackMessage
|
||||
slog.Error("rds-agent: "+message, "after_ms", otelsetup.Millis(g.after))
|
||||
if g.cp != nil {
|
||||
if _, err := g.cp.SubmitState(ctx, g.id, handlers_rds.EngineHealthUnhealthy, message); err != nil {
|
||||
if _, err := g.cp.SubmitState(ctx, g.id, rdsinstance.EngineHealthUnhealthy, message); err != nil {
|
||||
slog.Error("rds-agent: reporting the parameter rollback failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// A runner that fails only the offline config check, so the check's own
|
||||
@@ -39,13 +39,13 @@ func TestPostgresEngine_ApplyParametersRollsBackAValueTheEngineRejects(t *testin
|
||||
// roll back to.
|
||||
runner.checkErr = nil
|
||||
if _, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "work_mem", Value: "4096"}}); err != nil {
|
||||
[]rdsinstance.Parameter{{Name: "work_mem", Value: "4096"}}); err != nil {
|
||||
t.Fatalf("first ApplyParameters: %v", err)
|
||||
}
|
||||
runner.checkErr = errors.New(`unrecognized configuration parameter "work_men"`)
|
||||
|
||||
if _, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "work_mem", Value: "999999999"}}); err == nil {
|
||||
[]rdsinstance.Parameter{{Name: "work_mem", Value: "999999999"}}); err == nil {
|
||||
t.Fatal("ApplyParameters succeeded against a config the engine rejected")
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func TestPostgresEngine_ApplyParametersWithdrawsTheFirstRejectedSet(t *testing.T
|
||||
engine := newTestEngine(t, runner.run)
|
||||
|
||||
if _, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "work_mem", Value: "0"}}); err == nil {
|
||||
[]rdsinstance.Parameter{{Name: "work_mem", Value: "0"}}); err == nil {
|
||||
t.Fatal("ApplyParameters succeeded against a config the engine rejected")
|
||||
}
|
||||
if _, err := os.Stat(engine.params.installedPath()); !os.IsNotExist(err) {
|
||||
@@ -100,7 +100,7 @@ func TestPostgresEngine_ApplyBeforeFirstHeartbeatSeedsLastKnownGood(t *testing.T
|
||||
t.Fatalf("write serving parameter set: %v", err)
|
||||
}
|
||||
|
||||
pending, err := engine.ApplyParameters(t.Context(), []handlers_rds.Parameter{{Name: "shared_buffers", Value: "65536"}})
|
||||
pending, err := engine.ApplyParameters(t.Context(), []rdsinstance.Parameter{{Name: "shared_buffers", Value: "65536"}})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParameters: %v", err)
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func TestPostgresEngine_ServingSnapshotWaitsForParameterApply(t *testing.T) {
|
||||
|
||||
applyDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := engine.ApplyParameters(t.Context(), []handlers_rds.Parameter{{Name: "shared_buffers", Value: "65536"}})
|
||||
_, err := engine.ApplyParameters(t.Context(), []rdsinstance.Parameter{{Name: "shared_buffers", Value: "65536"}})
|
||||
applyDone <- err
|
||||
}()
|
||||
<-runner.checkStarted
|
||||
@@ -209,7 +209,7 @@ func TestPostgresEngine_LastKnownGoodTracksTheServingSet(t *testing.T) {
|
||||
apply := func(value string) {
|
||||
t.Helper()
|
||||
if _, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "work_mem", Value: value}}); err != nil {
|
||||
[]rdsinstance.Parameter{{Name: "work_mem", Value: value}}); err != nil {
|
||||
t.Fatalf("ApplyParameters(%s): %v", value, err)
|
||||
}
|
||||
}
|
||||
@@ -300,14 +300,14 @@ func TestPostgresEngine_RestoreLastKnownGoodParameters(t *testing.T) {
|
||||
}
|
||||
|
||||
if _, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "work_mem", Value: "4096"}}); err != nil {
|
||||
[]rdsinstance.Parameter{{Name: "work_mem", Value: "4096"}}); err != nil {
|
||||
t.Fatalf("ApplyParameters(4096): %v", err)
|
||||
}
|
||||
if err := engine.RecordServingParameters(t.Context()); err != nil {
|
||||
t.Fatalf("RecordServingParameters: %v", err)
|
||||
}
|
||||
if _, err := engine.ApplyParameters(context.Background(),
|
||||
[]handlers_rds.Parameter{{Name: "work_mem", Value: "8192"}}); err != nil {
|
||||
[]rdsinstance.Parameter{{Name: "work_mem", Value: "8192"}}); err != nil {
|
||||
t.Fatalf("ApplyParameters(8192): %v", err)
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ func TestParamGuard_RollsBackReportsAndRestartsWhenTheEngineNeverComesUp(t *test
|
||||
if len(states) != 1 {
|
||||
t.Fatalf("submitted states = %d, want the rollback report", len(states))
|
||||
}
|
||||
if states[0].health != handlers_rds.EngineHealthUnhealthy || states[0].message != handlers_rds.ParameterRollbackMessage {
|
||||
if states[0].health != rdsinstance.EngineHealthUnhealthy || states[0].message != rdsinstance.ParameterRollbackMessage {
|
||||
t.Errorf("rollback state = %+v, want the unhealthy rollback report", states[0])
|
||||
}
|
||||
}
|
||||
@@ -422,7 +422,7 @@ func TestParamGuard_ReportsAndRestartsWhenOnlyTheEnforcementFailed(t *testing.T)
|
||||
if len(states) != 1 {
|
||||
t.Fatalf("submitted states = %d, want the rollback still reported", len(states))
|
||||
}
|
||||
if states[0].health != handlers_rds.EngineHealthUnhealthy || states[0].message != handlers_rds.ParameterRollbackMessage {
|
||||
if states[0].health != rdsinstance.EngineHealthUnhealthy || states[0].message != rdsinstance.ParameterRollbackMessage {
|
||||
t.Errorf("rollback state = %+v, want the unhealthy rollback report", states[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// fakeSession stands in for the held psql child. Closing it is what the engine
|
||||
@@ -116,16 +116,16 @@ func TestPostgresEngine_UnquiesceStopsTheBackupAndEndsTheSession(t *testing.T) {
|
||||
func TestCommandRegistry_QuiesceCarriesTheLabelAndDeadline(t *testing.T) {
|
||||
engine := &fakeEngine{}
|
||||
reply := newCommander(nil, newCommandRegistry(engine, &fakeStorage{}), 0).execute(context.Background(),
|
||||
handlers_rds.Command{
|
||||
rdsinstance.Command{
|
||||
CommandID: "cmd-5",
|
||||
Type: handlers_rds.CommandQuiesce,
|
||||
Parameters: []handlers_rds.Parameter{
|
||||
{Name: handlers_rds.CommandParamQuiesceLabel, Value: "orders-db-pre-upgrade"},
|
||||
{Name: handlers_rds.CommandParamQuiesceDeadlineSeconds, Value: "360"},
|
||||
Type: rdsinstance.CommandQuiesce,
|
||||
Parameters: []rdsinstance.Parameter{
|
||||
{Name: rdsinstance.CommandParamQuiesceLabel, Value: "orders-db-pre-upgrade"},
|
||||
{Name: rdsinstance.CommandParamQuiesceDeadlineSeconds, Value: "360"},
|
||||
},
|
||||
})
|
||||
|
||||
if reply.Status != handlers_rds.CommandStatusSucceeded {
|
||||
if reply.Status != rdsinstance.CommandStatusSucceeded {
|
||||
t.Fatalf("reply = %+v, want succeeded", reply)
|
||||
}
|
||||
if engine.label != "orders-db-pre-upgrade" || engine.hold != 6*time.Minute {
|
||||
@@ -142,7 +142,7 @@ func TestCommandRegistry_QuiesceFlushesTheDataMount(t *testing.T) {
|
||||
reply := newCommander(nil, newCommandRegistry(&fakeEngine{}, storage), 0).execute(context.Background(),
|
||||
quiesceCommand("cmd-5a", "360"))
|
||||
|
||||
if reply.Status != handlers_rds.CommandStatusSucceeded {
|
||||
if reply.Status != rdsinstance.CommandStatusSucceeded {
|
||||
t.Fatalf("reply = %+v, want succeeded", reply)
|
||||
}
|
||||
if !storage.synced {
|
||||
@@ -158,7 +158,7 @@ func TestCommandRegistry_QuiesceFailsWhenTheDataMountCannotBeFlushed(t *testing.
|
||||
reply := newCommander(nil, newCommandRegistry(&fakeEngine{}, storage), 0).execute(context.Background(),
|
||||
quiesceCommand("cmd-5b", "360"))
|
||||
|
||||
if reply.Status != handlers_rds.CommandStatusFailed {
|
||||
if reply.Status != rdsinstance.CommandStatusFailed {
|
||||
t.Fatalf("reply = %+v, want failed", reply)
|
||||
}
|
||||
if !strings.Contains(reply.Message, "input/output error") {
|
||||
@@ -166,13 +166,13 @@ func TestCommandRegistry_QuiesceFailsWhenTheDataMountCannotBeFlushed(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func quiesceCommand(id, deadline string) handlers_rds.Command {
|
||||
return handlers_rds.Command{
|
||||
func quiesceCommand(id, deadline string) rdsinstance.Command {
|
||||
return rdsinstance.Command{
|
||||
CommandID: id,
|
||||
Type: handlers_rds.CommandQuiesce,
|
||||
Parameters: []handlers_rds.Parameter{
|
||||
{Name: handlers_rds.CommandParamQuiesceLabel, Value: "orders-db-pre-upgrade"},
|
||||
{Name: handlers_rds.CommandParamQuiesceDeadlineSeconds, Value: deadline},
|
||||
Type: rdsinstance.CommandQuiesce,
|
||||
Parameters: []rdsinstance.Parameter{
|
||||
{Name: rdsinstance.CommandParamQuiesceLabel, Value: "orders-db-pre-upgrade"},
|
||||
{Name: rdsinstance.CommandParamQuiesceDeadlineSeconds, Value: deadline},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -183,16 +183,16 @@ func TestCommandRegistry_QuiesceRejectsAnUnusableDeadline(t *testing.T) {
|
||||
for _, value := range []string{"", "soon", "0", "-30"} {
|
||||
engine := &fakeEngine{}
|
||||
reply := newCommander(nil, newCommandRegistry(engine, &fakeStorage{}), 0).execute(context.Background(),
|
||||
handlers_rds.Command{
|
||||
rdsinstance.Command{
|
||||
CommandID: "cmd-6",
|
||||
Type: handlers_rds.CommandQuiesce,
|
||||
Parameters: []handlers_rds.Parameter{
|
||||
{Name: handlers_rds.CommandParamQuiesceLabel, Value: "orders-db-pre-upgrade"},
|
||||
{Name: handlers_rds.CommandParamQuiesceDeadlineSeconds, Value: value},
|
||||
Type: rdsinstance.CommandQuiesce,
|
||||
Parameters: []rdsinstance.Parameter{
|
||||
{Name: rdsinstance.CommandParamQuiesceLabel, Value: "orders-db-pre-upgrade"},
|
||||
{Name: rdsinstance.CommandParamQuiesceDeadlineSeconds, Value: value},
|
||||
},
|
||||
})
|
||||
|
||||
if reply.Status != handlers_rds.CommandStatusFailed {
|
||||
if reply.Status != rdsinstance.CommandStatusFailed {
|
||||
t.Errorf("deadline %q: reply = %+v, want failed", value, reply)
|
||||
}
|
||||
if engine.label != "" {
|
||||
@@ -204,9 +204,9 @@ func TestCommandRegistry_QuiesceRejectsAnUnusableDeadline(t *testing.T) {
|
||||
func TestCommandRegistry_UnquiesceReachesTheEngine(t *testing.T) {
|
||||
engine := &fakeEngine{}
|
||||
reply := newCommander(nil, newCommandRegistry(engine, &fakeStorage{}), 0).execute(context.Background(),
|
||||
handlers_rds.Command{CommandID: "cmd-7", Type: handlers_rds.CommandUnquiesce})
|
||||
rdsinstance.Command{CommandID: "cmd-7", Type: rdsinstance.CommandUnquiesce})
|
||||
|
||||
if reply.Status != handlers_rds.CommandStatusSucceeded {
|
||||
if reply.Status != rdsinstance.CommandStatusSucceeded {
|
||||
t.Fatalf("reply = %+v, want succeeded", reply)
|
||||
}
|
||||
if !engine.released {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
)
|
||||
|
||||
// Stands in for the guest's storage in the registry tests, where what matters
|
||||
@@ -225,12 +225,12 @@ func TestCommandRegistry_GrowFilesystemReachesTheGuestStorage(t *testing.T) {
|
||||
storage := &fakeStorage{message: "grew /dev/vdb (ext4) at /var/lib/postgresql"}
|
||||
|
||||
reply := newCommander(nil, newCommandRegistry(&fakeEngine{}, storage), 0).execute(context.Background(),
|
||||
handlers_rds.Command{CommandID: "cmd-9", Type: handlers_rds.CommandGrowFilesystem})
|
||||
rdsinstance.Command{CommandID: "cmd-9", Type: rdsinstance.CommandGrowFilesystem})
|
||||
|
||||
if !storage.grown {
|
||||
t.Error("the grow-filesystem command did not reach the guest storage")
|
||||
}
|
||||
if reply.Status != handlers_rds.CommandStatusSucceeded || reply.Message != storage.message {
|
||||
if reply.Status != rdsinstance.CommandStatusSucceeded || reply.Message != storage.message {
|
||||
t.Errorf("reply = %+v, want succeeded carrying what the grow reported", reply)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,52 @@ import (
|
||||
// ClusterConfig is the root of spinifex.toml as returned by LoadConfig: the
|
||||
// cluster-wide settings plus a [nodes.<name>] Config for every node.
|
||||
type ClusterConfig struct {
|
||||
Epoch uint64 `mapstructure:"epoch"` // bump when leader commits changes
|
||||
Node string `mapstructure:"node"` // my node name
|
||||
Version string `mapstructure:"version"` // spinifex version
|
||||
Network NetworkConfig `mapstructure:"network"` // cluster-wide external network settings
|
||||
Bootstrap BootstrapConfig `mapstructure:"bootstrap"` // default VPC IDs for OVN reconciliation
|
||||
AWS AWSConfig `mapstructure:"aws"` // cluster-wide AWS-parity settings (region, endpoint suffix)
|
||||
Nodes map[string]Config `mapstructure:"nodes"` // full config for every node
|
||||
Epoch uint64 `mapstructure:"epoch"` // bump when leader commits changes
|
||||
Node string `mapstructure:"node"` // my node name
|
||||
Version string `mapstructure:"version"` // spinifex version
|
||||
Network NetworkConfig `mapstructure:"network"` // cluster-wide external network settings
|
||||
Bootstrap BootstrapConfig `mapstructure:"bootstrap"` // default VPC IDs for OVN reconciliation
|
||||
AWS AWSConfig `mapstructure:"aws"` // cluster-wide AWS-parity settings (region, endpoint suffix)
|
||||
Northstar NorthstarClusterConfig `mapstructure:"northstar"` // cluster-wide authoritative DNS settings
|
||||
Nodes map[string]Config `mapstructure:"nodes"` // full config for every node
|
||||
}
|
||||
|
||||
// MaxNameservers caps the nsN names a cluster publishes for its zones. Every
|
||||
// northstar node still serves every zone; the cap bounds only the delegation a
|
||||
// registrar has to carry.
|
||||
const MaxNameservers = 4
|
||||
|
||||
// ServiceNorthstar in --services marks a node as one of the published
|
||||
// nameservers. Northstar runs on every node either way, so it never narrows
|
||||
// the node's service list.
|
||||
const ServiceNorthstar = "northstar"
|
||||
|
||||
// SplitNameserverService returns services without ServiceNorthstar, and
|
||||
// whether it was named.
|
||||
func SplitNameserverService(services []string) ([]string, bool) {
|
||||
rest := make([]string, 0, len(services))
|
||||
named := false
|
||||
for _, s := range services {
|
||||
if strings.EqualFold(strings.TrimSpace(s), ServiceNorthstar) {
|
||||
named = true
|
||||
continue
|
||||
}
|
||||
rest = append(rest, s)
|
||||
}
|
||||
if len(rest) == 0 {
|
||||
rest = nil
|
||||
}
|
||||
return rest, named
|
||||
}
|
||||
|
||||
// NorthstarClusterConfig is the cluster-wide [northstar] block.
|
||||
type NorthstarClusterConfig struct {
|
||||
// NameserverCount is how many nsN names to publish, 1 to MaxNameservers.
|
||||
// Zero means one per northstar node, up to MaxNameservers.
|
||||
NameserverCount int `mapstructure:"nameserver_count"`
|
||||
// NameserverNodes pins ns1, ns2, ... to these nodes, in order. Unpinned
|
||||
// slots are filled from the remaining northstar nodes in name order.
|
||||
NameserverNodes []string `mapstructure:"nameserver_nodes"`
|
||||
}
|
||||
|
||||
// Defaults for cluster-wide AWS-parity settings.
|
||||
@@ -654,6 +693,32 @@ func LoadConfig(configPath string) (*ClusterConfig, error) {
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// validateNorthstarCluster checks the [northstar] nameserver settings against
|
||||
// the cap and the configured nodes.
|
||||
func validateNorthstarCluster(cc *ClusterConfig) error {
|
||||
ns := cc.Northstar
|
||||
if ns.NameserverCount < 0 || ns.NameserverCount > MaxNameservers {
|
||||
return fmt.Errorf("config: [northstar] nameserver_count=%d out of range; use 1-%d, or remove the key for one per northstar node up to %d", ns.NameserverCount, MaxNameservers, MaxNameservers)
|
||||
}
|
||||
if len(ns.NameserverNodes) > MaxNameservers {
|
||||
return fmt.Errorf("config: [northstar] nameserver_nodes lists %d nodes; at most %d are published", len(ns.NameserverNodes), MaxNameservers)
|
||||
}
|
||||
if ns.NameserverCount > 0 && len(ns.NameserverNodes) > ns.NameserverCount {
|
||||
return fmt.Errorf("config: [northstar] nameserver_nodes lists %d nodes but nameserver_count is %d", len(ns.NameserverNodes), ns.NameserverCount)
|
||||
}
|
||||
seen := make(map[string]bool, len(ns.NameserverNodes))
|
||||
for _, name := range ns.NameserverNodes {
|
||||
if _, ok := cc.Nodes[name]; !ok {
|
||||
return fmt.Errorf("config: [northstar] nameserver_nodes names %q, which is not a configured node", name)
|
||||
}
|
||||
if seen[name] {
|
||||
return fmt.Errorf("config: [northstar] nameserver_nodes names %q twice", name)
|
||||
}
|
||||
seen[name] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateClusterConfig rejects legacy DHCP config keys and validates external pool ranges.
|
||||
func validateClusterConfig(cc *ClusterConfig) error {
|
||||
if viper.IsSet("network.external_dhcp") {
|
||||
@@ -684,6 +749,10 @@ func validateClusterConfig(cc *ClusterConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateNorthstarCluster(cc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(cc.Network.NATExemptCIDRs) > 0 && cc.Network.ExternalMode != "nat" {
|
||||
return fmt.Errorf("config: [network] nat_exempt_cidrs requires external_mode = \"nat\"")
|
||||
}
|
||||
|
||||
@@ -300,6 +300,27 @@ expected_nodes = 3
|
||||
|
||||
// Tests for HasService / GetServices
|
||||
|
||||
func TestSplitNameserverService(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
in []string
|
||||
rest []string
|
||||
named bool
|
||||
}{
|
||||
"absent": {[]string{"nats", "daemon"}, []string{"nats", "daemon"}, false},
|
||||
"alone keeps every service": {[]string{"northstar"}, nil, true},
|
||||
"among others": {[]string{"nats", " Northstar ", "daemon"}, []string{"nats", "daemon"}, true},
|
||||
"empty": {nil, nil, false},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
rest, named := SplitNameserverService(tc.in)
|
||||
assert.Equal(t, tc.rest, rest)
|
||||
assert.Equal(t, tc.named, named)
|
||||
})
|
||||
}
|
||||
rest, _ := SplitNameserverService([]string{"northstar"})
|
||||
assert.Equal(t, AllServices, Config{Services: rest}.GetServices(), "northstar alone must not narrow the node's services")
|
||||
}
|
||||
|
||||
func TestHasService(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1426,3 +1447,43 @@ exoscale_instance_id = "0f1e2d3c-0000-4000-8000-000000000001"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_NorthstarNameservers(t *testing.T) {
|
||||
load := func(t *testing.T, block string) (*ClusterConfig, error) {
|
||||
t.Helper()
|
||||
resetViper(t)
|
||||
path := filepath.Join(t.TempDir(), "spinifex.toml")
|
||||
body := "node = \"n1\"\n\n" + block + "\n[nodes.n1]\nregion = \"us-east-1\"\n\n[nodes.n2]\nregion = \"us-east-1\"\n"
|
||||
require.NoError(t, os.WriteFile(path, []byte(body), 0600))
|
||||
return LoadConfig(path)
|
||||
}
|
||||
|
||||
t.Run("absent block means automatic", func(t *testing.T) {
|
||||
cfg, err := load(t, "")
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, cfg.Northstar.NameserverCount)
|
||||
assert.Empty(t, cfg.Northstar.NameserverNodes)
|
||||
})
|
||||
|
||||
t.Run("count and pin load", func(t *testing.T) {
|
||||
cfg, err := load(t, "[northstar]\nnameserver_count = 2\nnameserver_nodes = [\"n2\", \"n1\"]\n")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, cfg.Northstar.NameserverCount)
|
||||
assert.Equal(t, []string{"n2", "n1"}, cfg.Northstar.NameserverNodes)
|
||||
})
|
||||
|
||||
for name, tc := range map[string]struct{ block, want string }{
|
||||
"count above the cap": {"[northstar]\nnameserver_count = 5\n", "nameserver_count=5"},
|
||||
"negative count": {"[northstar]\nnameserver_count = -1\n", "nameserver_count=-1"},
|
||||
"pin longer than count": {"[northstar]\nnameserver_count = 1\nnameserver_nodes = [\"n1\", \"n2\"]\n", "nameserver_count is 1"},
|
||||
"pin names an unknown node": {"[northstar]\nnameserver_nodes = [\"n9\"]\n", `"n9"`},
|
||||
"pin repeats a node": {"[northstar]\nnameserver_nodes = [\"n1\", \"n1\"]\n", "twice"},
|
||||
"pin above the cap": {"[northstar]\nnameserver_nodes = [\"n1\", \"n2\", \"n3\", \"n4\", \"n5\"]\n", "at most 4"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := load(t, tc.block)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -82,7 +82,7 @@ type ACMServiceImpl struct {
|
||||
// SAN) by deriveValidationMode. Wired by the daemon post-construction
|
||||
// (mirroring CertMaterialUpdated below) to domains/dns.HostsZone. A func
|
||||
// field stands in for a direct import of domains/dns, the same pattern
|
||||
// CertMaterialUpdated uses for handlers/elbv2, so this package and its
|
||||
// CertMaterialUpdated uses for domains/elbv2, so this package and its
|
||||
// tests stay decoupled from domains/dns's S3/northstar-config
|
||||
// dependencies. Nil means "northstar hosts nothing" — the safe default for
|
||||
// tests and any deployment that never wires it, so an unwired service can
|
||||
@@ -91,7 +91,7 @@ type ACMServiceImpl struct {
|
||||
|
||||
// CertMaterialUpdated, when set, is invoked after new certificate material
|
||||
// is written under an existing ARN so the caller can re-render every load
|
||||
// balancer that references it (see handlers/elbv2.UpdateStoredConfigForCert).
|
||||
// balancer that references it (see domains/elbv2.UpdateStoredConfigForCert).
|
||||
// Wired by the daemon post-construction to avoid an elbv2 -> acm import
|
||||
// cycle; nil-safe — skipped in tests and whenever ELBv2 isn't wired up.
|
||||
// Fan-out errors are logged, never propagated: a rendering problem on one
|
||||
|
||||
@@ -21,9 +21,9 @@ import (
|
||||
"github.com/aws/aws-sdk-go/private/protocol/json/jsonutil"
|
||||
"github.com/aws/aws-sdk-go/service/acm"
|
||||
"github.com/mulgadc/spinifex/internal/testkit"
|
||||
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -33,7 +33,7 @@ const testAccountID = "000000000001"
|
||||
func setupACMService(t *testing.T) *ACMServiceImpl {
|
||||
t.Helper()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
masterKey, err := handlers_iam.GenerateMasterKey()
|
||||
masterKey, err := iamdomain.GenerateMasterKey()
|
||||
require.NoError(t, err)
|
||||
svc, err := NewACMServiceImplWithNATS(t.Context(), nil, nc, masterKey)
|
||||
require.NoError(t, err)
|
||||
@@ -307,7 +307,7 @@ func TestImportCertificate_PrivateKeyEncryptedAtRest(t *testing.T) {
|
||||
// TestDescribeCertificate_DoesNotLeakPrivateKey asserts the public
|
||||
// CertificateDetail response can never carry key material, however the
|
||||
// record is stored. Precedent: TestSensitiveDataNotLogged_MasterKey in
|
||||
// handlers/iam/service_impl_test.go.
|
||||
// domains/iam/service_impl_test.go.
|
||||
func TestDescribeCertificate_DoesNotLeakPrivateKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
@@ -331,7 +331,7 @@ func TestDescribeCertificate_DoesNotLeakPrivateKey(t *testing.T) {
|
||||
// TestSensitiveDataNotLogged_ACMPrivateKey asserts ImportCertificate and
|
||||
// DescribeCertificate never place the raw private key PEM in log output.
|
||||
// Precedent: TestSensitiveDataNotLogged_MasterKey in
|
||||
// handlers/iam/service_impl_test.go.
|
||||
// domains/iam/service_impl_test.go.
|
||||
// fakeCertAuthority is a minimal CertAuthority for testing RequestCertificate's
|
||||
// PRIVATE_CA path without the real tenant CA implementation (domains/acm/privateca.go
|
||||
// lives on a separate branch).
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvstore"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil"
|
||||
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
@@ -106,7 +106,7 @@ type CertRecord struct {
|
||||
ImportedAt time.Time `json:"imported_at"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
// InUseBy is the set of load balancer ARNs whose listeners currently
|
||||
// reference this certificate. Maintained by handlers/elbv2 as listeners are
|
||||
// reference this certificate. Maintained by domains/elbv2 as listeners are
|
||||
// created, modified and deleted; also surfaces as the public
|
||||
// CertificateDetail.InUseBy field.
|
||||
InUseBy []string `json:"in_use_by,omitempty"`
|
||||
@@ -150,7 +150,7 @@ type CertRecord struct {
|
||||
ACMEAttemptCount int `json:"acme_attempt_count,omitempty"`
|
||||
ACMENextAttemptAt time.Time `json:"acme_next_attempt_at"`
|
||||
|
||||
// --- Per-certificate issuance lease (see handlers/eks/cluster_reconciler.go
|
||||
// --- Per-certificate issuance lease (see domains/eks/cluster/cluster_reconciler.go
|
||||
// for the pattern this follows). Only the holder may drive this
|
||||
// certificate's order; empty/zero when unleased.
|
||||
LeaseHolder string `json:"lease_holder,omitempty"`
|
||||
@@ -161,7 +161,7 @@ type CertRecord struct {
|
||||
type Store struct {
|
||||
store *kvstore.Store[CertRecord]
|
||||
// masterKey encrypts/decrypts CertRecord.PrivateKey at rest (AES-256-GCM via
|
||||
// handlers_iam.EncryptSecret/DecryptSecret). Every Store over the ACM bucket
|
||||
// iamdomain.EncryptSecret/DecryptSecret). Every Store over the ACM bucket
|
||||
// — the ACM service and ELBv2's independent read-only Store alike — must be
|
||||
// constructed with the same deployment key: a keyed writer and an unkeyed (or
|
||||
// differently keyed) reader of the same bucket disagree silently, with the
|
||||
@@ -221,7 +221,7 @@ func certKey(certArn string) string {
|
||||
// via the legacy-passthrough path in decryptPrivateKey is therefore
|
||||
// re-encrypted the next time it is put.
|
||||
func (s *Store) PutCert(ctx context.Context, rec *CertRecord) error {
|
||||
ciphertext, err := handlers_iam.EncryptSecret(rec.PrivateKey, s.masterKey)
|
||||
ciphertext, err := iamdomain.EncryptSecret(rec.PrivateKey, s.masterKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt private key: %w", err)
|
||||
}
|
||||
@@ -324,7 +324,7 @@ func isPlaintextPrivateKeyPEM(raw string) bool {
|
||||
// ciphertext, garbage) is a hard error rather than a silent plaintext
|
||||
// fallback.
|
||||
func (s *Store) decryptPrivateKey(rec *CertRecord) error {
|
||||
if plaintext, err := handlers_iam.DecryptSecret(rec.PrivateKey, s.masterKey); err == nil {
|
||||
if plaintext, err := iamdomain.DecryptSecret(rec.PrivateKey, s.masterKey); err == nil {
|
||||
rec.PrivateKey = plaintext
|
||||
return nil
|
||||
}
|
||||
@@ -443,7 +443,7 @@ const maxInUseByCASAttempts = 50
|
||||
// No-op if the certificate does not exist or already lists resourceArn.
|
||||
//
|
||||
// InUseBy is the sole mechanism by which a re-imported certificate reaches
|
||||
// the data plane (see UpdateStoredConfigForCert in handlers/elbv2), so a lost
|
||||
// the data plane (see UpdateStoredConfigForCert in domains/elbv2), so a lost
|
||||
// update here is not a cosmetic race: it silently drops a load balancer from
|
||||
// fan-out, and that load balancer's certificate expires in HAProxy while ACM
|
||||
// still reports it as renewed. Two listeners on different load balancers can
|
||||
@@ -513,7 +513,7 @@ func (s *Store) UpdateTags(ctx context.Context, certArn string, mutate func(tags
|
||||
// AcquireLease attempts to take the per-certificate issuance/renewal lease on
|
||||
// certArn for holderID, valid until now+ttl. The lease lives on the record
|
||||
// itself (CertRecord.LeaseHolder/LeaseExpiresAt) rather than in a separate
|
||||
// leader-election bucket — contrast handlers/eks/cluster_reconciler.go, which
|
||||
// leader-election bucket — contrast domains/eks/cluster/cluster_reconciler.go, which
|
||||
// leases a whole cluster for the life of a Run loop — because the unit of
|
||||
// work here is one certificate, reissued and released in a single pass.
|
||||
//
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/spinifex/internal/testkit"
|
||||
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvstore"
|
||||
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
func setupACMStore(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
masterKey, err := handlers_iam.GenerateMasterKey()
|
||||
masterKey, err := iamdomain.GenerateMasterKey()
|
||||
require.NoError(t, err)
|
||||
store, err := NewStore(t.Context(), nc, masterKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user