mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-26 05:59:41 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c440a6717 | |||
| 00e38f9e76 | |||
| 57ea08be2a | |||
| 10fc0af2c0 | |||
| 524c20db8b | |||
| 2ed2a1c9dc | |||
| ac403f57bc | |||
| 6a41d269b9 | |||
| 1bb470517c | |||
| c545aa6220 | |||
| e006837e1c | |||
| f9dbe9526d | |||
| 738f1e6d4e | |||
| 42ca999e0b | |||
| f35853c1d3 | |||
| b586b7a1f9 | |||
| 24e2572ebd | |||
| 0ac0a106c0 | |||
| b2b7822f6a |
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -25,14 +25,14 @@ var allowlist = []allowance{
|
||||
|
||||
{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/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: "domains/iam", Debt: "ACM: the IAM AES-GCM helper import (ACM relocation residual)", Reason: "IAM AES-GCM helpers; needs a shared crypto boundary"},
|
||||
|
||||
@@ -42,7 +42,7 @@ var allowlist = []allowance{
|
||||
{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/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"},
|
||||
@@ -66,7 +66,7 @@ var allowlist = []allowance{
|
||||
{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"},
|
||||
@@ -89,8 +89,9 @@ 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: "domains/iam", Debt: "Cross-domain", Reason: "consumes another domain's implementation rather than a capability"},
|
||||
@@ -116,26 +117,28 @@ 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: "domains/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: "gateway/sts", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "GetCallerIdentity takes IAMService; needs an IAM 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", 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: "handlers/ecs", To: "domains/ec2/ebs/metadata", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
|
||||
{From: "handlers/ecs", To: "domains/ec2/eip", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
|
||||
{From: "handlers/ecs", To: "domains/ec2/instancetypes", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
|
||||
{From: "handlers/ecs", To: "handlers/elbv2", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
|
||||
{From: "handlers/ecs", To: "domains/iam", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
|
||||
|
||||
{From: "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: "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: "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/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/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"},
|
||||
@@ -145,19 +148,16 @@ var allowlist = []allowance{
|
||||
{From: "handlers/elbv2", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "system-instance role ensurer; needs an IAM capability"},
|
||||
{From: "handlers/elbv2", To: "lbagent", Debt: "handlers/elbv2/{agent_types,haproxy,health_checker,nginx}.go import lbagent", Reason: "health-report and cert/PID-path vocabulary; needs a guest/controller contract"},
|
||||
|
||||
{From: "handlers/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: "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: "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/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: "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: "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"},
|
||||
}
|
||||
|
||||
@@ -191,3 +191,21 @@ 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 the interim guardrail must still reject handlers and gateway
|
||||
// importing it at runtime/daemon.
|
||||
func TestADR0001_InterimDaemonDirectionCoversRuntimeDaemon(t *testing.T) {
|
||||
r := ruleByID("Interim_DaemonGatewayHandlersDirection")
|
||||
daemon, _ := classify("spinifex/runtime/daemon")
|
||||
for _, from := range []string{"spinifex/gateway", "spinifex/handlers/ecs"} {
|
||||
f, _ := classify(from)
|
||||
if !r.subject(f, from) || !r.forbids(f, daemon, from) {
|
||||
t.Errorf("%s -> runtime/daemon is not rejected", from)
|
||||
}
|
||||
}
|
||||
other, _ := classify("spinifex/runtime/host/process")
|
||||
if gw, _ := classify("spinifex/gateway"); r.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,7 +60,7 @@ func (c class) isLegacy(roots ...string) bool {
|
||||
|
||||
// legacyRoots are pre-ADR-0001 application roots still standing.
|
||||
var legacyRoots = map[string]bool{
|
||||
"handlers": true, "gateway": true, "daemon": true,
|
||||
"handlers": true, "gateway": true,
|
||||
"accountteardown": true, "lbagent": true,
|
||||
}
|
||||
|
||||
@@ -70,12 +71,8 @@ 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"}
|
||||
)
|
||||
// operatorLegacy are the legacy roots whose ADR-0001 target is operator/.
|
||||
var operatorLegacy = []string{"accountteardown"}
|
||||
|
||||
// classify maps a module-relative package directory to its layer. ok is false
|
||||
// for a package that has no deliberate classification.
|
||||
@@ -112,7 +109,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":
|
||||
|
||||
@@ -11,26 +11,18 @@ import (
|
||||
// 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/rds",
|
||||
"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{"admin", "daemon", "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.
|
||||
|
||||
@@ -73,7 +73,7 @@ var rules = []rule{
|
||||
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...))
|
||||
t.isLegacy(operatorLegacy...))
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -160,9 +160,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
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+3
-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
|
||||
@@ -118,7 +118,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 +136,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
|
||||
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ 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)
|
||||
|
||||
@@ -342,6 +342,11 @@ while the branch is in flight; they do not describe the umbrella repository's
|
||||
| `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/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. |
|
||||
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
|
||||
@@ -414,9 +419,9 @@ These residuals were recorded by individual slices and must be resolved once the
|
||||
- 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.
|
||||
- The root `gateway` package imports `domains/eks/cluster` directly (`gateway/oidc_discovery.go`: `AccountBucketName`, `GetClusterMeta`). The layering rules don't see it because the root gateway has no domain classification, so it has no allowlist entry. Removal condition: the public OIDC discovery and JWKS endpoints consume an EKS-owned resolver injected at composition, the same pattern as STS, leaving the root gateway with no `domains/eks/cluster` import.
|
||||
- `handlers/elbv2/{agent_types,haproxy,health_checker,nginx}.go` import `lbagent` (`HealthReport`, `ServerStatus`, `CertDir`, `NginxPIDPath`, `NginxCertDir`), a domain reading an agent's implementation package rather than a declared wire contract. Removal condition: the health-report and cert/PID-path vocabulary moves behind a guest/controller contract the domain and the agent both consume.
|
||||
- `domains/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.
|
||||
- `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.
|
||||
@@ -429,43 +434,45 @@ These residuals were recorded by individual slices and must be resolved once the
|
||||
- `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/eks/tokenwebhook`: imports `domains/eks/cluster` for `WebhookTokenReviewResult`, an agent depending on a domain implementation. Temporary: the eventual EKS contract or projection for the token-review reply needs its own design, and no contract is invented to complete the move.
|
||||
- `agents/ecs/agent`: takes its wire types from `contracts/ecs/v1`, and still imports `handlers/ecs` for the poll-assignment and GPU-report inputs and outputs and the instance-type and availability-zone attribute names. Temporary agent-to-domain debt; no contract is created during the relocation.
|
||||
- `agents/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/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`, `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.
|
||||
- `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` (service registration): the root `gateway` dispatch for IAM, STS, EKS and RDS imports `domains/{iam,sts,eks,rds}/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.
|
||||
- `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. 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 `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/admission/quota` (dependency, moved from `handlers/quota` unchanged): `live.go` imports `gateway/elbv2` 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: `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.
|
||||
- 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.
|
||||
- `domains/rds/instance` → `domains/rds/subnetgroup` (legacy edges): `instanceReferences` binds `Dependants` by scanning instance records, and `tags.go` writes the group record through its generic tag registry. The first clears when the RDS instance gains its own owner and answers `Dependants` from it. The second clears when tagging invokes a subnet-group-owned tag/untag capability rather than writing the record; moving the tagging adapter to `domains/rds/awsapi` alone does not clear it.
|
||||
- `domains/rds/instance` → `domains/rds/parametergroup` (legacy edges): `parameterGroupReferences` binds `Dependants` by scanning instance records; this clears when the RDS instance gains its own owner and answers `Dependants` from it. `tags.go` writes the meta record through its generic tag registry; this clears when tagging invokes a parameter-group-owned tag mutation capability, and moving the tagging adapter to `domains/rds/awsapi` alone does not clear it. The Modify handler still calls `propagateParameterGroup` after the owner's write; propagation is instance-side realization and stays legacy until it is designed as asynchronous realization (group generation, instance staleness, retry). `parameterGroupValues` returns the persisted `Record` and overrides to instance code; when the instance owner is extracted, replace them with a consumer-owned resolved-parameters projection.
|
||||
- `handlers/ecs` → `domains/ecs/taskdefinition` (legacy edges): `tags.go` reads and writes the revision record through its generic tag registry; this clears when ECS tagging invokes an owner-provided tag mutation capability, and moving `tags.go` to `domains/ecs/awsapi` alone does not clear it. `taskDefinitionResolver` returns the persisted `Record` to task and service launch code; when the task and service owners are extracted, replace the record crossing that boundary with a consumer-owned resolved task-definition (launch-spec) projection. Moving packages has not made the record private.
|
||||
- `domains/ecs/taskdefinition` (deliberate duplication, not debt): private `getJSON`, `putJSON` and `keysWithPrefix` copy the raw-KV helpers in `handlers/ecs` so the extraction keeps ECS's get/put/list error and recovery semantics. Revisit only if a later, tested state abstraction proves it preserves those semantics; do not migrate ECS to `kvstore` merely to remove them.
|
||||
- `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/eks/cluster` → `domains/eks/access` (legacy edges): `ResolveTokenReview` opens the EKS bucket in the gateway process and reads records through the exported `access.Get`; this clears when token review asks the owner across a contract rather than opening the bucket itself. `DeleteClusterPrefix` erases access-entry keys as part of its blanket cluster sweep; this clears when cluster delete invokes an owner-provided removal of the cluster's entries. The owner returns the persisted `Record` to the AWS adapter and token review; replace it with consumer-owned projections when those consumers are extracted.
|
||||
- `domains/eks/cluster` → `domains/eks/addon` (legacy edges): `DeleteClusterPrefix` erases add-on records and manifests in its blanket cluster sweep; this clears when cluster delete invokes an add-on-owned teardown capability. `stageGPUDeviceAddon` is a temporary node-group-to-add-on orchestration edge: best-effort, errors logged and swallowed, never undone on node-group delete; it clears when the node-group owner is extracted and the dependency is designed. `ClusterReconciler` hosts the add-on status subscription; it clears when add-on delivery observation has its own host under the lifecycle correction. Cluster existence is checked in the handler, not through an owner-declared capability; it moves into the owner when cluster admission is defined (today status is ignored). Guest wire types (`AddonStatusReport`, `AddonDeliveryPhase`, `AddonStatusSubject`, `StagedAddonManifest`, `InternalAddonsResponse`) are owned by `contracts/eks/v1`, which `domains/eks/cluster`, `gateway/eks` and `agents/eks/gatewayfetch` import directly.
|
||||
- `domains/eks/cluster` and `domains/eks/awsapi` → `domains/eks/addon` (further edges): the AWS adapter reads the persisted `Record` through `addon.Get`/`addon.List`; this clears when the owner returns a view and the add-on adapter moves to `domains/eks/awsapi`. The guest route reaches the owner through the `handlers_eks` NATS RPC (`ListStagedAddonManifests`); this clears with per-resource commands behind EKS registration. The add-on ARN is derived in both the owner and `domains/eks/awsapi/authz.go`; this clears when authorization obtains the ARN from the owner, which an incarnation suffix would require. The owner's `Manifest`/`Report` beside the `contracts/eks/v1` types is deliberate duplication, not debt.
|
||||
- `agents/rds/agent` (dependency, narrowed): engine metadata now arrives through the agent-declared `EngineCatalog`, but the agent still imports `domains/rds/instance` for the command protocol, wire and record types (`Command*`, `Parameter`, `EngineHealth*`, `BootstrapMode*`) and API output types. This clears when those move behind a guest/controller wire contract.
|
||||
- `domains/rds/awsapi` → `domains/rds/instance` (`InstanceSizing`): the gateway obtains its `Sizing` through the RDS handler package's adapter rather than its own composition root. This clears when the gateway's RDS composition moves to `runtime/roles/awsgw` and builds `Sizing` there.
|
||||
- `domains/admission/quota` → `domains/ec2/instancetypes`: `reconcile.go`, `records.go` and `vcpu.go` read `DefaultVCPUs` to size instances without a recorded vCPU count. This clears when quota receives vCPU sizing through a consumer-owned capability or an EC2 projection.
|
||||
- IAM implementation consumers in STS, Ochre and ELBv2: `domains/sts` imports `domains/iam` across five files (`IAMService`, `SessionRevoker`, `EncryptSecret`/`DecryptSecret`, `AccessKeyStatusActive`, `ValidateTrustPolicyDocument`, `PolicyEffectAllow`/`PolicyEffectDeny`, `IAMAccountBucketName`, `OIDCProviderARN`, `OIDCProviderKey`), not only `oidc_jwks.go`; `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.
|
||||
- IAM implementation consumers in STS, Ochre and ELBv2: `domains/sts` imports `domains/iam` across five files (`IAMService`, `SessionRevoker`, `EncryptSecret`/`DecryptSecret`, `AccessKeyStatusActive`, `ValidateTrustPolicyDocument`, `PolicyEffectAllow`/`PolicyEffectDeny`, `IAMAccountBucketName`, `OIDCProviderARN`, `OIDCProviderKey`), not only `oidc_jwks.go`; `domains/sts/awsapi/GetCallerIdentity.go` takes `IAMService`; `domains/ochre/credentials.go` uses `EncryptSecret`/`DecryptSecret`; and `handlers/elbv2/{lb_role,service_impl}.go` use `EnsureSystemInstanceProfile` and `SystemInstanceRoleEnsurer`. Each clears when its consumer receives a narrow IAM capability, or a shared crypto boundary for the secret helpers, rather than importing the IAM implementation.
|
||||
- `handlers/elbv2` (cross-domain): `service_impl.go` imports `domains/acm` (`Store`, `NewStore`), `domains/dns` (`ELBChanges`, `ELBName`, `Change`, `ActionUpsert`, `ResolveBaseDomain`), `domains/ec2/vpc` (`VPCServiceImpl`) and `domains/network/topology` (`Port`), and `launcher.go` re-exports `domains/ec2/systeminstance` launch types as aliases. These clear when ELBv2 is extracted and each edge becomes a consumer-owned capability, which ADR-0001 sequences after the ELBv2 data-plane boundary is settled.
|
||||
- `docs/service-interfaces.yaml` (ECS coverage gap, closed): the traced `ecs.bus.*` family and the ECS AWS-API action subjects are both listed. The action subjects are declared as `ecs.*`, the same wildcard convention as `rds.*`: `handlers/ecs/service_nats.go` publishes them from the gateway and `daemon/daemon.go` subscribes to them. Declaring `ecs.bus.*` alone had made `ecs` a managed prefix, so `manifest-lint` failed CI on all 34 undeclared action subjects until this was added.
|
||||
- `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.
|
||||
@@ -514,7 +521,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).
|
||||
@@ -523,7 +530,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)
|
||||
|
||||
@@ -652,22 +659,23 @@ series above; "needs decision" is open.
|
||||
|
||||
| Legacy path | What remains | What blocks it |
|
||||
|---|---|---|
|
||||
| `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`, `gateway/sts`, `gateway/bedrock`, 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/iam` | — | Done. The implementation moved to `domains/iam` (package `iam`) in `spinifex#1249` with no API, NATS subject, storage or behaviour change. ADR-0001's row says a shared IAM contract is not inferred from current KV readers, and the move adds none. The cross-domain readers (the EC2 instance and spot-instance AWS adapters, `domains/sts`, `domains/sts/awsapi`, `domains/ochre`, the ELBv2 system-instance role path and ECR/Ochre signing-key crypto) still import the implementation and each still needs an IAM capability; that is recorded dependency-rule debt, with its allowlist entries now targeting `domains/iam`, not a prerequisite to relocating IAM's implementation. |
|
||||
| `handlers/sts` | — | Done. The implementation moved to `domains/sts` (package `sts`) in `spinifex#1250` with no API, NATS subject, storage or behaviour change. ADR-0001's condition, cutting the EKS dependency, was already met through the injected `ClusterJWKSResolver`. `domains/sts` still imports `domains/iam` directly; per-consumer IAM capabilities remain dependency-rule debt, with its allowlist entry now sourced from `domains/sts`. |
|
||||
| `handlers/ecs` | Task, service and capacity resources (`taskdefinition` already extracted to `domains/ecs/taskdefinition`). | Deferred: ADR-0004 blocked-dependency list — `iam:PassRole` in the gateway adapter and EC2/ELBv2/EIP adapters called as internal APIs. |
|
||||
| `handlers/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/eks` | — | Done for the relocation: the remaining EKS implementation moved to `domains/eks/cluster` (package `cluster`) in `spinifex#1264` with no API, NATS subject, storage or behaviour change, alongside the already-extracted `access` and `addon` packages. Deferred follow-on: nodegroup service extraction. The nodegroup operations are methods on the shared `EKSServiceImpl` and use its private state, so moving them to their own resource package needs a nodegroup owner and interfaces, which is decomposition work, not a move. The ADR-0004 blocked-dependency imports (IAM role/instance-profile creation, Bluebottle `auth`, the NATS CA, and EKS identity: token review, OIDC) remain dependency-rule debt, with their allowlist entries now sourced from `domains/eks/cluster`. |
|
||||
| `handlers/rds` | — | Done. The remaining RDS instance resource moved to `domains/rds/instance` (package `instance`) in `spinifex#1251` with no API, NATS subject, storage or behaviour change, fulfilling ADR-0001's organise-by-resource condition alongside the already-extracted subnet group, parameter group and engine catalogue. The ADR-0004 blocked-dependency imports (quota admission, the duplicated expected-node fan-out, the IAM ensurer and the Bluebottle master key) remain dependency-rule debt, with their allowlist entries now sourced from `domains/rds/instance`. |
|
||||
| `handlers/elbv2` | Everything; not yet touched. | Deferred: ADR-0001 sequences this after the ELBv2 data-plane boundary is settled. Needs decision: what evidence establishes that boundary is settled. |
|
||||
| `handlers/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}` | Each service's AWS action adapters and dispatch, not yet registered through ADR-0002. | Deferred: each follows its `handlers/<svc>`, because a new-path `awsapi` cannot import a legacy handler. |
|
||||
| `gateway/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. |
|
||||
@@ -675,16 +683,16 @@ series above; "needs decision" is open.
|
||||
|
||||
### Remaining legacy roots by blocker class
|
||||
|
||||
Five legacy roots remain: `accountteardown`, `daemon`, `gateway`, `handlers` and `lbagent`.
|
||||
Four legacy roots remain: `accountteardown`, `gateway`, `handlers` and `lbagent`.
|
||||
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. |
|
||||
| Explicitly deferred by ADR-0001 | `lbagent` (Q-30) | Closed for this refactor; the ADR names the prerequisite. |
|
||||
| No missing boundary (candidate moves) | `handlers/ecs/bus` → `contracts/ecs/v1` (`a130f6b8e`); `admin/images_promote.go` → `domains/ec2/image` (`263c58167`); `admin` bootstrap account identifiers → `foundation/aws/identifiers` (`3fdaa54d9`); `admin` credential-format generators → `foundation/aws/credentials` `0368e861a`, retiring `admin` | Done. |
|
||||
| One narrow missing boundary | Done: `handlers/sts` EKS JWKS lookup → `handlers/eks` `ClusterJWKSResolver` (`254d898a3`); `gateway/tagging` → `domains/tagging` with AWSGW-composed listers (`0d830e901`, `f3545e3ea`, `aedd0b2c4`). Open: `gateway/spx` (owner of operator-plane dispatch registration) | Each needs one decision or one capability, then a mechanical move. |
|
||||
| Broader boundary or follows another package | `handlers/{ecs,eks,rds}` (ADR-0004 blocked-dependency lists); `handlers/elbv2` (data-plane boundary evidence); `accountteardown` (domain cleanup capabilities); `gateway` root (ADR-0002 auth and request-context phase); `gateway/{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 | `handlers/{ecs,eks,rds}` (ADR-0004 blocked-dependency lists); `handlers/elbv2` (data-plane boundary evidence); `accountteardown` (domain cleanup capabilities); `gateway` root (ADR-0002 auth and request-context phase); `gateway/{ecs,elbv2}` (each follows its `handlers/<svc>`, because a new-path `awsapi` cannot import a legacy handler) | Recorded debt with removal conditions above. |
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -50,13 +50,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
|
||||
@@ -78,12 +78,12 @@ services:
|
||||
- spinifex/domains/acm
|
||||
- spinifex/domains/iam
|
||||
- spinifex/handlers/ecs
|
||||
- spinifex/handlers/rds
|
||||
- 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).
|
||||
@@ -158,7 +158,7 @@ services:
|
||||
- "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.
|
||||
@@ -220,7 +220,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
|
||||
@@ -436,7 +436,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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// the cluster first would strand every worker node with nothing left to
|
||||
// address it by.
|
||||
func EKSReapers(nc *nats.Conn) []Reaper {
|
||||
svc := handlers_eks.NewNATSEKSService(nc)
|
||||
svc := ekscluster.NewNATSEKSService(nc)
|
||||
return []Reaper{
|
||||
&eksNodegroupReaper{svc: svc},
|
||||
&eksClusterReaper{svc: svc},
|
||||
@@ -29,7 +29,7 @@ func EKSReapers(nc *nats.Conn) []Reaper {
|
||||
const eksNodegroupSeparator = "/"
|
||||
|
||||
type eksNodegroupReaper struct {
|
||||
svc handlers_eks.EKSService
|
||||
svc ekscluster.EKSService
|
||||
}
|
||||
|
||||
func (r *eksNodegroupReaper) Kind() string { return "eks-nodegroup" }
|
||||
@@ -81,7 +81,7 @@ func (r *eksNodegroupReaper) Delete(ctx context.Context, accountID string, resou
|
||||
}
|
||||
|
||||
type eksClusterReaper struct {
|
||||
svc handlers_eks.EKSService
|
||||
svc ekscluster.EKSService
|
||||
}
|
||||
|
||||
func (r *eksClusterReaper) Kind() string { return "eks-cluster" }
|
||||
@@ -115,7 +115,7 @@ const eksListPageLimit = 100
|
||||
// listEKSClusterNames pages through ListClusters. The listing is capped at 100
|
||||
// per page, so taking only the first page would silently leave every cluster
|
||||
// past it behind — and a teardown that misses one never converges.
|
||||
func listEKSClusterNames(ctx context.Context, svc handlers_eks.EKSService, accountID string) ([]string, error) {
|
||||
func listEKSClusterNames(ctx context.Context, svc ekscluster.EKSService, accountID string) ([]string, error) {
|
||||
var names []string
|
||||
var token *string
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
"github.com/mulgadc/spinifex/internal/testkit"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -109,7 +109,7 @@ func (f *fakeEKS) requestAt(t *testing.T, subject string, index int, into any) {
|
||||
}
|
||||
|
||||
func newEKSReapers(nc *nats.Conn) (*eksNodegroupReaper, *eksClusterReaper) {
|
||||
svc := handlers_eks.NewNATSEKSService(nc)
|
||||
svc := ekscluster.NewNATSEKSService(nc)
|
||||
return &eksNodegroupReaper{svc: svc}, &eksClusterReaper{svc: svc}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/rds"
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// and an address. The configuration groups outlive them and cannot be removed
|
||||
// while an instance still references one, so they wait for the platform stage.
|
||||
func RDSReapers(nc *nats.Conn) []Reaper {
|
||||
svc := handlers_rds.NewNATSService(nc)
|
||||
svc := rdsinstance.NewNATSService(nc)
|
||||
return []Reaper{
|
||||
&rdsInstanceReaper{svc: svc},
|
||||
&rdsSubnetGroupReaper{svc: svc},
|
||||
@@ -31,7 +31,7 @@ func RDSReapers(nc *nats.Conn) []Reaper {
|
||||
const rdsDefaultParameterGroupPrefix = "default."
|
||||
|
||||
type rdsInstanceReaper struct {
|
||||
svc *handlers_rds.NATSService
|
||||
svc *rdsinstance.NATSService
|
||||
}
|
||||
|
||||
func (r *rdsInstanceReaper) Kind() string { return "rds-instance" }
|
||||
@@ -99,7 +99,7 @@ func isDeletionProtected(err error) bool {
|
||||
}
|
||||
|
||||
type rdsSubnetGroupReaper struct {
|
||||
svc *handlers_rds.NATSService
|
||||
svc *rdsinstance.NATSService
|
||||
}
|
||||
|
||||
func (r *rdsSubnetGroupReaper) Kind() string { return "rds-subnet-group" }
|
||||
@@ -129,7 +129,7 @@ func (r *rdsSubnetGroupReaper) Delete(ctx context.Context, accountID string, res
|
||||
}
|
||||
|
||||
type rdsParameterGroupReaper struct {
|
||||
svc *handlers_rds.NATSService
|
||||
svc *rdsinstance.NATSService
|
||||
}
|
||||
|
||||
func (r *rdsParameterGroupReaper) Kind() string { return "rds-parameter-group" }
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/rds"
|
||||
"github.com/mulgadc/spinifex/internal/testkit"
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -116,7 +116,7 @@ func (f *fakeRDS) called() []string {
|
||||
}
|
||||
|
||||
func newRDSReapers(nc *nats.Conn) (*rdsInstanceReaper, *rdsSubnetGroupReaper, *rdsParameterGroupReaper) {
|
||||
svc := handlers_rds.NewNATSService(nc)
|
||||
svc := rdsinstance.NewNATSService(nc)
|
||||
return &rdsInstanceReaper{svc: svc}, &rdsSubnetGroupReaper{svc: svc}, &rdsParameterGroupReaper{svc: svc}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -16,10 +16,10 @@ import (
|
||||
)
|
||||
|
||||
// BedrockUsageReader resolves accountID's current-period Bedrock token usage
|
||||
// (input+output, across every model), the counter gateway_bedrock's usage
|
||||
// (input+output, across every model), the counter ochre's usage
|
||||
// consumer maintains from the invocation stream. The
|
||||
// interface is defined here rather than importing gateway_bedrock so the two
|
||||
// packages stay decoupled; gateway_bedrock.UsageStore satisfies it
|
||||
// interface is defined here rather than importing domains/ochre so the two
|
||||
// packages stay decoupled; ochre.UsageStore satisfies it
|
||||
// structurally, and awsgw.go wires the two together at startup.
|
||||
type BedrockUsageReader interface {
|
||||
TokensThisPeriod(ctx context.Context, accountID string) (int64, error)
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/rds"
|
||||
ec2eipapi "github.com/mulgadc/spinifex/spinifex/domains/ec2/awsapi/eip"
|
||||
ec2vpcapi "github.com/mulgadc/spinifex/spinifex/domains/ec2/awsapi/vpc"
|
||||
rdsinstance "github.com/mulgadc/spinifex/spinifex/domains/rds/instance"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
gateway_elbv2 "github.com/mulgadc/spinifex/spinifex/gateway/elbv2"
|
||||
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -103,7 +103,7 @@ func (s *Service) EnforceRDSInstances(ctx context.Context, natsConn *nats.Conn,
|
||||
if s.Exempt(accountID) {
|
||||
return nil
|
||||
}
|
||||
out, err := handlers_rds.NewNATSService(natsConn).DescribeDBInstances(ctx, &rds.DescribeDBInstancesInput{}, accountID)
|
||||
out, err := rdsinstance.NewNATSService(natsConn).DescribeDBInstances(ctx, &rds.DescribeDBInstancesInput{}, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -172,38 +172,25 @@ func (s *Service) ChargeLaunch(ctx context.Context, accountID string, reservatio
|
||||
// static type without a NATS round trip.
|
||||
type InstanceTypeResolver func(accountID, instanceID string) (instanceType string, ok bool, err error)
|
||||
|
||||
// NATSInstanceTypeResolver builds the production resolver: an account-filtered,
|
||||
// single-instance describe returning the live type of a running or stopped
|
||||
// instance. It uses the strict reconcile variant so an incomplete sweep does not
|
||||
// masquerade as "instance absent" and wave a retype past the cap (see
|
||||
// confirmInstanceType). expectedNodes is the configured node total, re-evaluated
|
||||
// per call so a config change is reflected without a restart.
|
||||
func NATSInstanceTypeResolver(natsConn *nats.Conn, expectedNodes func() int) InstanceTypeResolver {
|
||||
// NATSInstanceTypeResolver builds the production resolver from a node-identity
|
||||
// checked describe of the one instance. A found instance resolves even with a node
|
||||
// silent; an absence a silent node could hide is a retryable ServiceUnavailable.
|
||||
func NATSInstanceTypeResolver(natsConn *nats.Conn, nodeIDs func() []string) InstanceTypeResolver {
|
||||
return func(accountID, instanceID string) (string, bool, error) {
|
||||
reservations, complete, err := ec2instanceapi.DescribeInstancesForReconcile(
|
||||
context.Background(), &ec2.DescribeInstancesInput{InstanceIds: []*string{aws.String(instanceID)}},
|
||||
natsConn, expectedNodes(), accountID)
|
||||
out, err := ec2instanceapi.DescribeInstancesChecked(context.Background(),
|
||||
&ec2.DescribeInstancesInput{InstanceIds: []*string{aws.String(instanceID)}},
|
||||
natsConn, 0, nodeIDs(), accountID)
|
||||
if err != nil {
|
||||
if code, ok := awserrors.ResolveErrorCode(err); ok && code == awserrors.ErrorInvalidInstanceIDNotFound {
|
||||
return "", false, nil
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
return confirmInstanceType(reservations, instanceID, complete)
|
||||
instanceType, ok := instanceTypeFromReservations(out.Reservations, instanceID)
|
||||
return instanceType, ok, nil
|
||||
}
|
||||
}
|
||||
|
||||
// confirmInstanceType resolves instanceID's type from a describe sweep. A genuine
|
||||
// absence on a complete sweep returns ok false with no error, so the retype gate
|
||||
// charges nothing and the daemon rejects the modify. An instance that cannot be
|
||||
// found on an incomplete sweep (a node or bucket missed) fails closed with
|
||||
// ErrorServerInternal: a partial view must not be read as absent and let a retype
|
||||
// skip its cap check. The client retries once the sweep is clean.
|
||||
func confirmInstanceType(reservations []*ec2.Reservation, instanceID string, complete bool) (string, bool, error) {
|
||||
instanceType, ok := instanceTypeFromReservations(reservations, instanceID)
|
||||
if !ok && !complete {
|
||||
return "", false, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
return instanceType, ok, nil
|
||||
}
|
||||
|
||||
// EnforceRetype is the check-before gate for a ModifyInstanceAttribute that
|
||||
// changes InstanceType: it rejects when retyping accountID's instance to newType
|
||||
// would push its vCPU counter over the cap. It returns the vCPU delta the caller
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package quota
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mulgadc/spinifex/internal/testkit"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
natsmsg "github.com/mulgadc/spinifex/spinifex/foundation/messaging/nats"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// reservation builds a single reservation whose instances carry the given type
|
||||
@@ -202,46 +206,123 @@ func TestInstanceTypeFromReservations(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfirmInstanceType locks in the partial-view fix the production resolver
|
||||
// relies on. A found instance resolves regardless of sweep completeness. A
|
||||
// genuine absence on a complete sweep returns ok false with no error, so the gate
|
||||
// charges nothing and the daemon rejects the modify; but an absence on an
|
||||
// incomplete sweep fails closed with ErrorServerInternal, so a node or bucket that
|
||||
// missed the sweep cannot be read as "absent" and wave a retype past the cap.
|
||||
func TestConfirmInstanceType(t *testing.T) {
|
||||
withID := func(id, instanceType, state string) *ec2.Instance {
|
||||
inst := instance(instanceType, state)
|
||||
inst.InstanceId = aws.String(id)
|
||||
return inst
|
||||
// replyAsNode answers every request on subject with reply, attributed to node.
|
||||
func replyAsNode(t *testing.T, nc *nats.Conn, subject, node string, reply any) {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(reply)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal reply: %v", err)
|
||||
}
|
||||
reservations := []*ec2.Reservation{
|
||||
reservation(withID("i-stopped", "t3.micro", ec2.InstanceStateNameStopped)),
|
||||
sub, err := nc.Subscribe(subject, func(msg *nats.Msg) {
|
||||
out := nats.NewMsg(msg.Reply)
|
||||
out.Data = data
|
||||
if node != "" {
|
||||
out.Header.Set(natsmsg.NodeIDHeader, node)
|
||||
}
|
||||
_ = msg.RespondMsg(out)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("subscribe %s: %v", subject, err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
instanceID string
|
||||
complete bool
|
||||
wantType string
|
||||
wantOK bool
|
||||
wantErr string
|
||||
}{
|
||||
{"found on complete sweep", "i-stopped", true, "t3.micro", true, ""},
|
||||
{"found on incomplete sweep", "i-stopped", false, "t3.micro", true, ""},
|
||||
{"absent on complete sweep defers to daemon", "i-missing", true, "", false, ""},
|
||||
{"absent on incomplete sweep fails closed", "i-missing", false, "", false, awserrors.ErrorServerInternal},
|
||||
t.Cleanup(func() { _ = sub.Unsubscribe() })
|
||||
}
|
||||
|
||||
// node-2 holds the instance but never answers, and node-1 answers twice.
|
||||
// Counting replies reads that as every node heard from and the instance as
|
||||
// absent, waving the retype past the cap; the gate must see node-2 is missing.
|
||||
func TestNATSInstanceTypeResolver_RefusesWhenOneNodeAnswersTwiceAndAnotherIsSilent(t *testing.T) {
|
||||
s := newVCPUService(t, Limits{Enabled: true, VCPUs: 8})
|
||||
if err := s.AddVCPU(t.Context(), testAccount, 7); err != nil {
|
||||
t.Fatalf("seed AddVCPU: %v", err)
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotType, gotOK, err := confirmInstanceType(reservations, tt.instanceID, tt.complete)
|
||||
if gotType != tt.wantType || gotOK != tt.wantOK {
|
||||
t.Fatalf("confirmInstanceType() = (%q, %v), want (%q, %v)", gotType, gotOK, tt.wantType, tt.wantOK)
|
||||
}
|
||||
switch {
|
||||
case tt.wantErr == "" && err != nil:
|
||||
t.Fatalf("confirmInstanceType() err = %v, want nil", err)
|
||||
case tt.wantErr != "" && (err == nil || err.Error() != tt.wantErr):
|
||||
t.Fatalf("confirmInstanceType() err = %v, want %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
replyAsNode(t, nc, "ec2.DescribeInstances", "node-1", ec2.DescribeInstancesOutput{})
|
||||
replyAsNode(t, nc, "ec2.DescribeInstances", "node-1", ec2.DescribeInstancesOutput{})
|
||||
replyAsNode(t, nc, "ec2.DescribeStoppedInstances", "", ec2.DescribeInstancesOutput{})
|
||||
replyAsNode(t, nc, "ec2.DescribeTerminatedInstances", "", ec2.DescribeInstancesOutput{})
|
||||
if err := nc.Flush(); err != nil {
|
||||
t.Fatalf("flush: %v", err)
|
||||
}
|
||||
|
||||
resolve := NATSInstanceTypeResolver(nc, func() []string { return []string{"node-1", "node-2"} })
|
||||
delta, err := s.EnforceRetype(t.Context(), resolve, testAccount, "i-on-node2", "m5.xlarge")
|
||||
if err == nil {
|
||||
t.Fatalf("EnforceRetype = (%d, nil), want a refusal while node-2 is silent", delta)
|
||||
}
|
||||
code, ok := awserrors.ResolveErrorCode(err)
|
||||
if !ok || code != awserrors.ErrorServiceUnavailable {
|
||||
t.Fatalf("EnforceRetype err = %v, want retryable %q", err, awserrors.ErrorServiceUnavailable)
|
||||
}
|
||||
}
|
||||
|
||||
// With every node answering, the resolver finds the instance on node-2 and the
|
||||
// gate judges the retype against the cap.
|
||||
func TestNATSInstanceTypeResolver_FindsInstanceWhenEveryNodeAnswers(t *testing.T) {
|
||||
s := newVCPUService(t, Limits{Enabled: true, VCPUs: 8})
|
||||
if err := s.AddVCPU(t.Context(), testAccount, 7); err != nil {
|
||||
t.Fatalf("seed AddVCPU: %v", err)
|
||||
}
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
inst := instance("t3.micro", ec2.InstanceStateNameRunning)
|
||||
inst.InstanceId = aws.String("i-on-node2")
|
||||
replyAsNode(t, nc, "ec2.DescribeInstances", "node-1", ec2.DescribeInstancesOutput{})
|
||||
replyAsNode(t, nc, "ec2.DescribeInstances", "node-2", ec2.DescribeInstancesOutput{Reservations: []*ec2.Reservation{reservation(inst)}})
|
||||
replyAsNode(t, nc, "ec2.DescribeStoppedInstances", "", ec2.DescribeInstancesOutput{})
|
||||
replyAsNode(t, nc, "ec2.DescribeTerminatedInstances", "", ec2.DescribeInstancesOutput{})
|
||||
if err := nc.Flush(); err != nil {
|
||||
t.Fatalf("flush: %v", err)
|
||||
}
|
||||
|
||||
resolve := NATSInstanceTypeResolver(nc, func() []string { return []string{"node-1", "node-2"} })
|
||||
_, err := s.EnforceRetype(t.Context(), resolve, testAccount, "i-on-node2", "m5.xlarge")
|
||||
if err == nil || err.Error() != awserrors.ErrorResourceLimitExceeded {
|
||||
t.Fatalf("EnforceRetype err = %v, want %q", err, awserrors.ErrorResourceLimitExceeded)
|
||||
}
|
||||
}
|
||||
|
||||
// An instance absent from a complete listing charges nothing and returns no
|
||||
// error, so the daemon rejects the modify with the AWS NotFound error.
|
||||
func TestNATSInstanceTypeResolver_AbsentOnCompleteListingDefersToDaemon(t *testing.T) {
|
||||
s := newVCPUService(t, Limits{Enabled: true, VCPUs: 8})
|
||||
if err := s.AddVCPU(t.Context(), testAccount, 7); err != nil {
|
||||
t.Fatalf("seed AddVCPU: %v", err)
|
||||
}
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
replyAsNode(t, nc, "ec2.DescribeInstances", "node-1", ec2.DescribeInstancesOutput{})
|
||||
replyAsNode(t, nc, "ec2.DescribeInstances", "node-2", ec2.DescribeInstancesOutput{})
|
||||
replyAsNode(t, nc, "ec2.DescribeStoppedInstances", "", ec2.DescribeInstancesOutput{})
|
||||
replyAsNode(t, nc, "ec2.DescribeTerminatedInstances", "", ec2.DescribeInstancesOutput{})
|
||||
if err := nc.Flush(); err != nil {
|
||||
t.Fatalf("flush: %v", err)
|
||||
}
|
||||
|
||||
resolve := NATSInstanceTypeResolver(nc, func() []string { return []string{"node-1", "node-2"} })
|
||||
delta, err := s.EnforceRetype(t.Context(), resolve, testAccount, "i-missing", "m5.xlarge")
|
||||
if delta != 0 || err != nil {
|
||||
t.Fatalf("EnforceRetype = (%d, %v), want (0, nil)", delta, err)
|
||||
}
|
||||
}
|
||||
|
||||
// A retype targets a stopped instance, which the shared stopped bucket serves
|
||||
// whatever the node fan-out does, so a silent node must not block judging it.
|
||||
func TestNATSInstanceTypeResolver_FindsStoppedInstanceWhileANodeIsSilent(t *testing.T) {
|
||||
s := newVCPUService(t, Limits{Enabled: true, VCPUs: 8})
|
||||
if err := s.AddVCPU(t.Context(), testAccount, 7); err != nil {
|
||||
t.Fatalf("seed AddVCPU: %v", err)
|
||||
}
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
inst := instance("t3.micro", ec2.InstanceStateNameStopped)
|
||||
inst.InstanceId = aws.String("i-stopped")
|
||||
replyAsNode(t, nc, "ec2.DescribeInstances", "node-1", ec2.DescribeInstancesOutput{})
|
||||
replyAsNode(t, nc, "ec2.DescribeStoppedInstances", "", ec2.DescribeInstancesOutput{Reservations: []*ec2.Reservation{reservation(inst)}})
|
||||
replyAsNode(t, nc, "ec2.DescribeTerminatedInstances", "", ec2.DescribeInstancesOutput{})
|
||||
if err := nc.Flush(); err != nil {
|
||||
t.Fatalf("flush: %v", err)
|
||||
}
|
||||
|
||||
resolve := NATSInstanceTypeResolver(nc, func() []string { return []string{"node-1", "node-2"} })
|
||||
_, err := s.EnforceRetype(t.Context(), resolve, testAccount, "i-stopped", "m5.xlarge")
|
||||
if err == nil || err.Error() != awserrors.ErrorResourceLimitExceeded {
|
||||
t.Fatalf("EnforceRetype err = %v, want %q", err, awserrors.ErrorResourceLimitExceeded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ func WithFanoutTimeout(d time.Duration) DescribeOption {
|
||||
// "not found" as "not currently running/present" rather than a fault.
|
||||
// The customer-facing gateway action uses DescribeInstancesChecked instead,
|
||||
// which adds the InvalidInstanceID.NotFound assertion AWS callers expect.
|
||||
// Callers that must not act on a partial view at all (the quota reconcile)
|
||||
// use DescribeInstancesForReconcile.
|
||||
// Callers that must not act on a partial view at all use
|
||||
// DescribeAccountInstancesComplete.
|
||||
func DescribeInstances(ctx context.Context, input *ec2.DescribeInstancesInput, natsConn *nats.Conn, expectedNodes int, accountID string, opts ...DescribeOption) (*ec2.DescribeInstancesOutput, error) {
|
||||
reservations, _, client4xx, err := gatherInstances(ctx, input, natsConn, expectedNodes, nil, accountID, opts...)
|
||||
if err != nil {
|
||||
@@ -165,18 +165,6 @@ func missingInstanceIDs(reservations []*ec2.Reservation, instanceIDs []*string)
|
||||
return missing
|
||||
}
|
||||
|
||||
// DescribeInstancesForReconcile is the strict variant, for a caller that may act
|
||||
// only on a provably whole view. complete is true only when the sweep observed
|
||||
// every expected node and both instance buckets; a partial sweep — a node down,
|
||||
// a timed-out fan-out, or a failed bucket query — returns complete=false so the
|
||||
// caller can decline to act rather than act on a short count. The quota vCPU
|
||||
// sweep reads the instance record space instead of this; the retype gate is
|
||||
// what still calls it.
|
||||
func DescribeInstancesForReconcile(ctx context.Context, input *ec2.DescribeInstancesInput, natsConn *nats.Conn, expectedNodes int, accountID string) (reservations []*ec2.Reservation, complete bool, err error) {
|
||||
reservations, complete, _, err = gatherInstances(ctx, input, natsConn, expectedNodes, nil, accountID)
|
||||
return reservations, complete, err
|
||||
}
|
||||
|
||||
// DescribeAccountInstancesComplete lists the account's instances, or returns a
|
||||
// retryable ServiceUnavailable unless every node in nodeIDs and both instance
|
||||
// buckets answered cleanly. An empty nodeIDs is an unknown set, so never complete.
|
||||
|
||||
@@ -2918,7 +2918,7 @@ func (s *InstanceServiceImpl) deleteInstanceVolumes(ctx context.Context, instanc
|
||||
}
|
||||
// Terminate implies detach: DeleteVolumeOnTerminate clears the stale
|
||||
// AttachedInstance left over from Stop (Unmount deliberately never
|
||||
// clears a Boot volume, daemon/vm_adapters.go) before deleting, so
|
||||
// clears a Boot volume, runtime/daemon/vm_adapters.go) before deleting, so
|
||||
// DeleteVolume's in-use guard does not reject an otherwise-legitimate
|
||||
// delete. The resulting error is surfaced into lastErr, not swallowed.
|
||||
if err := s.volumeDeleter.DeleteVolumeOnTerminate(ctx, ebsRequest.Name, instance.AccountID); err != nil {
|
||||
|
||||
@@ -2034,7 +2034,7 @@ func TestTerminateStoppedInstance_StampsTeardownVolumesFailedOnDeleteError(t *te
|
||||
|
||||
// TestTerminateStoppedInstance_NonDoTBootVolumeDetachedNotDeleted locks the
|
||||
// second half of the terminate-implies-detach fix: a DeleteOnTermination=false
|
||||
// Boot volume is never cleared by Stop's Unmount (daemon/vm_adapters.go), so
|
||||
// Boot volume is never cleared by Stop's Unmount (runtime/daemon/vm_adapters.go), so
|
||||
// without this it would strand attached to the now-gone instance forever.
|
||||
// Terminate must still detach it (AWS semantics: it survives as available,
|
||||
// it just isn't deleted).
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/mulgadc/spinifex/spinifex/daemon"
|
||||
"github.com/mulgadc/spinifex/spinifex/runtime/compute/vm"
|
||||
"github.com/mulgadc/spinifex/spinifex/runtime/daemon"
|
||||
)
|
||||
|
||||
// LocalVMReaderForTest is the local instance state reader's consumer-facing
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/daemon"
|
||||
"github.com/mulgadc/spinifex/spinifex/domains/ec2/guestmetadata"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvstore"
|
||||
"github.com/mulgadc/spinifex/spinifex/runtime/compute/vm"
|
||||
"github.com/mulgadc/spinifex/spinifex/runtime/daemon"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/daemon"
|
||||
"github.com/mulgadc/spinifex/spinifex/domains/ec2/instance/state"
|
||||
"github.com/mulgadc/spinifex/spinifex/runtime/compute/vm"
|
||||
"github.com/mulgadc/spinifex/spinifex/runtime/daemon"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ type faultyKV struct {
|
||||
failDelete func(key string) bool
|
||||
failKeys func() bool
|
||||
failUpdate func(key string) bool
|
||||
failCreate func(key string) bool
|
||||
beforeDelete func(key string)
|
||||
beforeCreate func(key string)
|
||||
beforeUpdate func(key string)
|
||||
@@ -47,6 +48,9 @@ func (k *faultyKV) Create(ctx context.Context, key string, value []byte, opts ..
|
||||
if k.beforeCreate != nil {
|
||||
k.beforeCreate(key)
|
||||
}
|
||||
if k.failCreate != nil && k.failCreate(key) {
|
||||
return 0, errInjected
|
||||
}
|
||||
return k.KeyValue.Create(ctx, key, value, opts...)
|
||||
}
|
||||
|
||||
|
||||
@@ -503,14 +503,21 @@ var errTemplateGone = errors.New("launch template deleted")
|
||||
// after a new body is written. A delete that removed the header at any earlier
|
||||
// revision makes it fail, so the body is never left without its template.
|
||||
func (s *LaunchTemplateServiceImpl) commitVersion(ctx context.Context, accountID, ltID string, entry jetstream.KeyValueEntry) error {
|
||||
return s.commitHeader(ctx, "CreateLaunchTemplateVersion", accountID, ltID, entry, nil)
|
||||
}
|
||||
|
||||
// commitHeader rewrites the header's current value at the revision read,
|
||||
// re-reading on a conflict. A non-nil check vets each re-read header and
|
||||
// stops the commit with its error.
|
||||
func (s *LaunchTemplateServiceImpl) commitHeader(ctx context.Context, op, accountID, ltID string, entry jetstream.KeyValueEntry, check func(*LaunchTemplateHeader) error) error {
|
||||
key := kvutil.AccountKey(accountID, ltID)
|
||||
for range maxVersionRetries {
|
||||
_, err := s.kv.Update(ctx, key, entry.Value(), entry.Revision())
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, jetstream.ErrKeyRevisionMismatch) && !errors.Is(err, jetstream.ErrKeyExists) {
|
||||
slog.ErrorContext(ctx, "CreateLaunchTemplateVersion: header commit failed", "launchTemplateId", ltID, "err", err)
|
||||
if !isRevisionConflict(err) {
|
||||
slog.ErrorContext(ctx, op+": header commit failed", "launchTemplateId", ltID, "err", err)
|
||||
return err
|
||||
}
|
||||
next, gerr := s.kv.Get(ctx, key)
|
||||
@@ -520,11 +527,25 @@ func (s *LaunchTemplateServiceImpl) commitVersion(ctx context.Context, accountID
|
||||
if gerr != nil {
|
||||
return gerr
|
||||
}
|
||||
if check != nil {
|
||||
var h LaunchTemplateHeader
|
||||
if err := json.Unmarshal(next.Value(), &h); err != nil {
|
||||
return fmt.Errorf("decode header: %w", err)
|
||||
}
|
||||
if err := check(&h); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
entry = next
|
||||
}
|
||||
return errors.New("header commit retries exhausted")
|
||||
}
|
||||
|
||||
// isRevisionConflict reports a CAS write that lost to a concurrent writer.
|
||||
func isRevisionConflict(err error) bool {
|
||||
return errors.Is(err, jetstream.ErrKeyRevisionMismatch) || errors.Is(err, jetstream.ErrKeyExists)
|
||||
}
|
||||
|
||||
// abandonVersion removes a body whose header commit failed. A failure is
|
||||
// logged and leaves a headerless body, which no ID, name or launch resolves.
|
||||
func (s *LaunchTemplateServiceImpl) abandonVersion(ctx context.Context, accountID, ltID string, n int64) {
|
||||
@@ -555,23 +576,9 @@ func (s *LaunchTemplateServiceImpl) ModifyLaunchTemplate(ctx context.Context, in
|
||||
}
|
||||
|
||||
if sel := aws.StringValue(input.DefaultVersion); sel != "" {
|
||||
notFound := fmt.Sprintf("The launch template version %s is not found for the specified launch template.", sel)
|
||||
n, err := s.resolveVersionNumber(ctx, accountID, header, sel)
|
||||
header, err = s.setDefaultVersion(ctx, accountID, input, header, entry, sel)
|
||||
if err != nil {
|
||||
return nil, withVersionNotFoundMessage(err, notFound)
|
||||
}
|
||||
// Verify the target body exists immediately before the header CAS so a
|
||||
// concurrent delete degrades to VersionNotFound, never a dangling default.
|
||||
if _, err := s.getVersion(ctx, accountID, header.LaunchTemplateId, n); err != nil {
|
||||
return nil, withVersionNotFoundMessage(err, notFound)
|
||||
}
|
||||
header.DefaultVersionNumber = n
|
||||
data, err := json.Marshal(header)
|
||||
if err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if _, err := s.kv.Update(ctx, kvutil.AccountKey(accountID, header.LaunchTemplateId), data, entry.Revision()); err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,6 +590,48 @@ func (s *LaunchTemplateServiceImpl) ModifyLaunchTemplate(ctx context.Context, in
|
||||
return &ec2.ModifyLaunchTemplateOutput{LaunchTemplate: headerToEC2(header, latest)}, nil
|
||||
}
|
||||
|
||||
// setDefaultVersion moves the header's default to sel under a revision guard.
|
||||
// A header changed meanwhile is re-read and the change retried, bounded; a
|
||||
// header deleted meanwhile is NotFound for the identifier the request used.
|
||||
func (s *LaunchTemplateServiceImpl) setDefaultVersion(ctx context.Context, accountID string, input *ec2.ModifyLaunchTemplateInput, header *LaunchTemplateHeader, entry jetstream.KeyValueEntry, sel string) (*LaunchTemplateHeader, error) {
|
||||
notFound := fmt.Sprintf("The launch template version %s is not found for the specified launch template.", sel)
|
||||
key := kvutil.AccountKey(accountID, header.LaunchTemplateId)
|
||||
for range maxVersionRetries {
|
||||
n, err := s.resolveVersionNumber(ctx, accountID, header, sel)
|
||||
if err != nil {
|
||||
return nil, withVersionNotFoundMessage(err, notFound)
|
||||
}
|
||||
// Verify the target body exists immediately before the header CAS. A
|
||||
// version delete commits through the header too, so a delete that
|
||||
// lands in between fails this CAS and the retry finds no body.
|
||||
if _, err := s.getVersion(ctx, accountID, header.LaunchTemplateId, n); err != nil {
|
||||
return nil, withVersionNotFoundMessage(err, notFound)
|
||||
}
|
||||
header.DefaultVersionNumber = n
|
||||
data, err := json.Marshal(header)
|
||||
if err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
_, err = s.kv.Update(ctx, key, data, entry.Revision())
|
||||
if err == nil {
|
||||
return header, nil
|
||||
}
|
||||
if !isRevisionConflict(err) {
|
||||
slog.ErrorContext(ctx, "ModifyLaunchTemplate: header update failed", "launchTemplateId", header.LaunchTemplateId, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
header, entry, err = s.getHeaderByID(ctx, accountID, header.LaunchTemplateId)
|
||||
if err != nil {
|
||||
if err.Error() == awserrors.ErrorInvalidLaunchTemplateIdNotFound {
|
||||
return nil, templateNotFound(input.LaunchTemplateId, input.LaunchTemplateName)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
slog.ErrorContext(ctx, "ModifyLaunchTemplate: header update retries exhausted", "launchTemplateId", header.LaunchTemplateId, "accountID", accountID)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
// --- DeleteLaunchTemplate ---
|
||||
|
||||
// DeleteLaunchTemplate deletes a template and every version. A store failure
|
||||
@@ -689,7 +738,7 @@ func (s *LaunchTemplateServiceImpl) clearRecordTags(ctx context.Context, account
|
||||
// --- DeleteLaunchTemplateVersions ---
|
||||
|
||||
func (s *LaunchTemplateServiceImpl) DeleteLaunchTemplateVersions(ctx context.Context, input *ec2.DeleteLaunchTemplateVersionsInput, accountID string) (*ec2.DeleteLaunchTemplateVersionsOutput, error) {
|
||||
header, _, err := s.resolveHeader(ctx, accountID, input.LaunchTemplateId, input.LaunchTemplateName)
|
||||
header, entry, err := s.resolveHeader(ctx, accountID, input.LaunchTemplateId, input.LaunchTemplateName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -700,11 +749,12 @@ func (s *LaunchTemplateServiceImpl) DeleteLaunchTemplateVersions(ctx context.Con
|
||||
// Naming the default version fails the whole request, so nothing is deleted.
|
||||
for _, v := range input.Versions {
|
||||
if n, perr := strconv.ParseInt(aws.StringValue(v), 10, 64); perr == nil && n == header.DefaultVersionNumber {
|
||||
return nil, awserrors.Errorf(awserrors.ErrorInvalidParameterValue, "The default version cannot be deleted. Either specify another version as default or delete the launch template.")
|
||||
return nil, errDefaultVersionDelete()
|
||||
}
|
||||
}
|
||||
|
||||
out := &ec2.DeleteLaunchTemplateVersionsOutput{}
|
||||
deleted := map[int64][]byte{}
|
||||
|
||||
for _, v := range input.Versions {
|
||||
sel := aws.StringValue(v)
|
||||
@@ -714,7 +764,8 @@ func (s *LaunchTemplateServiceImpl) DeleteLaunchTemplateVersions(ctx context.Con
|
||||
deleteErrorItem(header, 0, awserrors.ErrorInvalidLaunchTemplateIdVersionNotFound, "invalid version number"))
|
||||
continue
|
||||
}
|
||||
if _, err := s.getVersion(ctx, accountID, header.LaunchTemplateId, n); err != nil {
|
||||
body, err := s.kv.Get(ctx, versionKey(accountID, header.LaunchTemplateId, n))
|
||||
if err != nil {
|
||||
out.UnsuccessfullyDeletedLaunchTemplateVersions = append(out.UnsuccessfullyDeletedLaunchTemplateVersions,
|
||||
deleteErrorItem(header, n, launchTemplateVersionDoesNotExist, "The launch template version does not exist."))
|
||||
continue
|
||||
@@ -724,6 +775,7 @@ func (s *LaunchTemplateServiceImpl) DeleteLaunchTemplateVersions(ctx context.Con
|
||||
deleteErrorItem(header, n, awserrors.ErrorServerInternal, "failed to delete version"))
|
||||
continue
|
||||
}
|
||||
deleted[n] = body.Value()
|
||||
out.SuccessfullyDeletedLaunchTemplateVersions = append(out.SuccessfullyDeletedLaunchTemplateVersions,
|
||||
&ec2.DeleteLaunchTemplateVersionsResponseSuccessItem{
|
||||
LaunchTemplateId: aws.String(header.LaunchTemplateId),
|
||||
@@ -732,10 +784,66 @@ func (s *LaunchTemplateServiceImpl) DeleteLaunchTemplateVersions(ctx context.Con
|
||||
})
|
||||
}
|
||||
|
||||
if len(deleted) > 0 {
|
||||
if err := s.commitVersionDeletes(ctx, accountID, header.LaunchTemplateId, entry, deleted); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, errTemplateGone):
|
||||
return nil, templateNotFound(input.LaunchTemplateId, input.LaunchTemplateName)
|
||||
case errors.Is(err, errDefaultDeleted):
|
||||
return nil, errDefaultVersionDelete()
|
||||
}
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "DeleteLaunchTemplateVersions completed", "launchTemplateId", header.LaunchTemplateId, "deleted", len(out.SuccessfullyDeletedLaunchTemplateVersions), "failed", len(out.UnsuccessfullyDeletedLaunchTemplateVersions), "accountID", accountID)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// errDefaultDeleted: a concurrent change made a deleted version the default.
|
||||
var errDefaultDeleted = errors.New("deleted version became the default")
|
||||
|
||||
func errDefaultVersionDelete() error {
|
||||
return awserrors.Errorf(awserrors.ErrorInvalidParameterValue, "The default version cannot be deleted. Either specify another version as default or delete the launch template.")
|
||||
}
|
||||
|
||||
// commitVersionDeletes commits deleted bodies through the header revision, so
|
||||
// a default change that landed meanwhile is seen. If it names a deleted version,
|
||||
// or the commit fails, the bodies are restored; a deleted template keeps none.
|
||||
func (s *LaunchTemplateServiceImpl) commitVersionDeletes(ctx context.Context, accountID, ltID string, entry jetstream.KeyValueEntry, deleted map[int64][]byte) error {
|
||||
err := s.commitHeader(ctx, "DeleteLaunchTemplateVersions", accountID, ltID, entry, func(h *LaunchTemplateHeader) error {
|
||||
if _, ok := deleted[h.DefaultVersionNumber]; ok {
|
||||
return errDefaultDeleted
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err == nil || errors.Is(err, errTemplateGone) {
|
||||
return err
|
||||
}
|
||||
restoreFailed := false
|
||||
for n, body := range deleted {
|
||||
if _, cerr := s.kv.Create(ctx, versionKey(accountID, ltID, n), body); cerr != nil {
|
||||
restoreFailed = true
|
||||
slog.ErrorContext(ctx, "DeleteLaunchTemplateVersions: failed to restore a version", "launchTemplateId", ltID, "version", n, "err", cerr)
|
||||
}
|
||||
}
|
||||
// A template delete that removed the header before the restore has already
|
||||
// swept its bodies, so the restored ones would be left headerless.
|
||||
if _, gerr := s.kv.Get(ctx, kvutil.AccountKey(accountID, ltID)); errors.Is(gerr, jetstream.ErrKeyNotFound) {
|
||||
for n := range deleted {
|
||||
if derr := s.kv.Delete(ctx, versionKey(accountID, ltID, n)); derr != nil {
|
||||
slog.ErrorContext(ctx, "DeleteLaunchTemplateVersions: failed to remove a restored version", "launchTemplateId", ltID, "version", n, "err", derr)
|
||||
}
|
||||
}
|
||||
return errTemplateGone
|
||||
}
|
||||
// A version left deleted makes the default-version refusal untrue.
|
||||
if restoreFailed {
|
||||
return errors.New("restore deleted versions failed")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// launchTemplateVersionDoesNotExist is the per-version code AWS reports in a
|
||||
// DeleteLaunchTemplateVersions result; it is never a request-level error.
|
||||
const launchTemplateVersionDoesNotExist = "launchTemplateVersionDoesNotExist"
|
||||
|
||||
@@ -233,3 +233,343 @@ func TestAccountTemplateReferences_FailsClosed(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A default-version change whose header commit loses to a whole delete fails
|
||||
// with the NotFound code for whichever identifier the request used.
|
||||
func TestModifyLaunchTemplate_RacingADeleteIsNotFound(t *testing.T) {
|
||||
for _, byName := range []bool{false, true} {
|
||||
t.Run(map[bool]string{false: "by-id", true: "by-name"}[byName], func(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
header := kvutil.AccountKey(testAccountID, id)
|
||||
raced := false
|
||||
svc.kv = &faultyKV{KeyValue: svc.kv, beforeUpdate: func(key string) {
|
||||
if key == header && !raced {
|
||||
raced = true
|
||||
raceDelete(t, svc, id)
|
||||
}
|
||||
}}
|
||||
in := &ec2.ModifyLaunchTemplateInput{DefaultVersion: aws.String("2")}
|
||||
want := awserrors.ErrorInvalidLaunchTemplateIdNotFound
|
||||
if byName {
|
||||
in.LaunchTemplateName = aws.String("web")
|
||||
want = awserrors.ErrorInvalidLaunchTemplateNameNotFoundException
|
||||
} else {
|
||||
in.LaunchTemplateId = aws.String(id)
|
||||
}
|
||||
|
||||
_, err := svc.ModifyLaunchTemplate(context.Background(), in, testAccountID)
|
||||
require.True(t, raced)
|
||||
require.Error(t, err)
|
||||
code, _ := awserrors.ResolveErrorCode(err)
|
||||
assert.Equal(t, want, code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A default-version change racing another header change retries at the new
|
||||
// revision and its own default is the one that lands.
|
||||
func TestModifyLaunchTemplate_RetriesAConcurrentHeaderChange(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
_, err := svc.CreateLaunchTemplateVersion(context.Background(), newVersionInput(id), testAccountID) // v3
|
||||
require.NoError(t, err)
|
||||
inner := svc.kv
|
||||
header := kvutil.AccountKey(testAccountID, id)
|
||||
raced := false
|
||||
svc.kv = &faultyKV{KeyValue: inner, beforeUpdate: func(key string) {
|
||||
if key != header || raced {
|
||||
return
|
||||
}
|
||||
raced = true
|
||||
other := &LaunchTemplateServiceImpl{kv: inner}
|
||||
_, err := other.ModifyLaunchTemplate(context.Background(), &ec2.ModifyLaunchTemplateInput{
|
||||
LaunchTemplateId: aws.String(id), DefaultVersion: aws.String("2"),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
}}
|
||||
|
||||
out, err := svc.ModifyLaunchTemplate(context.Background(), &ec2.ModifyLaunchTemplateInput{
|
||||
LaunchTemplateId: aws.String(id), DefaultVersion: aws.String("3"),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, raced)
|
||||
assert.Equal(t, int64(3), aws.Int64Value(out.LaunchTemplate.DefaultVersionNumber))
|
||||
h, _, err := svc.getHeaderByID(context.Background(), testAccountID, id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), h.DefaultVersionNumber)
|
||||
}
|
||||
|
||||
// A header that changes before every commit attempt exhausts the bounded
|
||||
// retries and fails InternalError rather than spinning.
|
||||
func TestModifyLaunchTemplate_ConflictRetriesAreBounded(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
inner := svc.kv
|
||||
header := kvutil.AccountKey(testAccountID, id)
|
||||
attempts := 0
|
||||
svc.kv = &faultyKV{KeyValue: inner, beforeUpdate: func(key string) {
|
||||
if key != header {
|
||||
return
|
||||
}
|
||||
attempts++
|
||||
entry, err := inner.Get(context.Background(), header)
|
||||
require.NoError(t, err)
|
||||
_, err = inner.Put(context.Background(), header, entry.Value())
|
||||
require.NoError(t, err)
|
||||
}}
|
||||
|
||||
_, err := svc.ModifyLaunchTemplate(context.Background(), &ec2.ModifyLaunchTemplateInput{
|
||||
LaunchTemplateId: aws.String(id), DefaultVersion: aws.String("2"),
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
assert.Equal(t, maxVersionRetries, attempts)
|
||||
}
|
||||
|
||||
// A conflict whose header re-read fails at the store is InternalError: an
|
||||
// unreadable header is not proof the template was deleted.
|
||||
func TestModifyLaunchTemplate_ConflictReReadFailureIsInternal(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
inner := svc.kv
|
||||
header := kvutil.AccountKey(testAccountID, id)
|
||||
conflicted := false
|
||||
svc.kv = &faultyKV{
|
||||
KeyValue: inner,
|
||||
beforeUpdate: func(key string) {
|
||||
if key != header || conflicted {
|
||||
return
|
||||
}
|
||||
conflicted = true
|
||||
entry, err := inner.Get(context.Background(), header)
|
||||
require.NoError(t, err)
|
||||
_, err = inner.Put(context.Background(), header, entry.Value())
|
||||
require.NoError(t, err)
|
||||
},
|
||||
failGet: func(key string) bool { return key == header && conflicted },
|
||||
}
|
||||
|
||||
_, err := svc.ModifyLaunchTemplate(context.Background(), &ec2.ModifyLaunchTemplateInput{
|
||||
LaunchTemplateId: aws.String(id), DefaultVersion: aws.String("2"),
|
||||
}, testAccountID)
|
||||
require.True(t, conflicted)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
}
|
||||
|
||||
func setDefault(svc *LaunchTemplateServiceImpl, id, version string) error {
|
||||
_, err := svc.ModifyLaunchTemplate(context.Background(), &ec2.ModifyLaunchTemplateInput{
|
||||
LaunchTemplateId: aws.String(id), DefaultVersion: aws.String(version),
|
||||
}, testAccountID)
|
||||
return err
|
||||
}
|
||||
|
||||
func deleteVersions(svc *LaunchTemplateServiceImpl, id string, versions ...string) (*ec2.DeleteLaunchTemplateVersionsOutput, error) {
|
||||
return svc.DeleteLaunchTemplateVersions(context.Background(), &ec2.DeleteLaunchTemplateVersionsInput{
|
||||
LaunchTemplateId: aws.String(id), Versions: aws.StringSlice(versions),
|
||||
}, testAccountID)
|
||||
}
|
||||
|
||||
// requireDefaultResolves asserts the header's default version has a body.
|
||||
func requireDefaultResolves(t *testing.T, svc *LaunchTemplateServiceImpl, id string, want int64) {
|
||||
t.Helper()
|
||||
h, _, err := svc.getHeaderByID(context.Background(), testAccountID, id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, h.DefaultVersionNumber)
|
||||
_, err = svc.getVersion(context.Background(), testAccountID, id, h.DefaultVersionNumber)
|
||||
assert.NoError(t, err, "the default version has a body")
|
||||
}
|
||||
|
||||
// A default change that commits while a version delete is removing the new
|
||||
// default makes the delete fail as a default-version delete and restore it.
|
||||
func TestDeleteVersionsRacingADefaultChangeKeepsTheDefault(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
inner := svc.kv
|
||||
raced := false
|
||||
svc.kv = &faultyKV{KeyValue: inner, beforeDelete: func(key string) {
|
||||
if key != versionKey(testAccountID, id, 2) || raced {
|
||||
return
|
||||
}
|
||||
raced = true
|
||||
require.NoError(t, setDefault(&LaunchTemplateServiceImpl{kv: inner}, id, "2"))
|
||||
}}
|
||||
|
||||
_, err := deleteVersions(svc, id, "2")
|
||||
require.True(t, raced)
|
||||
require.Error(t, err)
|
||||
code, _ := awserrors.ResolveErrorCode(err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidParameterValue, code)
|
||||
requireDefaultResolves(t, svc, id, 2)
|
||||
}
|
||||
|
||||
// A default change paused before its header write, while the target version
|
||||
// is deleted, fails VersionNotFound and leaves the old default.
|
||||
func TestDefaultChangeRacingAVersionDeleteFailsNotFound(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
inner := svc.kv
|
||||
header := kvutil.AccountKey(testAccountID, id)
|
||||
raced := false
|
||||
svc.kv = &faultyKV{KeyValue: inner, beforeUpdate: func(key string) {
|
||||
if key != header || raced {
|
||||
return
|
||||
}
|
||||
raced = true
|
||||
out, err := deleteVersions(&LaunchTemplateServiceImpl{kv: inner}, id, "2")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.SuccessfullyDeletedLaunchTemplateVersions, 1)
|
||||
}}
|
||||
|
||||
err := setDefault(svc, id, "2")
|
||||
require.True(t, raced)
|
||||
require.Error(t, err)
|
||||
code, _ := awserrors.ResolveErrorCode(err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidLaunchTemplateIdVersionNotFound, code)
|
||||
requireDefaultResolves(t, svc, id, 1)
|
||||
}
|
||||
|
||||
// A default change to a version the delete does not name lets the delete
|
||||
// commit at the new header revision and keeps the change.
|
||||
func TestDeleteVersionsKeepsAnUnrelatedDefaultChange(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
addVersion(t, svc, id, "t3.large", "") // v3
|
||||
inner := svc.kv
|
||||
raced := false
|
||||
svc.kv = &faultyKV{KeyValue: inner, beforeDelete: func(key string) {
|
||||
if key != versionKey(testAccountID, id, 2) || raced {
|
||||
return
|
||||
}
|
||||
raced = true
|
||||
require.NoError(t, setDefault(&LaunchTemplateServiceImpl{kv: inner}, id, "3"))
|
||||
}}
|
||||
|
||||
out, err := deleteVersions(svc, id, "2")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, raced)
|
||||
require.Len(t, out.SuccessfullyDeletedLaunchTemplateVersions, 1)
|
||||
requireDefaultResolves(t, svc, id, 3)
|
||||
_, err = svc.getVersion(context.Background(), testAccountID, id, 2)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// A version delete whose header commit fails restores the bodies it removed.
|
||||
func TestFailedVersionDeleteCommitRestoresTheBodies(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
before := accountKeys(t, svc.kv, testAccountID)
|
||||
header := kvutil.AccountKey(testAccountID, id)
|
||||
svc.kv = &faultyKV{KeyValue: svc.kv, failUpdate: func(key string) bool { return key == header }}
|
||||
|
||||
_, err := deleteVersions(svc, id, "2")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
assert.Equal(t, before, accountKeys(t, svc.kv, testAccountID))
|
||||
}
|
||||
|
||||
// A template deleted while its versions are being deleted fails NotFound and
|
||||
// leaves nothing behind.
|
||||
func TestVersionDeleteRacingATemplateDeleteLeavesNothing(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
header := kvutil.AccountKey(testAccountID, id)
|
||||
raced := false
|
||||
svc.kv = &faultyKV{KeyValue: svc.kv, beforeUpdate: func(key string) {
|
||||
if key != header || raced {
|
||||
return
|
||||
}
|
||||
raced = true
|
||||
raceDelete(t, svc, id)
|
||||
}}
|
||||
|
||||
_, err := deleteVersions(svc, id, "2")
|
||||
require.True(t, raced)
|
||||
require.Error(t, err)
|
||||
code, _ := awserrors.ResolveErrorCode(err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidLaunchTemplateIdNotFound, code)
|
||||
assert.Empty(t, accountKeys(t, svc.kv, testAccountID))
|
||||
}
|
||||
|
||||
// A template deleted after a version delete lost to a default change, but
|
||||
// before it restored the bodies, is not left with restored headerless bodies.
|
||||
func TestRestoredVersionsAfterATemplateDeleteLeaveNothing(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
inner := svc.kv
|
||||
v2 := versionKey(testAccountID, id, 2)
|
||||
defaulted, deleted := false, false
|
||||
svc.kv = &faultyKV{KeyValue: inner,
|
||||
beforeDelete: func(key string) {
|
||||
if key != v2 || defaulted {
|
||||
return
|
||||
}
|
||||
defaulted = true
|
||||
require.NoError(t, setDefault(&LaunchTemplateServiceImpl{kv: inner}, id, "2"))
|
||||
},
|
||||
beforeCreate: func(key string) {
|
||||
if key != v2 || deleted {
|
||||
return
|
||||
}
|
||||
deleted = true
|
||||
raceDelete(t, svc, id)
|
||||
},
|
||||
}
|
||||
|
||||
_, err := deleteVersions(svc, id, "2")
|
||||
require.True(t, deleted)
|
||||
require.Error(t, err)
|
||||
code, _ := awserrors.ResolveErrorCode(err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidLaunchTemplateIdNotFound, code)
|
||||
assert.Empty(t, accountKeys(t, inner, testAccountID))
|
||||
}
|
||||
|
||||
// A default change whose header write loses to a version create re-reads the
|
||||
// header and commits at its new revision.
|
||||
func TestDefaultChangeRetriesAfterAConcurrentVersionCreate(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
inner := svc.kv
|
||||
header := kvutil.AccountKey(testAccountID, id)
|
||||
raced := false
|
||||
svc.kv = &faultyKV{KeyValue: inner, beforeUpdate: func(key string) {
|
||||
if key != header || raced {
|
||||
return
|
||||
}
|
||||
raced = true
|
||||
_, err := (&LaunchTemplateServiceImpl{kv: inner}).CreateLaunchTemplateVersion(context.Background(), newVersionInput(id), testAccountID)
|
||||
require.NoError(t, err)
|
||||
}}
|
||||
|
||||
require.NoError(t, setDefault(svc, id, "2"))
|
||||
require.True(t, raced)
|
||||
requireDefaultResolves(t, svc, id, 2)
|
||||
_, err := svc.getVersion(context.Background(), testAccountID, id, 3)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// A version delete that loses to a default change but cannot restore the
|
||||
// version fails InternalError, not the refusal that implies nothing was deleted.
|
||||
func TestUnrestoredVersionDeleteFailsInternalError(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
id := createWithUserData(t, svc, "web")
|
||||
inner := svc.kv
|
||||
v2 := versionKey(testAccountID, id, 2)
|
||||
raced := false
|
||||
svc.kv = &faultyKV{KeyValue: inner,
|
||||
beforeDelete: func(key string) {
|
||||
if key != v2 || raced {
|
||||
return
|
||||
}
|
||||
raced = true
|
||||
require.NoError(t, setDefault(&LaunchTemplateServiceImpl{kv: inner}, id, "2"))
|
||||
},
|
||||
failCreate: func(key string) bool { return key == v2 },
|
||||
}
|
||||
|
||||
_, err := deleteVersions(svc, id, "2")
|
||||
require.True(t, raced)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ func volumeAttachment(ctx context.Context, store objectstore.ObjectStore, bucket
|
||||
// volume-to-node resolution is needed here.
|
||||
//
|
||||
// An attachment record is not proof of a live writer: stop deliberately leaves
|
||||
// a boot volume attached (daemon/vm_adapters.go's volumeMounterAdapter.Unmount)
|
||||
// a boot volume attached (runtime/daemon/vm_adapters.go's volumeMounterAdapter.Unmount)
|
||||
// while tearing down both the NBD plugin and this subscription. The two
|
||||
// not-running signals below are therefore the stopped-instance path, not a
|
||||
// failure — treating them as one would make every stopped instance's root
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestDeleteVolume_CrossAccountDocumentIsStillNotFound(t *testing.T) {
|
||||
// UpdateVolumeState succeeds, but the document goes unreadable before
|
||||
// DeleteVolume's own read of the same key. That read failure must come back
|
||||
// as a non-NotFound error, which is exactly the signal
|
||||
// daemon/vm_adapters.go's DeleteVolumes and instance/service_impl.go's
|
||||
// runtime/daemon/vm_adapters.go's DeleteVolumes and instance/service_impl.go's
|
||||
// deleteInstanceVolumes use to tell a real failure from an already-gone
|
||||
// volume (!awserrors.IsNotFound(err) && !objectstore.IsNoSuchKeyError(err)).
|
||||
func TestDeleteVolumeOnTerminate_DeleteReadFailureIsNotSuccess(t *testing.T) {
|
||||
|
||||
@@ -1742,7 +1742,7 @@ func TestDeleteVolume_EmptyStateUnattachedDeletable(t *testing.T) {
|
||||
// TestDeleteVolumeOnTerminate_ClearsAttachmentThenDeletes locks the
|
||||
// A DeleteOnTermination root volume left attached to a
|
||||
// stopped instance (Stop's Unmount deliberately never clears a Boot volume's
|
||||
// AttachedInstance, daemon/vm_adapters.go) hits DeleteVolume's in-use guard
|
||||
// AttachedInstance, runtime/daemon/vm_adapters.go) hits DeleteVolume's in-use guard
|
||||
// directly — see TestDeleteVolume_VolumeAttachedButAvailable above.
|
||||
// DeleteVolumeOnTerminate must clear the stale attachment first (terminate
|
||||
// implies detach) so the terminate delete actually succeeds instead of
|
||||
|
||||
@@ -591,7 +591,7 @@ func (s *VPCServiceImpl) rejectAttachedIGW(ctx context.Context, accountID, vpcID
|
||||
|
||||
// describeVpcsValidFilters defines the set of filter names accepted by DescribeVpcs.
|
||||
// cidr-block and is-default are the spellings an internal caller
|
||||
// (handlers/rds/network.go) is pinned to; cidr and isDefault are the
|
||||
// (domains/rds/instance/network.go) is pinned to; cidr and isDefault are the
|
||||
// spellings AWS itself sends, so both must be accepted side by side.
|
||||
var describeVpcsValidFilters = map[string]bool{
|
||||
"vpc-id": true,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ func CreateAccessEntry(ctx context.Context, natsConn *nats.Conn, accountID, clus
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).CreateAccessEntry(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).CreateAccessEntry(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DescribeAccessEntry — GET /clusters/{name}/access-entries/{arn}.
|
||||
@@ -24,13 +24,13 @@ func DescribeAccessEntry(ctx context.Context, natsConn *nats.Conn, accountID, cl
|
||||
ClusterName: aws.String(cluster),
|
||||
PrincipalArn: aws.String(principalARN),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DescribeAccessEntry(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).DescribeAccessEntry(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListAccessEntries — GET /clusters/{name}/access-entries.
|
||||
func ListAccessEntries(ctx context.Context, natsConn *nats.Conn, accountID, cluster string) (*eks.ListAccessEntriesOutput, error) {
|
||||
input := &eks.ListAccessEntriesInput{ClusterName: aws.String(cluster)}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListAccessEntries(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).ListAccessEntries(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// UpdateAccessEntry — POST /clusters/{name}/access-entries/{arn}.
|
||||
@@ -41,7 +41,7 @@ func UpdateAccessEntry(ctx context.Context, natsConn *nats.Conn, accountID, clus
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
input.PrincipalArn = aws.String(principalARN)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).UpdateAccessEntry(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).UpdateAccessEntry(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DeleteAccessEntry — DELETE /clusters/{name}/access-entries/{arn}.
|
||||
@@ -50,7 +50,7 @@ func DeleteAccessEntry(ctx context.Context, natsConn *nats.Conn, accountID, clus
|
||||
ClusterName: aws.String(cluster),
|
||||
PrincipalArn: aws.String(principalARN),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DeleteAccessEntry(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).DeleteAccessEntry(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// AssociateAccessPolicy — POST /clusters/{name}/access-entries/{arn}/access-policies.
|
||||
@@ -61,7 +61,7 @@ func AssociateAccessPolicy(ctx context.Context, natsConn *nats.Conn, accountID,
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
input.PrincipalArn = aws.String(principalARN)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).AssociateAccessPolicy(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).AssociateAccessPolicy(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DisassociateAccessPolicy — DELETE /clusters/{name}/access-entries/{arn}/access-policies/{policyArn}.
|
||||
@@ -71,7 +71,7 @@ func DisassociateAccessPolicy(ctx context.Context, natsConn *nats.Conn, accountI
|
||||
PrincipalArn: aws.String(principalARN),
|
||||
PolicyArn: aws.String(policyARN),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DisassociateAccessPolicy(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).DisassociateAccessPolicy(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListAssociatedAccessPolicies — GET /clusters/{name}/access-entries/{arn}/access-policies.
|
||||
@@ -80,10 +80,10 @@ func ListAssociatedAccessPolicies(ctx context.Context, natsConn *nats.Conn, acco
|
||||
ClusterName: aws.String(cluster),
|
||||
PrincipalArn: aws.String(principalARN),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListAssociatedAccessPolicies(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).ListAssociatedAccessPolicies(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListAccessPolicies — GET /access-policies.
|
||||
func ListAccessPolicies(ctx context.Context, natsConn *nats.Conn, accountID string) (*eks.ListAccessPoliciesOutput, error) {
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListAccessPolicies(ctx, &eks.ListAccessPoliciesInput{}, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).ListAccessPolicies(ctx, &eks.ListAccessPoliciesInput{}, accountID)
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// ListAddons — GET /clusters/{name}/addons.
|
||||
func ListAddons(ctx context.Context, natsConn *nats.Conn, accountID, cluster string) (*eks.ListAddonsOutput, error) {
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListAddons(ctx, &eks.ListAddonsInput{
|
||||
return ekscluster.NewNATSEKSService(natsConn).ListAddons(ctx, &eks.ListAddonsInput{
|
||||
ClusterName: aws.String(cluster),
|
||||
}, accountID)
|
||||
}
|
||||
|
||||
// DescribeAddonVersions — GET /addons/supported-versions.
|
||||
func DescribeAddonVersions(ctx context.Context, natsConn *nats.Conn, accountID string) (*eks.DescribeAddonVersionsOutput, error) {
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DescribeAddonVersions(ctx, &eks.DescribeAddonVersionsInput{}, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).DescribeAddonVersions(ctx, &eks.DescribeAddonVersionsInput{}, accountID)
|
||||
}
|
||||
|
||||
// CreateAddon — POST /clusters/{name}/addons.
|
||||
@@ -27,7 +27,7 @@ func CreateAddon(ctx context.Context, natsConn *nats.Conn, accountID, cluster st
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).CreateAddon(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).CreateAddon(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DeleteAddon — DELETE /clusters/{name}/addons/{addon}.
|
||||
@@ -36,7 +36,7 @@ func DeleteAddon(ctx context.Context, natsConn *nats.Conn, accountID, cluster, a
|
||||
ClusterName: aws.String(cluster),
|
||||
AddonName: aws.String(addon),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DeleteAddon(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).DeleteAddon(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DescribeAddon — GET /clusters/{name}/addons/{addon}.
|
||||
@@ -45,7 +45,7 @@ func DescribeAddon(ctx context.Context, natsConn *nats.Conn, accountID, cluster,
|
||||
ClusterName: aws.String(cluster),
|
||||
AddonName: aws.String(addon),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DescribeAddon(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).DescribeAddon(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// UpdateAddon — POST /clusters/{name}/addons/{addon}/update.
|
||||
@@ -56,5 +56,5 @@ func UpdateAddon(ctx context.Context, natsConn *nats.Conn, accountID, cluster, a
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
input.AddonName = aws.String(addon)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).UpdateAddon(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).UpdateAddon(ctx, input, accountID)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,11 +1,11 @@
|
||||
package gateway_eks_test
|
||||
package awsapi_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/domains/eks/access"
|
||||
eksawsapi "github.com/mulgadc/spinifex/spinifex/domains/eks/awsapi"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
|
||||
gateway_eks "github.com/mulgadc/spinifex/spinifex/gateway/eks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
|
||||
func resolveARNs(t *testing.T, action string, params []string, body string) []string {
|
||||
t.Helper()
|
||||
resources, err := gateway_eks.ResourceARNs(action, authzRegion, authzAccountID, params, []byte(body))
|
||||
resources, err := eksawsapi.ResourceARNs(action, authzRegion, authzAccountID, params, []byte(body))
|
||||
require.NoError(t, err)
|
||||
return resources
|
||||
}
|
||||
@@ -206,6 +206,6 @@ func TestResourceARNsIdentifierIsAValue(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResourceARNsUnknownActionFailsClosed(t *testing.T) {
|
||||
_, err := gateway_eks.ResourceARNs("NotAnAction", authzRegion, authzAccountID, nil, nil)
|
||||
_, err := eksawsapi.ResourceARNs("NotAnAction", authzRegion, authzAccountID, nil, nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -17,24 +17,24 @@ func CreateCluster(ctx context.Context, natsConn *nats.Conn, accountID, callerPr
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).CreateCluster(ctx, input, accountID, callerPrincipalARN)
|
||||
return ekscluster.NewNATSEKSService(natsConn).CreateCluster(ctx, input, accountID, callerPrincipalARN)
|
||||
}
|
||||
|
||||
// DescribeCluster — GET /clusters/{name}.
|
||||
func DescribeCluster(ctx context.Context, natsConn *nats.Conn, accountID, name string) (*eks.DescribeClusterOutput, error) {
|
||||
input := &eks.DescribeClusterInput{Name: aws.String(name)}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DescribeCluster(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).DescribeCluster(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListClusters — GET /clusters.
|
||||
func ListClusters(ctx context.Context, natsConn *nats.Conn, accountID string) (*eks.ListClustersOutput, error) {
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListClusters(ctx, &eks.ListClustersInput{}, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).ListClusters(ctx, &eks.ListClustersInput{}, accountID)
|
||||
}
|
||||
|
||||
// DeleteCluster — DELETE /clusters/{name}.
|
||||
func DeleteCluster(ctx context.Context, natsConn *nats.Conn, accountID, name string) (*eks.DeleteClusterOutput, error) {
|
||||
input := &eks.DeleteClusterInput{Name: aws.String(name)}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DeleteCluster(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).DeleteCluster(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// unmarshalIfBody decodes body into out only when non-empty.
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gateway_eks is the HTTP-side glue between awsgw and the EKS handlers.
|
||||
// Package awsapi is the HTTP-side glue between awsgw and the EKS handlers.
|
||||
// EKS speaks AWS REST-JSON 1.1; error envelopes are JSON and Content-Type is
|
||||
// application/x-amz-json-1.1 throughout.
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
@@ -1,4 +1,4 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
eksv1 "github.com/mulgadc/spinifex/contracts/eks/v1"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -24,8 +24,8 @@ func ListInternalAddons(ctx context.Context, natsConn *nats.Conn, clusterName, a
|
||||
if clusterName == "" || accountID == "" {
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
out, err := handlers_eks.NewNATSEKSService(natsConn).ListStagedAddonManifests(ctx,
|
||||
&handlers_eks.ListStagedAddonManifestsInput{ClusterName: clusterName}, accountID)
|
||||
out, err := ekscluster.NewNATSEKSService(natsConn).ListStagedAddonManifests(ctx,
|
||||
&ekscluster.ListStagedAddonManifestsInput{ClusterName: clusterName}, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
@@ -125,7 +125,7 @@ var internalBodyAccounts = map[string]func(body []byte) (string, error){
|
||||
func requireCPAgent(ctx context.Context, action string, caller Caller) error {
|
||||
if caller.PrincipalType != principalTypeAssumedRole ||
|
||||
caller.AccountID != awsidentifiers.GlobalAccountID ||
|
||||
caller.RoleName != handlers_eks.CPInstanceRoleName ||
|
||||
caller.RoleName != ekscluster.CPInstanceRoleName ||
|
||||
caller.SessionName == "" {
|
||||
slog.WarnContext(ctx, "EKS: internal route rejected for non-CP-agent caller",
|
||||
"action", action, "principalType", caller.PrincipalType,
|
||||
@@ -140,7 +140,7 @@ func requireCPAgent(ctx context.Context, action string, caller Caller) error {
|
||||
// for the bucket handle. A nil meta is returned for an account with no clusters,
|
||||
// for a cluster that is absent, and for a name no bucket or key can hold; all
|
||||
// are denials rather than failures.
|
||||
func lookupClusterMeta(ctx context.Context, natsConn *nats.Conn, accountID, clusterName string) (*handlers_eks.ClusterMeta, error) {
|
||||
func lookupClusterMeta(ctx context.Context, natsConn *nats.Conn, accountID, clusterName string) (*ekscluster.ClusterMeta, error) {
|
||||
if natsConn == nil {
|
||||
return nil, errors.New("gateway NATS connection not initialised")
|
||||
}
|
||||
@@ -148,21 +148,21 @@ func lookupClusterMeta(ctx context.Context, natsConn *nats.Conn, accountID, clus
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kv, err := js.KeyValue(ctx, handlers_eks.AccountBucketName(accountID))
|
||||
kv, err := js.KeyValue(ctx, ekscluster.AccountBucketName(accountID))
|
||||
if err != nil {
|
||||
if errors.Is(err, jetstream.ErrBucketNotFound) || errors.Is(err, jetstream.ErrInvalidBucketName) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
entry, err := kv.Get(ctx, handlers_eks.ClusterMetaKey(clusterName))
|
||||
entry, err := kv.Get(ctx, ekscluster.ClusterMetaKey(clusterName))
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) || errors.Is(err, jetstream.ErrInvalidKey) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var meta handlers_eks.ClusterMeta
|
||||
var meta ekscluster.ClusterMeta
|
||||
if err := json.Unmarshal(entry.Value(), &meta); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal cluster meta %s: %w", clusterName, err)
|
||||
}
|
||||
+46
-46
@@ -1,13 +1,13 @@
|
||||
package gateway_eks_test
|
||||
package awsapi_test
|
||||
|
||||
import (
|
||||
awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers"
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/internal/testkit"
|
||||
eksawsapi "github.com/mulgadc/spinifex/spinifex/domains/eks/awsapi"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
gateway_eks "github.com/mulgadc/spinifex/spinifex/gateway/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -26,25 +26,25 @@ const (
|
||||
)
|
||||
|
||||
// cpAgent is the principal an IMDS-credentialed control-plane VM presents.
|
||||
func cpAgent(instanceID string) gateway_eks.Caller {
|
||||
return gateway_eks.Caller{
|
||||
func cpAgent(instanceID string) eksawsapi.Caller {
|
||||
return eksawsapi.Caller{
|
||||
AccountID: awsidentifiers.GlobalAccountID,
|
||||
PrincipalType: principalTypeAssumedRole,
|
||||
RoleName: handlers_eks.CPInstanceRoleName,
|
||||
RoleName: ekscluster.CPInstanceRoleName,
|
||||
SessionName: instanceID,
|
||||
}
|
||||
}
|
||||
|
||||
// seedCluster writes a cluster meta whose control plane is the given members.
|
||||
func seedCluster(t *testing.T, nc *nats.Conn, accountID, cluster string, meta *handlers_eks.ClusterMeta) {
|
||||
func seedCluster(t *testing.T, nc *nats.Conn, accountID, cluster string, meta *ekscluster.ClusterMeta) {
|
||||
t.Helper()
|
||||
js := testutil.NewJetStream(t, nc)
|
||||
kv, err := js.CreateKeyValue(t.Context(), jetstream.KeyValueConfig{
|
||||
Bucket: handlers_eks.AccountBucketName(accountID),
|
||||
Bucket: ekscluster.AccountBucketName(accountID),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
meta.Name = cluster
|
||||
require.NoError(t, handlers_eks.PutClusterMeta(t.Context(), kv, meta))
|
||||
require.NoError(t, ekscluster.PutClusterMeta(t.Context(), kv, meta))
|
||||
}
|
||||
|
||||
func publishBody(accountID string) []byte {
|
||||
@@ -67,41 +67,41 @@ func assertDenied(t *testing.T, err error) {
|
||||
func TestAuthorizeInternal_RejectsNonCPPrincipals(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
caller gateway_eks.Caller
|
||||
caller eksawsapi.Caller
|
||||
}{
|
||||
{"tenant user with eks:*", gateway_eks.Caller{
|
||||
{"tenant user with eks:*", eksawsapi.Caller{
|
||||
AccountID: tenantAccount, PrincipalType: "user", SessionName: "alice",
|
||||
}},
|
||||
{"tenant role session", gateway_eks.Caller{
|
||||
{"tenant role session", eksawsapi.Caller{
|
||||
AccountID: tenantAccount,
|
||||
PrincipalType: principalTypeAssumedRole,
|
||||
RoleName: handlers_eks.CPInstanceRoleName,
|
||||
RoleName: ekscluster.CPInstanceRoleName,
|
||||
SessionName: cpInstanceID,
|
||||
}},
|
||||
{"system user, not a role session", gateway_eks.Caller{
|
||||
{"system user, not a role session", eksawsapi.Caller{
|
||||
AccountID: awsidentifiers.GlobalAccountID, PrincipalType: "user", SessionName: "root",
|
||||
}},
|
||||
{"another role in the system account", gateway_eks.Caller{
|
||||
{"another role in the system account", eksawsapi.Caller{
|
||||
AccountID: awsidentifiers.GlobalAccountID,
|
||||
PrincipalType: principalTypeAssumedRole,
|
||||
RoleName: "spinifex-rds-instance",
|
||||
SessionName: cpInstanceID,
|
||||
}},
|
||||
{"no session name", gateway_eks.Caller{
|
||||
{"no session name", eksawsapi.Caller{
|
||||
AccountID: awsidentifiers.GlobalAccountID,
|
||||
PrincipalType: principalTypeAssumedRole,
|
||||
RoleName: handlers_eks.CPInstanceRoleName,
|
||||
RoleName: ekscluster.CPInstanceRoleName,
|
||||
}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assertDenied(t, gateway_eks.AuthorizeInternal(t.Context(), nil, "ListInternalAddons",
|
||||
assertDenied(t, eksawsapi.AuthorizeInternal(t.Context(), nil, "ListInternalAddons",
|
||||
tc.caller, []string{"alpha", tenantAccount}, nil))
|
||||
assertDenied(t, gateway_eks.AuthorizeInternal(t.Context(), nil, "GetRecoveryDirective",
|
||||
assertDenied(t, eksawsapi.AuthorizeInternal(t.Context(), nil, "GetRecoveryDirective",
|
||||
tc.caller, []string{"alpha", tenantAccount, cpInstanceID}, nil))
|
||||
assertDenied(t, gateway_eks.AuthorizeInternal(t.Context(), nil, "PublishInternal",
|
||||
assertDenied(t, eksawsapi.AuthorizeInternal(t.Context(), nil, "PublishInternal",
|
||||
tc.caller, []string{"alpha"}, publishBody(tenantAccount)))
|
||||
assertDenied(t, gateway_eks.AuthorizeInternal(t.Context(), nil, "WebhookTokenReview",
|
||||
assertDenied(t, eksawsapi.AuthorizeInternal(t.Context(), nil, "WebhookTokenReview",
|
||||
tc.caller, []string{"alpha"}, tokenReviewBody(tenantAccount)))
|
||||
})
|
||||
}
|
||||
@@ -109,26 +109,26 @@ func TestAuthorizeInternal_RejectsNonCPPrincipals(t *testing.T) {
|
||||
|
||||
// Actions outside the internal set keep their own gating; this one adds none.
|
||||
func TestAuthorizeInternal_IgnoresOtherActions(t *testing.T) {
|
||||
assert.False(t, gateway_eks.IsInternalAction("DescribeCluster"))
|
||||
require.NoError(t, gateway_eks.AuthorizeInternal(t.Context(), nil, "DescribeCluster",
|
||||
gateway_eks.Caller{AccountID: tenantAccount, PrincipalType: "user"}, []string{"alpha"}, nil))
|
||||
assert.False(t, eksawsapi.IsInternalAction("DescribeCluster"))
|
||||
require.NoError(t, eksawsapi.AuthorizeInternal(t.Context(), nil, "DescribeCluster",
|
||||
eksawsapi.Caller{AccountID: tenantAccount, PrincipalType: "user"}, []string{"alpha"}, nil))
|
||||
}
|
||||
|
||||
func TestAuthorizeInternal_AllowsClusterMember(t *testing.T) {
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
seedCluster(t, nc, tenantAccount, "alpha", &handlers_eks.ClusterMeta{
|
||||
ControlPlaneNodes: []handlers_eks.ControlPlaneNode{
|
||||
seedCluster(t, nc, tenantAccount, "alpha", &ekscluster.ClusterMeta{
|
||||
ControlPlaneNodes: []ekscluster.ControlPlaneNode{
|
||||
{InstanceID: otherInstance}, {InstanceID: cpInstanceID},
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, gateway_eks.AuthorizeInternal(t.Context(), nc, "ListInternalAddons",
|
||||
require.NoError(t, eksawsapi.AuthorizeInternal(t.Context(), nc, "ListInternalAddons",
|
||||
cpAgent(cpInstanceID), []string{"alpha", tenantAccount}, nil))
|
||||
require.NoError(t, gateway_eks.AuthorizeInternal(t.Context(), nc, "GetRecoveryDirective",
|
||||
require.NoError(t, eksawsapi.AuthorizeInternal(t.Context(), nc, "GetRecoveryDirective",
|
||||
cpAgent(cpInstanceID), []string{"alpha", tenantAccount, cpInstanceID}, nil))
|
||||
require.NoError(t, gateway_eks.AuthorizeInternal(t.Context(), nc, "PublishInternal",
|
||||
require.NoError(t, eksawsapi.AuthorizeInternal(t.Context(), nc, "PublishInternal",
|
||||
cpAgent(cpInstanceID), []string{"alpha"}, publishBody(tenantAccount)))
|
||||
require.NoError(t, gateway_eks.AuthorizeInternal(t.Context(), nc, "WebhookTokenReview",
|
||||
require.NoError(t, eksawsapi.AuthorizeInternal(t.Context(), nc, "WebhookTokenReview",
|
||||
cpAgent(cpInstanceID), []string{"alpha"}, tokenReviewBody(tenantAccount)))
|
||||
}
|
||||
|
||||
@@ -136,11 +136,11 @@ func TestAuthorizeInternal_AllowsClusterMember(t *testing.T) {
|
||||
// scalar field only, and their VMs must keep reaching the routes.
|
||||
func TestAuthorizeInternal_AllowsLegacyScalarMember(t *testing.T) {
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
seedCluster(t, nc, tenantAccount, "alpha", &handlers_eks.ClusterMeta{
|
||||
seedCluster(t, nc, tenantAccount, "alpha", &ekscluster.ClusterMeta{
|
||||
ControlPlaneInstanceID: cpInstanceID,
|
||||
})
|
||||
|
||||
require.NoError(t, gateway_eks.AuthorizeInternal(t.Context(), nc, "ListInternalAddons",
|
||||
require.NoError(t, eksawsapi.AuthorizeInternal(t.Context(), nc, "ListInternalAddons",
|
||||
cpAgent(cpInstanceID), []string{"alpha", tenantAccount}, nil))
|
||||
}
|
||||
|
||||
@@ -148,11 +148,11 @@ func TestAuthorizeInternal_AllowsLegacyScalarMember(t *testing.T) {
|
||||
// does not serve, which is what the path segment previously let it do.
|
||||
func TestAuthorizeInternal_RejectsClusterTheCallerDoesNotServe(t *testing.T) {
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
seedCluster(t, nc, tenantAccount, "alpha", &handlers_eks.ClusterMeta{
|
||||
ControlPlaneNodes: []handlers_eks.ControlPlaneNode{{InstanceID: cpInstanceID}},
|
||||
seedCluster(t, nc, tenantAccount, "alpha", &ekscluster.ClusterMeta{
|
||||
ControlPlaneNodes: []ekscluster.ControlPlaneNode{{InstanceID: cpInstanceID}},
|
||||
})
|
||||
seedCluster(t, nc, "444455556666", "beta", &handlers_eks.ClusterMeta{
|
||||
ControlPlaneNodes: []handlers_eks.ControlPlaneNode{{InstanceID: otherInstance}},
|
||||
seedCluster(t, nc, "444455556666", "beta", &ekscluster.ClusterMeta{
|
||||
ControlPlaneNodes: []ekscluster.ControlPlaneNode{{InstanceID: otherInstance}},
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
@@ -169,11 +169,11 @@ func TestAuthorizeInternal_RejectsClusterTheCallerDoesNotServe(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assertDenied(t, gateway_eks.AuthorizeInternal(t.Context(), nc, "ListInternalAddons",
|
||||
assertDenied(t, eksawsapi.AuthorizeInternal(t.Context(), nc, "ListInternalAddons",
|
||||
cpAgent(cpInstanceID), []string{tc.cluster, tc.account}, nil))
|
||||
assertDenied(t, gateway_eks.AuthorizeInternal(t.Context(), nc, "PublishInternal",
|
||||
assertDenied(t, eksawsapi.AuthorizeInternal(t.Context(), nc, "PublishInternal",
|
||||
cpAgent(cpInstanceID), []string{tc.cluster}, publishBody(tc.account)))
|
||||
assertDenied(t, gateway_eks.AuthorizeInternal(t.Context(), nc, "WebhookTokenReview",
|
||||
assertDenied(t, eksawsapi.AuthorizeInternal(t.Context(), nc, "WebhookTokenReview",
|
||||
cpAgent(cpInstanceID), []string{tc.cluster}, tokenReviewBody(tc.account)))
|
||||
})
|
||||
}
|
||||
@@ -182,13 +182,13 @@ func TestAuthorizeInternal_RejectsClusterTheCallerDoesNotServe(t *testing.T) {
|
||||
// A member reads its own directive and no other member's.
|
||||
func TestAuthorizeInternal_RecoveryIsSelfOnly(t *testing.T) {
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
seedCluster(t, nc, tenantAccount, "alpha", &handlers_eks.ClusterMeta{
|
||||
ControlPlaneNodes: []handlers_eks.ControlPlaneNode{
|
||||
seedCluster(t, nc, tenantAccount, "alpha", &ekscluster.ClusterMeta{
|
||||
ControlPlaneNodes: []ekscluster.ControlPlaneNode{
|
||||
{InstanceID: cpInstanceID}, {InstanceID: otherInstance},
|
||||
},
|
||||
})
|
||||
|
||||
assertDenied(t, gateway_eks.AuthorizeInternal(t.Context(), nc, "GetRecoveryDirective",
|
||||
assertDenied(t, eksawsapi.AuthorizeInternal(t.Context(), nc, "GetRecoveryDirective",
|
||||
cpAgent(cpInstanceID), []string{"alpha", tenantAccount, otherInstance}, nil))
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ func TestAuthorizeInternal_RecoveryIsSelfOnly(t *testing.T) {
|
||||
func TestAuthorizeInternal_RejectsUnreadableBody(t *testing.T) {
|
||||
for _, action := range []string{"PublishInternal", "WebhookTokenReview"} {
|
||||
for _, body := range []string{``, `{not-json`, `{"channel":"state","token":"t"}`, `{"accountId":7}`} {
|
||||
err := gateway_eks.AuthorizeInternal(t.Context(), nil, action, cpAgent(cpInstanceID),
|
||||
err := eksawsapi.AuthorizeInternal(t.Context(), nil, action, cpAgent(cpInstanceID),
|
||||
[]string{"alpha"}, []byte(body))
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidParameterValue, awserrors.ValidErrorCodeFromError(err),
|
||||
@@ -209,7 +209,7 @@ func TestAuthorizeInternal_RejectsUnreadableBody(t *testing.T) {
|
||||
|
||||
func TestAuthorizeInternal_RejectsEmptyPathSegments(t *testing.T) {
|
||||
for _, params := range [][]string{{"", tenantAccount}, {"alpha", ""}, {"alpha"}} {
|
||||
err := gateway_eks.AuthorizeInternal(t.Context(), nil, "ListInternalAddons", cpAgent(cpInstanceID), params, nil)
|
||||
err := eksawsapi.AuthorizeInternal(t.Context(), nil, "ListInternalAddons", cpAgent(cpInstanceID), params, nil)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidParameterValue, err.Error())
|
||||
}
|
||||
@@ -219,12 +219,12 @@ func TestAuthorizeInternal_RejectsEmptyPathSegments(t *testing.T) {
|
||||
// denial: reporting AccessDenied would send the on-VM agent's retry budget
|
||||
// chasing a permission it already has.
|
||||
func TestAuthorizeInternal_NoNATSIsServerInternal(t *testing.T) {
|
||||
err := gateway_eks.AuthorizeInternal(t.Context(), nil, "ListInternalAddons",
|
||||
err := eksawsapi.AuthorizeInternal(t.Context(), nil, "ListInternalAddons",
|
||||
cpAgent(cpInstanceID), []string{"alpha", tenantAccount}, nil)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
|
||||
err = gateway_eks.AuthorizeInternal(t.Context(), nil, "PublishInternal",
|
||||
err = eksawsapi.AuthorizeInternal(t.Context(), nil, "PublishInternal",
|
||||
cpAgent(cpInstanceID), []string{"alpha"}, publishBody(tenantAccount))
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
+8
-8
@@ -1,4 +1,4 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/messaging/nats"
|
||||
"log/slog"
|
||||
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -36,10 +36,10 @@ type publishInternalOutput struct{}
|
||||
// validBootstrapKinds is the closed set of bootstrap subject suffixes the
|
||||
// broker will relay, preventing arbitrary subject injection.
|
||||
var validBootstrapKinds = map[string]struct{}{
|
||||
handlers_eks.BootstrapSubjectToken: {},
|
||||
handlers_eks.BootstrapSubjectKubeconfig: {},
|
||||
handlers_eks.BootstrapSubjectJWKS: {},
|
||||
handlers_eks.BootstrapSubjectCA: {},
|
||||
ekscluster.BootstrapSubjectToken: {},
|
||||
ekscluster.BootstrapSubjectKubeconfig: {},
|
||||
ekscluster.BootstrapSubjectJWKS: {},
|
||||
ekscluster.BootstrapSubjectCA: {},
|
||||
}
|
||||
|
||||
// decodeInternalPublish is the one reading of the body that both AuthorizeInternal
|
||||
@@ -83,9 +83,9 @@ func PublishInternal(ctx context.Context, natsConn *nats.Conn, clusterName strin
|
||||
slog.DebugContext(ctx, "PublishInternal: unknown bootstrap kind", "cluster", clusterName, "kind", req.Kind)
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
subject = handlers_eks.BootstrapSubject(req.AccountID, clusterName, req.Kind)
|
||||
subject = ekscluster.BootstrapSubject(req.AccountID, clusterName, req.Kind)
|
||||
case internalChannelState:
|
||||
subject = handlers_eks.StateSubject(req.AccountID, clusterName)
|
||||
subject = ekscluster.StateSubject(req.AccountID, clusterName)
|
||||
case internalChannelAddon:
|
||||
subject = eksv1.AddonStatusSubject(req.AccountID, clusterName)
|
||||
default:
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/spinifex/internal/testkit"
|
||||
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"
|
||||
)
|
||||
@@ -24,13 +24,13 @@ func TestPublishInternal_RelaysToSubjects(t *testing.T) {
|
||||
{
|
||||
name: "bootstrap token",
|
||||
body: `{"accountId":"111122223333","channel":"bootstrap","kind":"k3s-bootstrap-token","payload":{"token":"k3s-secret"}}`,
|
||||
subject: handlers_eks.BootstrapSubject("111122223333", "alpha", handlers_eks.BootstrapSubjectToken),
|
||||
subject: ekscluster.BootstrapSubject("111122223333", "alpha", ekscluster.BootstrapSubjectToken),
|
||||
payload: `{"token":"k3s-secret"}`,
|
||||
},
|
||||
{
|
||||
name: "state report",
|
||||
body: `{"accountId":"111122223333","channel":"state","payload":{"healthz":"ok","node_count":1,"ts":42}}`,
|
||||
subject: handlers_eks.StateSubject("111122223333", "alpha"),
|
||||
subject: ekscluster.StateSubject("111122223333", "alpha"),
|
||||
payload: `{"healthz":"ok","node_count":1,"ts":42}`,
|
||||
},
|
||||
{
|
||||
+5
-5
@@ -1,18 +1,18 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// internalRecoveryOutput is the body returned to the on-VM k3s-recovery agent:
|
||||
// the member's current recovery directive.
|
||||
type internalRecoveryOutput struct {
|
||||
Directive handlers_eks.RecoveryDirective `json:"directive"`
|
||||
Directive ekscluster.RecoveryDirective `json:"directive"`
|
||||
}
|
||||
|
||||
// GetRecoveryDirective — GET /clusters/{name}/internal-recovery/{acct}/{instanceId}.
|
||||
@@ -28,8 +28,8 @@ func GetRecoveryDirective(ctx context.Context, natsConn *nats.Conn, clusterName,
|
||||
if clusterName == "" || accountID == "" || instanceID == "" {
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
out, err := handlers_eks.NewNATSEKSService(natsConn).GetRecoveryDirective(
|
||||
ctx, &handlers_eks.GetRecoveryDirectiveInput{ClusterName: clusterName, InstanceID: instanceID}, accountID)
|
||||
out, err := ekscluster.NewNATSEKSService(natsConn).GetRecoveryDirective(
|
||||
ctx, &ekscluster.GetRecoveryDirectiveInput{ClusterName: clusterName, InstanceID: instanceID}, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ func CreateNodegroup(ctx context.Context, natsConn *nats.Conn, accountID, cluste
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).CreateNodegroup(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).CreateNodegroup(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DescribeNodegroup — GET /clusters/{name}/node-groups/{ng}.
|
||||
@@ -24,13 +24,13 @@ func DescribeNodegroup(ctx context.Context, natsConn *nats.Conn, accountID, clus
|
||||
ClusterName: aws.String(cluster),
|
||||
NodegroupName: aws.String(ng),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DescribeNodegroup(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).DescribeNodegroup(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListNodegroups — GET /clusters/{name}/node-groups.
|
||||
func ListNodegroups(ctx context.Context, natsConn *nats.Conn, accountID, cluster string) (*eks.ListNodegroupsOutput, error) {
|
||||
input := &eks.ListNodegroupsInput{ClusterName: aws.String(cluster)}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListNodegroups(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).ListNodegroups(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// UpdateNodegroupConfig — POST /clusters/{name}/node-groups/{ng}/update-config.
|
||||
@@ -41,7 +41,7 @@ func UpdateNodegroupConfig(ctx context.Context, natsConn *nats.Conn, accountID,
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
input.NodegroupName = aws.String(ng)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).UpdateNodegroupConfig(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).UpdateNodegroupConfig(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// DeleteNodegroup — DELETE /clusters/{name}/node-groups/{ng}.
|
||||
@@ -50,5 +50,5 @@ func DeleteNodegroup(ctx context.Context, natsConn *nats.Conn, accountID, cluste
|
||||
ClusterName: aws.String(cluster),
|
||||
NodegroupName: aws.String(ng),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DeleteNodegroup(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).DeleteNodegroup(ctx, input, accountID)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
@@ -1,10 +1,10 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ func TagResource(ctx context.Context, natsConn *nats.Conn, accountID, resourceAR
|
||||
return nil, err
|
||||
}
|
||||
input.ResourceArn = aws.String(resourceARN)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).TagResource(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).TagResource(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// UntagResource — DELETE /tags/{arn}.
|
||||
@@ -25,11 +25,11 @@ func UntagResource(ctx context.Context, natsConn *nats.Conn, accountID, resource
|
||||
return nil, err
|
||||
}
|
||||
input.ResourceArn = aws.String(resourceARN)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).UntagResource(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).UntagResource(ctx, input, accountID)
|
||||
}
|
||||
|
||||
// ListTagsForResource — GET /tags/{arn}.
|
||||
func ListTagsForResource(ctx context.Context, natsConn *nats.Conn, accountID, resourceARN string) (*eks.ListTagsForResourceOutput, error) {
|
||||
input := &eks.ListTagsForResourceInput{ResourceArn: aws.String(resourceARN)}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListTagsForResource(ctx, input, accountID)
|
||||
return ekscluster.NewNATSEKSService(natsConn).ListTagsForResource(ctx, input, accountID)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ func decodeWebhookTokenReview(body []byte) (webhookTokenReviewRequest, error) {
|
||||
// WebhookTokenReview — POST /clusters/{name}/token-review. Resolves an
|
||||
// `aws eks get-token` bearer token to a K8s identity via the AWSGW, once
|
||||
// AuthorizeInternal has bound the caller to this cluster and account.
|
||||
func WebhookTokenReview(ctx context.Context, natsConn *nats.Conn, clusterName string, body []byte) (*handlers_eks.WebhookTokenReviewResult, error) {
|
||||
func WebhookTokenReview(ctx context.Context, natsConn *nats.Conn, clusterName string, body []byte) (*ekscluster.WebhookTokenReviewResult, error) {
|
||||
if natsConn == nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func WebhookTokenReview(ctx context.Context, natsConn *nats.Conn, clusterName st
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
res, err := handlers_eks.ResolveTokenReview(ctx, natsConn, req.AccountID, clusterName, req.Token, tokenReviewVerifyTimeout)
|
||||
res, err := ekscluster.ResolveTokenReview(ctx, natsConn, req.AccountID, clusterName, req.Token, tokenReviewVerifyTimeout)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "WebhookTokenReview: resolve failed", "cluster", clusterName, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
"github.com/mulgadc/spinifex/internal/testkit"
|
||||
"github.com/mulgadc/spinifex/spinifex/domains/eks/access"
|
||||
ekscluster "github.com/mulgadc/spinifex/spinifex/domains/eks/cluster"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -61,15 +61,15 @@ func TestWebhookTokenReview_InfraFaultIsServerInternal(t *testing.T) {
|
||||
func TestWebhookTokenReview_ResolvesIdentity(t *testing.T) {
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
js := testutil.NewJetStream(t, nc)
|
||||
kv, err := js.CreateKeyValue(t.Context(), jetstream.KeyValueConfig{Bucket: handlers_eks.AccountBucketName("111122223333")})
|
||||
kv, err := js.CreateKeyValue(t.Context(), jetstream.KeyValueConfig{Bucket: ekscluster.AccountBucketName("111122223333")})
|
||||
require.NoError(t, err)
|
||||
_, err = access.New("us-east-1").Create(t.Context(), kv, "111122223333", access.Spec{
|
||||
Cluster: "alpha", PrincipalARN: testARN, Groups: []string{"system:masters"}, Type: access.EntryTypeStandard,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
sub, err := nc.Subscribe(handlers_eks.TokenVerifySubject, func(m *nats.Msg) {
|
||||
resp, _ := json.Marshal(handlers_eks.TokenVerifyResponse{
|
||||
sub, err := nc.Subscribe(ekscluster.TokenVerifySubject, func(m *nats.Msg) {
|
||||
resp, _ := json.Marshal(ekscluster.TokenVerifyResponse{
|
||||
AccountID: "111122223333",
|
||||
ARN: testARN,
|
||||
UserID: "AROAEXAMPLE:session",
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package gateway_eks
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"testing"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
package handlers_eks_test
|
||||
package cluster_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
"github.com/mulgadc/spinifex/internal/testkit"
|
||||
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"
|
||||
)
|
||||
@@ -29,9 +29,9 @@ func TestClusterTokenParamHash_IgnoresTokenReflectsParams(t *testing.T) {
|
||||
diffParams := *base
|
||||
diffParams.Version = aws.String("1.31")
|
||||
|
||||
assert.Equal(t, handlers_eks.ClusterTokenParamHash(base), handlers_eks.ClusterTokenParamHash(&sameParamsDiffToken),
|
||||
assert.Equal(t, ekscluster.ClusterTokenParamHash(base), ekscluster.ClusterTokenParamHash(&sameParamsDiffToken),
|
||||
"token must not affect the param hash")
|
||||
assert.NotEqual(t, handlers_eks.ClusterTokenParamHash(base), handlers_eks.ClusterTokenParamHash(&diffParams),
|
||||
assert.NotEqual(t, ekscluster.ClusterTokenParamHash(base), ekscluster.ClusterTokenParamHash(&diffParams),
|
||||
"a real param change must change the hash")
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestClusterTokenParamHash_LeavesTheInputAlone(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := &eks.CreateClusterInput{Name: aws.String("c1"), ClientRequestToken: aws.String("tok-A")}
|
||||
|
||||
handlers_eks.ClusterTokenParamHash(input)
|
||||
ekscluster.ClusterTokenParamHash(input)
|
||||
|
||||
assert.Equal(t, "tok-A", aws.StringValue(input.ClientRequestToken),
|
||||
"hashing must not strip the token the create still needs")
|
||||
@@ -51,7 +51,7 @@ func TestClusterTokenParamHash_LeavesTheInputAlone(t *testing.T) {
|
||||
func TestClusterTokenStore_ReplaysTheClusterName(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
store, err := handlers_eks.NewClusterTokenStore(t.Context(), testutil.NewJetStream(t, nc))
|
||||
store, err := ekscluster.NewClusterTokenStore(t.Context(), testutil.NewJetStream(t, nc))
|
||||
require.NoError(t, err)
|
||||
const account, tok, hash = "111122223333", "tok-1", "h"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_eks
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"testing"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user