Until versions can be published, the only version is $LATEST: the list holds
its configuration with the FunctionArn qualified as :$LATEST and no
NextMarker. MaxItems outside 1 to 10000 is refused, and Marker is refused
outright rather than guessing AWS's paging behaviour. It is authorized as
lambda:ListVersionsByFunction on the function ARN and does not need the code
store.
* feat(admin): non-finalizing DeleteAccount exercises the quiesced EC2 platform kinds
A non-finalizing DeleteAccount quiesces the account, records the S9 evaluation, cleans only the key-pair, placement-group and launch-template kinds it recorded as quiesced, then re-reads the owner inventory and accounts for what remains. The job ends EXERCISED with the account still TERMINATING; a later ordinary delete takes the same operation over.
Only an exercise records a quiesced scope, so ordinary deletes still record none. The cleanup capability gains a kinds selector, awsgw binds the in-process owners, and the instance reference reader fails closed.
* refactor(admin): name the takeover of a finished exercise exercise_finished
Code signing is unsupported, so the read only proves the function exists and
returns AWS's body for a function with no code signing config: both members
present and null. A missing function is ResourceNotFoundException. It is
authorized as lambda:GetFunctionCodeSigningConfig on the function ARN and
needs neither the code store nor environment decryption.
Serves GET /2015-03-31/functions/{FunctionName}/configuration with the
same name, qualifier, ownership and environment handling as GetFunction,
authorized as lambda:GetFunctionConfiguration on the function ARN. It is
a metadata read, so it neither opens nor presigns the code store.
Dev-only code delivery behind the lambdatestlink tag: the host attaches the
ZIP as a read-only disk and passes its exact size and SHA-256 through
fw_cfg. The agent reads those values before sealing fw_cfg, and installs
the package only after authenticating and receiving the host's first
frame: it checks size and hash, validates every entry before writing any,
extracts through os.Root into a nosuid,nodev tmpfs at /var/task, and makes
it read-only. Any failure exits without starting the bootstrap.
Entry names that could escape the task root, entries other than regular
files and directories, and sizes over the dev bounds are refused; declared
sizes cannot wrap the running total, and actual bytes are counted whatever
the header claims. The bounds are dev limits, not AWS limits.
StartBootstrap now starts the bootstrap in its own directory, so packaged
functions can open their files by relative path.
* feat(config): refuse a spinifex.toml whose nodes resolve to different EBS providers
* feat(daemon): lock a cluster to one EBS provider in spinifex-cluster-config
The first daemon records its provider at ebs-provider with Create, which
also backfills an existing cluster. A daemon whose provider differs from
the recorded one refuses to start, naming both providers and the recovery.
* fix(config): compare only explicit EBS providers across nodes
A generated spinifex.toml writes no ebs stanza for peers, so treating an
absent peer provider as viperblockd refused every non-default cluster.
* fix(daemon): exit on an EBS provider lock refusal and refuse a malformed lock
A mismatch now exits 1 so the unit shows failed. A recorded value that
does not decode or names no supported provider is a distinct permanent
refusal quoting the raw value. Permanence is a sentinel matched with
errors.Is, and the cluster-config bucket is declared beside the others.
* style(daemon): align JetStreamManager fields
* feat(ebs): contract schema version 3 for the provider data path
Servers accept schema versions 1 to 3 and answer every request in the
version it was sent at, while every client still sends version 1.
Version 3 adds no_seal to UnpublishVolumeRequest, refused below 3, and
torn_down to UnpublishVolumeResponse. UnpublishVolume now returns an
UnpublishedVolume so the torn-down result reaches callers.
A publish refused because another node holds the volume returns
volume_held_elsewhere at version 3 and volume_in_use below it, which is
what v1.23.0 returns. A read-only mismatch on republish stays
volume_in_use at every version.
GetCapabilities lists the contract versions served and is also answered
on a per-node subject. The unsealed-writes subject is part of
EBSProvider, and write-through providers answer it with nothing held. A
fence-notice event subject and message are defined. Unpublish gets the
data-movement timeout.
The memory provider, natsserve and qemunbdd implement all of this. A
qemunbdd republish whose qemu-nbd has died re-exports the volume instead
of returning the dead socket.
* feat(viperblock): serve contract schema version 3 from viperblockd
Every provider subject replies in its request's version, and
GetCapabilities lists the contract versions and is also served on the
node's own capabilities subject.
PublishVolume no longer returns an existing entry early. It goes through
mountVolume, which hands back a live export and drops and remounts one
whose nbdkit has died. Mount failures map to not_found, held_elsewhere
(version 3 only, volume_in_use below it), volume_in_use for a mode
mismatch, unavailable for a backing or lease store that is not ready,
and internal otherwise.
UnpublishVolume with no_seal abandons the export without sealing it,
and reports whether an export was torn down.
* test(ebs): conformance cases for contract schema version 3
Every provider run through the suite now has to:
- list ascending contract versions including the legacy one, answer each
in that version, and refuse an unlisted one without changing state;
- refuse a publish elsewhere with volume_held_elsewhere only at version 3
and volume_in_use at versions 1 and 2;
- refuse a republish in the other access mode as volume_in_use, never
held elsewhere;
- report torn_down truthfully, refuse no_seal below version 3, and either
honour no_seal at version 3 or refuse it as unsupported;
- answer the unsealed-writes query with a subset of what was asked.
SuiteConfig.KillExport lets a provider whose export is a process prove
that a republish after the export died returns a serving export.
qemunbdd and viperblockd supply it.
* fix(ebs): stop a running export before remounting over its lost socket
An export whose socket is gone was treated as dead even while its
process ran, and a republish started a second export beside it. A
client connected before the socket went keeps writing through the old
process, so that made two writers on one volume.
qemunbdd now SIGTERMs that qemu-nbd and waits for it to exit before
exporting again. viperblockd stops the nbdkit through unmountVolume, so
the old copy is sealed as a normal unmount would seal it. If the process
will not stop, the publish is refused as unavailable and the entry is
kept for the next attempt.
* test(viperblock): give the no-seal tests a seal they could reach
The no-seal unpublish and abandon tests left BaseDir empty, so
volumeNeedsSeal was false and no path could seal whatever the code did.
Both now create local state under BaseDir first, and check that a seal
is reachable before asserting none ran.
* refactor(ebs): leave version rules to the provider and trim the v3 surface
The provider decides what a request's version allows, so natsserve no
longer gates TornDown or held-elsewhere on it: classifyError takes no
version, and the reply carries what the provider returned. viperblockd
applies the TornDown rule in unpublishVolume, its provider half.
The unpublish handlers check the version once, through
CheckUnpublishVersion, and classify its refusal. Each handler binds its
reply version once.
UnpublishedVolume is reduced to TornDown. movesData classifies a subject
by its parsed verb, which replaces isNodeSubject and the owner-verb
list. nodeSubjectWildcard is unexported. The conformance suite reads
capabilities once.
* test(ebs): check v3 success replies, timeouts and refusals end to end
- Unpublish is sized as data movement, and publish, unsealed writes and
per-node capabilities keep the caller's timeout.
- A mount whose lease store or backing store is not ready reaches the
wire as unavailable.
- A successful publish replies in the request's version at every served
version, in natsserve and in viperblockd. Every natsserve case now gets
its own seeded provider, so none relies on an earlier case's state.
The conformance republish runs at every listed version.
- viperblockd's per-node capabilities list the versions it serves.
- A no_seal unpublish above the served version is refused, and the
export is neither torn down nor sealed.
- The dead-entry republish test asserts unconditionally. The snapshot
lease test runs against real volume state and asserts nothing was
written. The conformance torn-down case skips a provider that does
not list version 3.
* fix(ebs): wrap the stop failure behind an unavailable publish
The refusal now wraps the stop error as well as ErrUnavailable, so a
caller can still inspect why the old export did not stop. Also fixes
import order and spacing that the linter flagged in the new tests.
* fix(admin): read the newest network test by its claim key, so a listing never leaves it out
* fix(admin): keep the newest-test lookup additive, so it never fails a listing
The lambdatestlink agent now loads its identity, seals fw_cfg and dials the
host exactly as the normal agent does, then carries the test stream over
that mutual-TLS connection instead of a device path. Nothing tenant-facing
starts until the host's first valid frame: a refusal after the handshake,
an early close or a shutdown never starts the bootstrap. Each VM serves one
invocation and then stops.
The normal agent is unchanged and still accepts no invocations. The tagged
tests join the race tier for the two packages that carry the build tag.
* fix: find node planes from the node's configuration, and admit only a node's own address as a network test target
Node facts looked for planes only on br-wan, br-lan and br-vpc, so a host
whose addresses sit on plain NICs (a Vultr or OCI VM) reported no planes:
the topology had no links, every port read unused, and Run test stayed
disabled. Each plane now resolves to the device holding the node's
configured address: ovn-encap-ip for vpc, the node's own host for lan,
and vpcd.external_interface (or the bridge it is a port of) for wan, with
the br-* names as the fallback.
A run request now names its target node, and the client admits only the
address that node reports for the plane. The old subnet rule refused
legitimate WAN tests on Vultr, where each VM has its own /23, and admitted
strangers sharing a cloud subnet.
The network test's selects showed their raw values (__all__) because Base
UI's Select needs its options as items to render a label.
* fix(ui): list a plane on a plain NIC without an empty bridge, and note a shared LAN port as well as a shared bridge
* fix(vultr): turn on public addresses in the default subnets once the Vultr pool is live
* fix(ui): say where an RDS database goes, and how to use another VPC, when no DB subnet group exists
A stop/start that leaves the guest stuck in cloud-init-local waiting on
IMDS fails the restart SSH probe, but the diagnostics only captured the
EIP/OVN side, which was healthy. Capture br-imds flows, the endpoint's
link, addresses, sysctls, reply rule and route, the responder listener
and conntrack too, so the next occurrence names the broken hop.
* fix(rds-agent): stop a command blocked on a backup hold from keeping out its release
The agent ran commands one at a time inside its poll loop. A MariaDB
ALTER USER issued while an automated backup held BACKUP STAGE blocked at
its commit, so the unquiesce that would release the hold could never be
delivered: the rotation timed out at the API while the new password was
already live in memory, and the instance stayed backing-up.
Commands now run on their own goroutines while the agent keeps polling,
re-published copies of a running or answered command are ignored, and a
finished reply cuts the current poll short so it is carried at once.
MariaDB SetPassword also waits for any hold to end before it runs, for
less than the control plane's budget, so a rotation that cannot finish
fails before touching the engine.
* fix(rds-agent): keep engine commands serial and bound every command by its budget
Engine-mutating commands go back to running one at a time on a single
lane, as before; only unquiesce runs beside it, so a command blocked on a
backup hold still cannot keep out the release.
Each command is bounded by its type's control-plane budget, now exported
as rdsinstance.CommandBudget and measured from receipt. A command whose
budget ran out while it queued is answered as expired and never run, so
a stale quiesce cannot put the engine in backup mode. Quiesce takes its
bookkeeping lock within its context, and the MariaDB ALTER USER carries a
lock_wait_timeout from what is left of the budget.
* test(rds): rotate the MariaDB master password through an automated backup
Opens the backup window on purpose and rotates the password back and
forth until one rotation is in flight while the instance reports
backing-up, asserting each outcome is whole and that the backup releases
the engine. Every suite create now pins its backup and maintenance
windows clear of the run, so no other subtest's outcome depends on the
hour the cell runs.
* fix(rds): cap an agent command by the budget its issuer has left
The agent measured each command's deadline from receipt, so a first copy
delayed by a gateway or bus outage was granted a whole fresh budget when
its issuer had nearly given up; a late quiesce could then take a hold
nobody would release. The issuer now stamps every republished copy with
the budget it has left, a duration that needs no clock agreement, and the
agent uses the smaller of that and its own type budget.
* feat(ui): console feedback round: state tones, inline tags, filter bar and form fixes
Settled states go green and transitions yellow with a spinner, status text is
lowercase, tags are edited inline row by row, every list filters through one
filter bar, long cells truncate with a tooltip and times are relative.
EC2 launch takes a Name and offers ICMP echo, and leaves Placement Group out
when no group is available. RDS create is reordered with engine tiles and
window pickers. Route 53 records get bulk delete, a TTL select, value labels
that follow the type, and NS. Tooltips use the popover tokens, and the console
is sized over a 110% root.
* feat(ui): size RDS instance classes and give each record value its own input
An RDS class is a db.-prefixed EC2 type, so the class picker shows its vCPUs
and memory in every option and under the field, as the EC2 launch form does,
on create, modify and restore.
A Route 53 record's values were one textarea, which let a line break into a
value. Each value is now a single-line input with Add value; Enter starts the
next one, a pasted list splits into rows, and CNAME takes exactly one.
* fix(ui): open a DNS record for editing from the keyboard
* feat: admin node inventory, topology, live host metrics and network test
Each node collects its hardware facts as root at daemon start (dmidecode,
ethtool, networkctl LLDP where available) into /etc/spinifex/node-facts, and
the daemon serves them with its plane wiring over NATS.
New admin-only actions, refused for every account but the system admin
account regardless of policy:
- GetNodeFacts: hardware, NICs, LLDP neighbours and planes per node
- GetNodeMetrics: live CPU, memory and per-plane traffic for one node
- StartNetworkTest / DescribeNetworkTests: iperf2 between nodes over a
chosen plane, both directions, one test cluster-wide at a time, recorded
in KV and converged asynchronously
The iperf listener runs only for the duration of a run, without
capabilities, on TCP 5001, which the peers firewall rule now allows. A run
refuses any target outside the node's own plane subnet.
The console gains a node panel (hardware, network, metrics, network test
with a dial scaled to the port's line rate) and a topology view built with
@xyflow/react. setup.sh installs dmidecode and iperf.
* test: cover the node insight handlers and the facts file mode
* feat: report every CPU flag in node facts
* feat(ui): node table on the dashboard, every CPU flag and a readable topology
The dashboard's nodes section is the Nodes page table, open by default, and a
row opens that node. Ready reads green, capacity sorts by most available, and
the processor's features tip lists every flag. Topology draws one row per
port with switches in a muted card, highlights a node's links on hover, and
offers a left-to-right layout beside top-down.
* chore(ui): rebuild dist
* fix: align the CPU facts fields
* fix: bound the network test server start and read ARM CPU features and cores
* chore(ui): rebuild dist
* test(ui): pin the instances baseline clock and take the screenshots from CI's Playwright image
* fix: retry a network test claim whose holder released it mid-check
StartBootstrap sets no_new_privs on a dedicated locked OS thread and
starts the bootstrap from it, as uid/gid 993 with no supplementary groups
and only the environment it is given. The thread is never unlocked, so
the flag never reaches the agent's other threads, and no dynamic loader
runs before no_new_privs is set. The test-only agent start uses it
instead of running the bootstrap as root. The normal agent still accepts
no invocations.
* feat(lambda): open the authenticated control connection at guest startup
A normal lambda-agent build now reads its per-generation identity from
fw_cfg, unloads qemu_fw_cfg and refuses to start unless the fw_cfg tree is
gone, then dials the host under the pinned mutual TLS and holds the
connection. It accepts no invocations and never starts the bootstrap: any
data from the host fails closed until the message protocol exists.
The image loads qemu_fw_cfg and the virtio_net chain and configures the
guest interface from opt/spinifex/netcfg, as other system VMs do. The
lambdatestlink build also seals fw_cfg before starting the bootstrap.
* test(lambda): bound startup test waits and report host accept errors
* feat(iam): record quiescence evidence on the deletion job, with the L09 scope unresolved until the final inventory re-read
* fix(iam): cite ADR-0008:S9 as the basis for the STS consumer
* feat(lambda): add the Lambda runtime image and guest agent
* fix(lambda): stop the guest agent after a timed-out invocation
* docs(lambda): declare the L-GUEST executor-to-agent contract
* refactor(vm): remove the unused virtio-serial port builder
Lambda guest control moves to TCP over the microVM's network, so no
caller needs SerialPorts. This reverts d7905107b.
* fix(lambda): drop the serial carrier from guest startup and keep virtio-net
run-agent no longer waits for a carrier device named on the kernel
command line, init loads virtio_net instead of virtio_console, and the
image keeps virtio_net's failover and net_failover dependencies. The
normal agent still refuses to start without an authenticated transport.
* feat(lambda): authenticate the guest's TCP control connection with pinned mutual TLS
hostlink loads the per-generation identity from fw_cfg and dials the host
over TLS 1.3 with ALPN spinifex-lambda-guest/1. VerifyPeer requires exactly
the pinned certificate, validates it, and checks its single URI SAN names
the expected scope and role. The L-GUEST contract now describes this
transport; message framing and fencing stay open, and the normal agent
still refuses to start.
* feat(lambda): add the host end of the guest's TCP mutual TLS connection
guestconn creates fresh host and guest identities for each environment
generation, produces the fw_cfg entries the guest reads, and accepts only
that generation's exact guest certificate. Peers on the untrusted bridge
that fail the handshake are closed and the listener keeps waiting until
its context ends; the cancel callback finishes before the deadline is
cleared so the listener can be reused.
The pinned-certificate check and the L-GUEST vocabulary move to
foundation/pki and contracts/lambda/v1 so both ends share them.
* docs(lambda): describe the shared connection vocabulary in the contract README
* test(eks): specify the AWS-style cluster security group layout
* feat(eks): give clusters one AWS-style cluster security group
Create the eks-cluster-sg-<name>-<id> SG in the customer VPC with the EKS tags and a self rule, report it as clusterSecurityGroupId, launch workers with it (or the launch template's SGs), and attach it to the private-endpoint ENI. New clusters no longer get customer-VPC control-plane and nodegroup SGs; clusters created before keep theirs, and delete finds the new SG by tag.
* fix(eks): own the cluster SG by its recorded id and scope the delete sweep
Workers and teardown use the SG id recorded on the cluster; a create retry only adopts an SG carrying tags the service alone can set. DeleteClusterSGs now revokes every rule only on cluster-owned SGs and, on any other SG in the VPC, only the rules that reference one.
* docs(eks): use the cluster security group in workbooks and e2e
Workbooks read vpc_config[0].cluster_security_group_id and drop the hand
NodePort rules. E2E covers an internet-facing instance-target ALB with no
hand rule, exec and logs, and a bystander security group surviving
DeleteCluster.
* docs(eks): drop stale node-group SG sentence
* feat(operator): resume account deletions whose holder died
Each awsgw scans the deletion job bucket and takes over RUNNING jobs that
carry an accepted operation, a fence of at least 1 and an expired lease,
through the same fenced takeover a retried request uses. A resumed job runs
on a fresh timeout budget, so recovery no longer depends on the requester.
* chore: trim resumer comments to three lines
* refactor(cli): delete accounts only through the admin API
* docs: describe account delete as admin API only
* feat(iam): quiesce an account under a fenced deletion operation
* fix(iam): keep the termination capabilities off the shared interface and fence the account removal
* test(awsgw): guard the IAM termination binding on the operator deps
* docs: say which credentials the account delete signs with
* feat(lambda): add unregistered CreateFunction and GetFunction
Function records live in a daemon-owned per-account KV bucket, keyed by
Region and name, claimed with one conditional create that carries the
function ID and a Pending state. A claim whose code copy never completes
expires, so a later create can take the name over; the original gateway
then fails to mark its code stored. Code copies are never deleted here.
CreateFunction checks the ZIP is readable, refuses an execution role that
does not trust lambda.amazonaws.com (using the STS trust evaluation), and
encrypts environment values in the gateway with the master key, bound to
the account and function ID. GetFunction returns Code.Location as a
10-minute presigned URL for the off-host Predastore address, signed by a
session that outlasts it; with no off-host address it fails instead of
naming an internal one. Lambda uses the REST-JSON error envelope.
The handlers are built in the gateway but not routed: no public Lambda API
is registered until a runtime image exists.
* fix(lambda): return and accept only the default LoggingConfig
Every function configuration now carries LoggingConfig with LogFormat Text
and LogGroup /aws/lambda/<name>. CreateFunction accepts the default when
supplied and refuses a JSON format, another log group or a log level.
Adds an integration test that drives a signed CreateFunction and
GetFunction through the gateway, the NATS record owner and the code store,
then downloads Code.Location and checks the bytes. The STS trust check
moves to stsdomain.RoleTrusts so the test uses the production evaluation.
* fix(lambda): render the stored record in the CreateFunction response
CodeStored now returns the record the owner wrote, and CreateFunction
renders it instead of stamping its own time, so CreatedAt, LastModified
and RevisionId match an immediate GetFunction.
* fix(lambda): match IAM error codes, not messages, in code-store provisioning
IAM errors now carry a message, so comparing err.Error() with the bare
code no longer matched EntityAlreadyExists or NoSuchEntity. Provisioning
then failed on every start after the first and could not adopt its own
role. Resolve the AWS error code instead.
* test(iam): check the resolved NoSuchEntity code in UpdateUser tests
IAM errors now carry a message, so err.Error() no longer equals the bare
code. Assert the resolved AWS error code instead.
* test(iam): account for UpdateUser as named by bare name
UpdateUser addresses its user by bare name, so the cross-tenant ARN table
cannot target another account; list it with the other byName IAM actions.
Add a focused test that a rename stays within the caller's account.
Lambda code copies live in a system-account Predastore bucket reached
only through a short-lived session on a Regional role scoped to that
bucket. STS gains an in-process AssumeCodeStoreRole that derives the
Region's code-store role itself and assumes it as lambda.amazonaws.com;
it is not on the STS interface, NATS or HTTPS.
Provisioning creates or verifies the owner-tagged role, inline policy
and bucket on first use only, retries Lambda's own partial create,
and refuses an untagged or foreign resource with an operator-visible
reason. Copies are keyed by their SHA-256, so different bytes can never
replace a recorded revision. The gateway holds the root client and IAM;
the Lambda package receives only the bounded provisioning operations
and the scoped store.
* test: pin AWS-format EKS add-on versions
* feat: return EKS add-on versions in AWS format and accept them as input
* test: DescribeAddonVersions query parameters, compatibilities and paging
* feat: honour DescribeAddonVersions filters and report compatibilities
* test: EKS parameter refusals must carry a code the operation declares
* fix: EKS parameter refusals use a code the operation declares
* docs: describe AWS-format EKS add-on versions
Document the v<x.y.z>-eksbuild.<n> form, the bare-form leniency, the
DescribeAddonVersions filters and compatibilities, and how to pin a
version in Terraform. Replace the leave-addon_version-unset guidance.
The four crash cases each restarted the store, which on the three-node
cluster means a full Raft re-election every time. They now set up in
separate namespaces, crash once, and run the same post-crash checks as
named subtests, cutting TestR3/Conformance from ~39s to ~11s.
* refactor(compute): extract a shared direct-boot microVM builder
runtime/compute/vm gains MicroVMSpec, NewMicroVMConfig and
MicroVMMachineType, with CPU mitigations on unless a caller sets
MitigationsOff. The ELB and SQS system-instance launches pass their own
settings to it; only the load balancer opts out of mitigations. A golden
test pins the full QEMU argv for both launches, unchanged by the move.
* test(daemon): stub QEMU on PATH in the microVM argv golden test
Execute resolves a QEMU binary before returning the command, and CI
runners have none installed. The test now puts an empty
qemu-system-x86_64 on PATH, so it checks the real argument construction
without QEMU.
TestR3 restarts real three-node JetStream clusters and waits out Raft
elections, adding ~40s to the slowest preflight package while covering
~0.1% of its statements. It now skips unless SPINIFEX_SQS_R3 is set, and
make test-race sets it, so CI's Race Detection job runs it on every PR.
* fix(awsgw): admit accounts with a synchronous in-process status read
* fix(iam): refuse to move the system and admin accounts off ACTIVE
* fix(awsgw): deny a missing account record instead of failing the request
install.mulgadc.com serves the latest release's setup.sh, so a --channel dev
or --version install ran an installer older than the tarball it unpacked and
missed files it did not know about, such as lib/distro.sh. setup.sh now hands
off to the tarball's own setup.sh, with the same arguments, before it changes
anything on the host.
The Vultr guide now installs from the latest release instead of dev.
* test: pod identity webhook mutation, publisher and webhook config contracts
* feat: pod identity webhook mutator, CA bundle publisher and webhook configuration
* feat(eks): run the pod identity webhook on every control-plane server
Add the eks-pod-identity-webhook binary and its OpenRC service. It listens on
127.0.0.1:8444 with a serving certificate signed by the k3s server CA, authenticates
to the API server with a client certificate signed by the k3s client CA, and runs
under its own least-privilege ClusterRole. Each server stages the RBAC and the
MutatingWebhookConfiguration (caBundle is the server CA) into the k3s manifests
directory, so server-join nodes get them too.
Match upstream's container skip rule: skip only when the regional STS key is
present, and add the token mount otherwise.
* fix(eks): issue pod identity webhook certs for one year and renew in the running process
Both the serving and client certificates were valid for ten years. Kubernetes
cannot revoke a client certificate, so a copy taken off a control-plane disk
would stay a valid identity for a decade. Issue both for one year with the
existing 30-day renew window.
The process loaded the serving certificate once. A daily check now exits
with ErrCertRenewal when either certificate enters the renew window, so the
supervisor restarts it and EnsureCerts reissues.
Add tests for KubeClient against a TLS fake API server and for Run, which the
package had none of.
* test: e2e covers pod-identity webhook and unmodified LBC chart
IRSAPod now uses a ServiceAccount annotation only and asserts the injected
env and volumes plus an untouched unannotated pod. LBCChart installs the
upstream chart 1.11.0 and reconciles an Ingress into an active ALB.
* test: pass the tagging endpoint to the LBC chart and mark Shield/WAF flags Spinifex-only
* fix: make the pod identity webhook init script executable in the eks-node image
setup.sh chmods each shipped init script to 0755 explicitly, and the new
service was missing from the list. rc-update then refused it, and
eks-node-role, running under set -eu, aborted before starting k3s.
Add a guard test that every init script in INSTALL_FILES is chmod'd.
* fix: start the pod identity webhook last and keep its failures non-fatal
eks-node-role runs under set -eu, so a fault enabling or starting the
optional webhook aborted the control plane boot before k3s-first-boot,
state-report and addon-sync. Enable and start it at the end of the server
and server-join roles and log a warning on failure instead.
The role test now asserts the webhook is the last call and that an enable
or start failure still exits 0 and finishes the role.
* fix: move the pod identity webhook off the token webhook readiness port
eks-token-webhook already serves its plain HTTP readiness endpoint on 8444,
which the API server target group health-checks. The pod identity webhook
bound the same port, failed with address already in use and respawned
forever, so no pod was ever injected.
Listen on 127.0.0.1:8445 and add a test that the two ports differ.
* docs: describe pod identity webhook for user workloads in the EKS guide
* fix: split STUB_FAIL assignment from export in the role test
* feat(eks): converge DeleteCluster's teardown behind the recorded DELETING
DeleteCluster ran the whole cluster teardown inside the NATS request. Each
control-plane VM that ignores ACPI costs 40 s (20 s graceful powerdown, then
20 s waiting for the PID file), so a two-member cluster overran the 30 s
budget: the caller got a 500, and its retry queued behind the first handler
on the serial subscription and got a 404.
The request now validates, answers a DELETING cluster from its record,
records DELETING and returns. The teardown runs in a background converger
started by the request and backed by the deleting reaper:
- An in-process per-cluster gate sits in front of the teardown lease, which
re-adopts a key its own node holds; one per-node bound covers kicks and
reaper hand-offs.
- The lease renews on the service context, not the request's, and the
converger stops at a step boundary if it is lost.
- The converger acts only on a record that is DELETING when re-read, refuses
to remove a record that still has a node group, deletes the cluster's
child records before the meta, and deletes the meta only if no ownership
ref appeared that its pass did not tear down.
- DELETING is sticky for every status writer, and the create launch's meta
writes add their refs to a DELETING record instead of overwriting it, and
never recreate a swept one.
- The reaper hands work to the converger instead of running it inline,
retries at a backoff capped at 30 minutes without ever abandoning a
record, and resumes records an older build flagged exhausted.
- A stuck delete is surfaced in DescribeCluster as one INTERNAL_FAILURE
issue naming the step, never the internal error.
* fix(eks): refuse DeleteCluster while node groups are attached
AWS answers ResourceInUseException ("Cluster has nodegroups attached") when
managed node groups remain. DeleteCluster went ahead instead, and its sweep
erased the node group records, leaving their workers running in the
customer account with no record to own them and no control plane.
The request now decides that error before recording anything. The e2e
suites, the terraform workbooks and the account teardown engine all delete
node groups first; the console shows the refusal's message.
* fix(eks): refuse new add-ons and access entries on a deleting cluster
CreateAddon and CreateAccessEntry checked only that the cluster record
existed, so a dependant could be written to a cluster whose delete was
already recorded, and the teardown would then sweep it with the record.
Both now answer ResourceInUseException for a DELETING cluster, a code both
operations declare in the EKS model.
* test(e2e): DeleteCluster returns DELETING at once and never reads ACTIVE after
The first delete-cluster must return within 5 s with status DELETING, as
AWS does. While the cluster is still listed every DescribeCluster must read
DELETING, and a repeat delete must answer DELETING from the record; once it
is gone a further delete answers ResourceNotFoundException.
* docs(eks): record the cluster delete lifecycle contract
The contract ADR-0003 S1 asks for before a conformance claim: what the
request decides, the recorded obligation, how the teardown converges and
recovers, and the S8 and S7 tests that pin each item, with the remaining
gaps listed.
* test(eks): wait for the delete request before counting terminates
On a build whose DeleteCluster is synchronous the request goroutine is still
recording terminates when the assertions read, so the tests must wait for it
to return as well as for the background teardown.
* test(eks): classify every eks request subject by its handler's work
The ADR-0003 S8 subject table for EKS. Every eks.* subject the gateway sends
and the daemon serves is classified in eksSubjects, and the table fails for
one that is not, so a new subject is classified deliberately rather than by
inheriting the 30 s default. DeleteCluster is request-scaled. Four handlers
still hold the request for work that grows with the resource and are named
as not conformant: CreateCluster's FAILED-name reclaim, DeleteNodegroup,
UpdateNodegroupConfig and RestoreSnapshot. Their budget stays 30 s, since a
larger one would hide the defect.
* fix(eks): leave no child record behind a launch overtaken by a delete
A create launch still running when its cluster was deleted and swept could
seed the creator's access entry afterwards, leaving it under a cluster with
no record. The launch now re-checks the record after seeding and, finding it
gone, removes the cluster's child keys with the infra it holds. The teardown
also sweeps child keys again after deleting the meta, for a write that lands
between its two deletes.
* fix(eks): fence the create launch on its generation and keep its infra owned
Review findings on the async delete, each with a test that fails before:
- The launch's meta write checked only the status, so a launch overtaken by
a delete could overwrite a newer cluster of the same name and then release
by name. Writes are now fenced on CreatedAt and the ARN; a superseded
launch releases only the instances it started, by ID.
- A launch whose record was swept released its infra once, best effort. It
now recreates the record as DELETING with its refs, so the teardown and
the reaper own and retry the release.
- A record marked FAILED under a running launch was treated as owned by a
teardown; the launch now merges its refs and releases what it built.
- Child records: a fresh claim clears keys an earlier cluster of the name
left, the teardown stops the cluster's bootstrap subscriber, bootstrap
refuses to write into a record that is gone or DELETING, the creator's
access entry and the GPU device add-on are gated on the record.
- Deleting the last node group of a DELETING cluster resumes its delete,
and waiting on node groups no longer counts as a failed pass.
- One converger run is bounded at 15 minutes, so a hung step cannot hold
the gate and lease for ever; the timed-out pass is recorded.
- The FAILED-name reclaim purge runs under the teardown gate and lease.
- The meta-last test drives the converger with a failing KV instead of
DeleteClusterPrefix, which production no longer called and is removed.
- The S7 test asserts the teardown issues no volume delete and terminates
no worker.
* fix(eks): keep a deleted cluster's record while its create launch runs
Second review of the async delete, each behavioural change with a test that
fails before:
- A create launch now heartbeats its owner and time on the record, and the
teardown keeps the record while that heartbeat is fresh, so DescribeCluster
never answers NotFound and then DELETING again when an overtaken launch
recreates the record. The launch hands its refs over at its next write and
starts the teardown when it returns; a heartbeat silent for 90 s is taken
as a crashed launch, and recreating a swept record stays as the backstop.
- The launch's OIDC keypair writes are fenced on the record's generation, so
an overtaken launch cannot replace a newer same-name cluster's signing key.
- Re-creating a FAILED cluster's name is no longer refused by the per-node
teardown bound.
- A deadline reached while removing the record is still recorded; an exited
bootstrap subscriber removes its own entry; a record that is not DELETING
is refused as not recorded even when it has node groups.
The guest's SQS nats-server listens on 127.0.0.1 and the worker dials it
there, but the kernel leaves lo down, so the store failed to bind and the
worker never polled. Also correct the stale CA comment in init.
DescribeTargetGroups rejects a malformed target group ARN with ValidationError
and reports a missing group with AWS's message, DescribeLoadBalancers fails with
LoadBalancerNotFound for an unknown name, and DescribeListeners fails with
LoadBalancerNotFound for an unknown load balancer ARN. The EKS cluster NLB lookup
treats the new not-found as absent.
* feat(sqs): run queue and message owners in the worker by signed pull
The SQS worker's ready step now opens the queue and message owners on
its dedicated store and serves owner calls it pulls from its node's
gateway over the existing instance-role channel (PollWork,
CompleteWork). Each poll and post is authorized against the current
worker identity, and a result is accepted only from the identity that
took the work. No payload crosses platform NATS.
A receipt-signing key is created on the worker disk only under the
first-store decision that creates the store key, and its fingerprint
is bound in the store; a missing or changed key refuses to serve.
Received messages are released only when the gateway proves the caller
left; receives are capped so a worst-case result fits one post.
The public relay is not registered in production.
* fix: harden SQS relay results and bound pending bytes
Unavailable outranks client faults when encoding owner errors, and wire
errors carry fixed text only, never owner or backend text. The ingress
rejects incomplete or mismatched success results as unavailable, and the
relay bounds total pending input bytes as well as request count.
Adds tests of every relay action against real owners, the receive claim
limit, release, faulting work, the receipt-key fingerprint marker and the
poll loop through gateway failures.
* fix: refuse SQS serving outside a single-node host cluster
DescribeStore now reports the gateway's declared cluster node count, and
the worker refuses to serve, before reading keys, connecting or creating
any store state, unless that count is exactly one. A gateway that reports
no count reads as zero and is refused.
* feat(sqs): record a store server key pin once per store instance
Add the store role and its internal actions so the bound store instance
can learn its claim and record the public pin of its TLS server key,
once, without the platform holding the private key. The worker sees the
store as bound only after a pin exists and must receive a valid pin.
* refactor(sqs): serve store server keys from a separate capability
Keep the worker Stores interface unchanged and give the store guest's
actions their own ServerKeys dependency, so existing Stores fakes need
no store-key methods.
* feat(sqs): launch the store with a mgmt NIC, CA and scoped store role
The store now boots with the region, gateway URL and the daemon's own
cluster CA, a mgmt NIC to reach the gateway, and an sqsStoreRole profile
granted only the store's two internal actions, so it can authenticate
its server key pin. A store record that predates these fields refuses
to relaunch; its volume is kept.
* feat(sqs): keep the store TLS server key on its disk and record its pin
The store image gains sqs-store-key, run after the data disk is mounted.
It loads or creates a P-256 server key under the disk's tls directory,
records only the key's public pin with the gateway as the bound store
instance, and writes a self-signed certificate for it to the run
directory. nats-server then requires TLS 1.3 with that key.
A record for another volume, a recorded pin with a missing key, or a key
that does not match the recorded pin refuses the store; no key is
regenerated and nothing on the disk is deleted. Launch-not-yet-bound and
lost replies are retried. The store role vocabulary moves to
contracts/sqs/v1 so the guest agent does not import a domain.
* feat(sqs): pin the worker's store connection to the recorded server key
The worker's readiness check and serving connection now use TLS 1.3 and
accept only the store server key whose pin the bound store recorded with
the gateway. The pin is checked before the worker key or any queue or
message data is sent, and on every reconnect. A store with another key,
a plaintext store or a malformed pin is refused as final.
The worker NKey client authentication is unchanged. The host still
holds the store's data disk key, so the pin does not protect the store
from a privileged host.
* fix(sqs): retry store key bootstrap and track guest binary sources
The store's server key check moves from sysinit into the respawned run
step: a gateway or IMDS outage exits after a pause so init retries it,
without remounting or creating a new key, while a missing, changed or
wrong-store key is recorded as refused and never retried. Stop also ends
a running key helper before unmounting.
The guest helper and worker binaries now depend on every main-module
package dir and file they link, from go list, so edits, additions,
deletions and renames all rebuild them.
* feat(sqs): operator enable and status with a signed ingress test
Add a per-Region SQS service record (disabled by default, pinned to one
node, refused on multi-node clusters before any write), gate the relay on
it, and expose `spx admin sqs enable` and `spx admin sqs status`. The
gateway now hands queued work to the polling worker and reports whether
one is polling.
The public SQS service stays unregistered in production until its
capability profile is reviewed. An integration test registers it through
the harness and drives real SigV4 and IAM requests to a real worker for
create, send, receive and delete, asserting no SQS body crosses platform
NATS.
* fix(sqs): report readiness only from observed component state
Status now reports whether the tenant SQS API is registered on this
gateway and is never Ready while it is not. The worker stops polling while
its store connection is down, so a worker that lost its store drops out of
the gateway's poller window instead of looking healthy.
* wip: sqs model inventory
* wip: sqs public registration
* wip: sqs registration review fixes
* fix(sqs): refuse pre-API store and worker records on enable
A store or worker record this release creates now carries an explicit
contract. enable reads both records without creating a bucket and refuses
any record without it before it writes desired state or launches anything;
status names the record and is never Ready. A worker replacement keeps its
predecessor's contract, since it reuses the same data volume.
Missing metadata is only ever grounds to refuse: nothing is upgraded,
inspected, emptied, replaced or deleted. Upgrading a pre-API dev install is
not supported.
* fix(sqs): describe the enable refusal as an unsupported launch contract
A missing or unknown contract proves neither when a record was written nor
that it predates authenticated bootstrap: an older writer can drop a newer
field, and a future contract is not pre-API. The refusal is unchanged; its
message, status field and docs now name a missing or unsupported launch
contract and say any existing volume is kept.
* wip: port reviewed SQS serving path onto dev without the store VM
* wip: single SQS system VM
* fix(sqs): retry receipt bootstrap under supervision and drop store install path
Run the receipt-key boot step from the respawned store service so a
transient gateway or IMDS outage recovers without a reboot; final key
and volume refusals stay refused. Remove the separate store image
install and staging path from setup.
* fix(sqs): align worker ready config fields
* fix(sqs): stop an in-progress boot before unmounting the worker disk
The receipt-key boot step ran as an unrecorded child of the store shell,
so shutdown stopped only the shell and could unmount the disk under a
boot still talking to the gateway. Record the boot child like the other
guest processes and stop it before unmounting; a survivor keeps the disk
mounted.
* fix(sqs): record the boot child before its stopping check
The parent wrote boot.pid only after starting the child, so a shutdown
in that gap could unmount under an unrecorded boot. The child now
records itself and checks stopping before it runs sqs-worker boot.
Bumps the docker-images group with 1 update in the /build directory: golang.
Bumps the docker-images group with 2 updates in the /docs/terraform-workbooks/demo-app directory: golang and distroless/static.
Bumps the docker-images group with 1 update in the /scripts directory: debian.
Updates `golang` from `972b411` to `d9b6a1b`
Updates `golang` from `f2c872b` to `d200d2d`
Updates `distroless/static` from `18f4cc2` to `25ac8f0`
Updates `debian` from `62bd423` to `f6b08b0`
---
updated-dependencies:
- dependency-name: golang
dependency-version: 1.27.3-bookworm
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: docker-images
- dependency-name: golang
dependency-version: 1.27.3-alpine
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: docker-images
- dependency-name: distroless/static
dependency-version: nonroot
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: debian
dependency-version: trixie-slim
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(test): never hand KillProcess a PID a real process can hold
* fix(iam): queue same-record writers so a looping self-write cannot starve DetachUserPolicy
* fix(ecs-agent): report RUNNING before starting container exit-waits
* fix(rds): wait for the lease-loss cancellation instead of sleeping in the takeover test
* fix(cli): name the account when the follow deadline lands mid-poll
* test(imagecatalog): never answer from the deadline fixture
* test(nats): order Gather StopOnFirst frames causally, not by sleep
* fix(ec2/key): find a key pair by name past an unreadable metadata record
* fix(accountlifecycle): re-list at the drain deadline so a just-deleted resource is not stuck
* fix(eks): give only the exhausting subtest the short SG delete budget
* fix(eks): stop healthz and nodegroup Ready-gate tests racing fixed wall-clock windows
* fix(test): let in-process NATS clusters bind their own route ports
* fix(objectstore): list the memory store in S3 key order with real paging
* test(viperblock): create a fresh volume on every run of the local-state test
* test(ochre): close router streams so drained tests release their admission slot
* test(cli): restore shared cobra flags after each ochre command test
* test(daemon): keep shared-JetStream, metric and RSS tests correct on repeat runs
* fix(ochre): let a warm-up probe stop so a test's poller cannot hit a later test's server
* test(cli): pin the package's cluster size so a multi-node config load cannot strand later fixtures
* feat(admission): add the quota override seal capability contract
* feat(awsgw): serve the quota override seal capability per node
* refactor(accountlifecycle): parameterise the seal capability probe and add the quota one
* test(accountlifecycle): pin the probe wire shape against both capability contracts
* feat(accountlifecycle): seal the quota override record during account teardown
* docs: record the quota override seal contract and teardown behaviour
* docs: fill in the quota override seal PR number in the package boundary migration
The ISO cells build the rootfs as root inside the workspace and only cleaned
it at the start of their own job, so the next job on the same runner from any
workflow failed its checkout on files it could not remove.
* fix(eks): delete the Node of a terminated nodegroup worker
K3s runs no AWS cloud provider, and its own reports every instance as
existing, so a worker terminated by DeleteNodegroup, scale-down or reclaim
stayed a NotReady Node forever. Its pods stayed bound to it, and once most
Nodes were NotReady the node lifecycle controller stopped evicting, leaving
metrics-server on a dead Node.
Add an internal-workers route the control plane pulls the cluster's live
nodegroup workers from, and a mulga-eks-node-gc agent on every server that
deletes a nodegroup Node only when it is not Ready and its instance is no
longer one of those workers, as the AWS cloud controller does.
* test(e2e): check the live worker's Node and wait out nodegroup deletes
WorkerNodeReady accepted any Ready Node, the tolerate-all DaemonSet check
dropped pods with no Ready condition, and nodegroup cleanup returned before
the nodegroup was gone. Scope the Ready check to the CPIsolation nodegroup,
count a pod with no Ready condition as not Ready, wait for each nodegroup
delete, and assert that deleted nodegroups leave no Node behind.
* test(integration): account for the internal ListInternalWorkers route in the cross-tenant table
NetworkManager restarted while /etc/resolv.conf is a plain file runs in
rc-manager=symlink mode and writes resolv.conf on its first DNS update, which
can land after the installer links the stub. Write a conf.d drop-in
(dns=systemd-resolved, rc-manager=unmanaged) before the restart, refuse a
later file that overrides rc-manager, wait for NetworkManager to settle, and
assert the link holds with bounded relinks. A rerun after an older handover
adds the drop-in, reloads NetworkManager and relinks the stub.
* test: pin the ENI owner stamp and failed-LB cleanup contracts
Failing tests: an ENI update after the caller's own write must survive a
lagging replica read, an unwritable owner stamp must fail the system
launch, and the reaper must terminate the failed LB's own VM only.
* fix: stamp the ENI owner in the attach write and release a failed LB's VM
A lagging-replica read before the owner stamp's compare-and-swap lost the
stamp, leaving a system VM that IMDS could never resolve.
- AttachENIWithOwner records the instance owner in the attach write.
- ENI read-modify-writes re-read and re-apply on a revision conflict,
bounded, and report the store error to the caller's log.
- A system launch whose ENI cannot be attached or stamped fails and cleans
up the VM.
- The reaper that marks an LB failed terminates that LB's own VM and
detaches its ENIs.
* fix: log ENI write retries and exhaustion
An Info line on a write that succeeds after a revision conflict and an
Error line when the bounded retries run out, so the sink can count both.
* test: cover an internal ALB in a private subnet and report the ENI attachment owner
DescribeNetworkInterfaces now returns Attachment.InstanceOwnerId when the record has one, as AWS does, so the owner stamp is observable. The e2e launches an internal ALB in a VPC with no internet gateway and checks the owner, the private address and in-VPC traffic. The ENI write error log now reports the real attempt count, so a non-conflict store error is not counted as an exhausted retry.
* revert: stop reporting the ENI attachment owner through Describe
AWS always returns Attachment.InstanceOwnerId, so reporting it only for system-owned ENIs adds a divergence. The e2e now relies on the LB reaching active with a healthy target, which needs the owner stamp to resolve.
* test: direct-boot LB and SQS worker NICs must be virtio-net-pci on microvm pcie=on
* fix: boot LB and SQS worker microvms on PCIe with virtio-net-pci NICs
virtio-mmio devices share level-triggered IRQ lines and a NIC's RX path
stalled on them. The direct-boot machine gets pcie=on, so NICs are
virtio-net-pci with MSI-X, and the virtio_mmio.device= kernel args go.
IsMMIO is now the transport check (false with pcie=on) and IsMicrovm the
machine check. Data volumes keep their serial and ID on virtio-blk-pci.
The LB guest console loglevel is raised to 7.
* refactor: trim microvmMachineType comment
* docs: describe direct-boot microVM NICs and data volumes on PCIe
* refactor: move the gateway into ingress/aws/pipeline and retire the gateway layering rows
The gateway's request pipeline joins the packages it already consumed.
Operation inventory moves to ingress/aws/dispatch. Files keep their names;
only the package line, the import paths and the now-redundant pipeline
qualifier change. The layering rows for the gateway root are removed: the
package is under ingress, so R2 binds it, and the daemon-direction test now
asserts R2 rejects runtime, operator and domain imports.
An empty registry_helper_test.go was dropped rather than moved.
* refactor: rename pipeline.GatewayConfig to pipeline.Config
* docs: update the design and package boundary docs for the gateway move
* docs: point the EKS add-on lifecycle evidence at the tests' current files
RebootInstances powered the guest down under -action shutdown=pause and
then ran system_reset + cont in the same QEMU process. On some hosts the
reset left the guest silent in firmware with QMP still answering, while a
fresh QEMU on the same volumes always booted.
A reboot now shuts the guest down over ACPI (forcing it off after 60s, as
before), ends that QEMU, and starts a fresh one on the same mounted volumes,
taps, IMDS datapath and identity, the way nova's soft reboot and Proxmox's
qm reboot do. The instance stays running in Describe, no hooks fire, the
console log is appended rather than truncated, and the existing QMP client
is reconnected in place so the heartbeat carries on.
A relaunched guest that writes nothing to its console within 60s has its
run state, vCPUs, vCPU thread states and registers (sampled twice) logged,
and if it has printed before, its QEMU is replaced once more. A relaunch
that cannot start goes to crash recovery. Stop, terminate and hot-plug
wait for a relaunch in flight so volumes are never sealed under a process
being started. Guest-initiated reboots stay an in-process reset.
The daemon now logs the host kernel, QEMU version and the OVMF package
and checksum at start, and every reboot relaunch carries them.
* fix(vm): release an auto-assigned public IP before a stop publishes stopped
* fix(e2e): skip a TryReady caller once the suite's SSH datapath is marked broken
* fix(e2e): skip a catalog entry whose third-party mirror is unreachable, still fail on a non-2xx
* fix(e2e): retry the diskperf fio install with backoff before calling it an infra fault
* fix(e2e): make the storagefault root-load check observe the load it started
* fix(ci): give the single suite a 40m budget and the nightly cells that run it 60m
* fix(ci): fail a nightly cell fast when a hypervisor it provisions on is out of disk
Three defects in the fence path made partition recovery fail:
- onVolumeLeaseLost read the lease holder before killing nbdkit, with no
deadline, spending the 5s margin between local validity and the server
TTL. The kill now comes first and the read is bounded at 2s.
- The fence notice was one NATS publish, lost when the node's NATS
restarts mid-fence. It is now a request repeated until the daemon
acknowledges, dropped once the volume is exported here again.
- A fenced guest got a 20s ACPI powerdown, a 20s pidfile wait and a
SIGTERM, keeping QEMU alive while a survivor relaunched it. It is now
SIGKILLed, and resumeAfterIOError never resumes a fenced guest.
The routed-NAT host route to a VPC CIDR was installed only by the vpcd
that received the IGW attach event, which goes through a queue group,
and by whichever node won a leased drift pass. Every other node waited
for the five-minute resync, and with two VPCs on one CIDR each node kept
whichever route it met first.
Each node now runs a host ingress loop outside the drift lease. It reads
intent and the gateway LRPs on every intent write, on a nudge from the
local IGW hooks and every 30s, installs or repoints the routes, and
prunes tagged routes no attached VPC holds. A shared CIDR goes to the
bootstrap VPC, then to the lowest VPC ID.
The kernel tracks connections only while some rule uses conntrack. With
inet spinifex_filter unloaded nothing tracked, so OVN RAFT links formed
while disarmed (or before the daemon's first arm) were first seen
mid-stream at arm time. With tcp_loose a packet from the server end became
a new flow from peer:6643/6644 to an ephemeral port and hit the drop rule,
and RAFT lost its leader.
Add table inet spinifex_conntrack (accept-only) and keep it loaded on
every node: setup.sh loads it at install, the boot unit loads it
unconditionally, firstboot loads it before OVN, and disable swaps the
policy for it in one transaction instead of turning conntrack off.
* test: pin the order of the unauthenticated mounts against SigV4 and the throttle
* refactor: mount the OIDC and registry routes through a public-routes hook
* refactor: take the admin registration name and request-id context as gateway config
* refactor: move node discovery out of the gateway into the awsgw role
* refactor: drop the gateway config fields production never reads (Config, RootCAs, InternalSuffix)
* refactor: drop the gateway Version and Commit config fields
* refactor: compose the production gateway config in one tested function
* refactor: drop the gateway IAMService, STSService, NATSConn and ExpectedNodes config fields
Production never reads them: composeGateway builds Policy and Readiness from
its own inputs. Tests that used the config as a carrier now keep those
collaborators in gwfix.Deps, recorded per config with gwfix.WithDeps and read
back with gwfix.DepsOf. Gateway non-test code now imports nothing under
domains/, runtime/ or operator/.
Test edits are setup only: 153 GatewayConfig literals in 45 files had the four
keys moved into gwfix.WithDeps(..., gwfix.Deps{...}), and 113 field reads
became gwfix.DepsOf(x).{IAM,STS,NATS,Nodes}. No assertion changed.
Script 1, literal rewrite (balanced-brace scanner, keys moved out of the
literal):
import re,sys,subprocess,os
KEYS={'IAMService':'IAM','STSService':'STS','NATSConn':'NATS','ExpectedNodes':'Nodes'}
def scan_close(s,i):
# s[i]=='{' ; return index of matching '}' ; skips strings, runes, comments
d=0;n=len(s);k=i
while k<n:
c=s[k]
if c=='"':
k+=1
while s[k]!='"':
k+= 2 if s[k]=='\\' else 1
elif c=='`':
k=s.index('`',k+1)
elif c=="'":
k+=1
while s[k]!="'":
k+= 2 if s[k]=='\\' else 1
elif s.startswith('//',k):
k=s.index('\n',k)
continue
elif s.startswith('/*',k):
k=s.index('*/',k)+1
elif c in '{([': d+=1
elif c in '})]':
d-=1
if d==0: return k
k+=1
raise Exception('unbalanced')
def split_top(body):
parts=[];d=0;start=0;k=0;n=len(body)
while k<n:
c=body[k]
if c=='"':
k+=1
while body[k]!='"': k+= 2 if body[k]=='\\' else 1
elif c=='`': k=body.index('`',k+1)
elif c=="'":
k+=1
while body[k]!="'": k+= 2 if body[k]=='\\' else 1
elif body.startswith('//',k):
k=body.index('\n',k); continue
elif body.startswith('/*',k): k=body.index('*/',k)+1
elif c in '{([': d+=1
elif c in '})]': d-=1
elif c==',' and d==0:
parts.append(body[start:k]); start=k+1
k+=1
parts.append(body[start:])
return parts
changed={}
files=[l for l in subprocess.check_output(['git','ls-files','*.go']).decode().split() if os.path.exists(l)]
SKIP={'spinifex/gateway/gateway.go','spinifex/runtime/roles/awsgw/compose.go','spinifex/runtime/roles/awsgw/awsgw.go'}
lit=re.compile(r'(&?)((?:gateway\.)?GatewayConfig)\{')
total=0
for f in files:
if f in SKIP: continue
s=open(f).read()
if 'GatewayConfig{' not in s: continue
out=[];pos=0;cnt=0
for m in lit.finditer(s):
if m.start()<pos: continue
o=m.end()-1
c=scan_close(s,o)
body=s[o+1:c]
parts=split_top(body)
keep=[];moved=[]
for p in parts:
km=re.match(r'\s*(IAMService|STSService|NATSConn|ExpectedNodes)\s*:\s*(.*)$',p,re.S)
if km: moved.append((KEYS[km.group(1)],km.group(2).strip()))
else: keep.append(p)
if not moved: continue
# rebuild
kb=','.join(keep)
if kb.strip()=='' : kb=''
elif not kb.rstrip().endswith('\n') and '\n' in body: pass
d=', '.join(f'{k}: {v}' for k,v in moved)
new=f'gwfix.WithDeps({m.group(1)}{m.group(2)}{{{kb}}}, gwfix.Deps{{{d}}})'
if not m.group(1): new=f'gwfix.WithDeps(&{m.group(2)}{{{kb}}}, gwfix.Deps{{{d}}})' # value literals flagged below
out.append((m.start(),c+1,new)); pos=c+1; cnt+=1
if not m.group(1): print('VALUE LITERAL',f,s.count('\n',0,m.start())+1)
if not out: continue
r=[];last=0
for a,b,n in out:
r.append(s[last:a]);r.append(n);last=b
r.append(s[last:])
s2=''.join(r)
if 'testkit/gwfix"' not in s2:
s2=re.sub(r'import \(\n','import (\n\t"github.com/mulgadc/spinifex/internal/testkit/gwfix"\n',s2,1)
open(f,'w').write(s2); total+=cnt; changed[f]=cnt
print('literals rewritten',total,'files',len(changed))
Script 2, read rewrite (driven by the compiler's "has no field or method"
errors for the type, so only GatewayConfig receivers are touched):
import re,subprocess,sys,collections
M={'IAMService':'IAM','STSService':'STS','NATSConn':'NATS','ExpectedNodes':'Nodes'}
def run():
env=dict(__import__('os').environ,GOWORK='off')
out=''
for tags,pk in [('',['./spinifex/gateway','./spinifex/runtime/roles/awsgw/...']),('integration',['./tests/integration/...'])]:
cmd=['go','test','-count=1','-run','NONE_X','-gcflags=-e']+(['-tags',tags] if tags else [])+pk
r=subprocess.run(cmd,capture_output=True,text=True,env=env)
out+=r.stdout+r.stderr
return out
fixed=0
for it in range(8):
out=run()
errs=re.findall(r'^(\S+\.go):(\d+):(\d+): (.*?) undefined \(type (\*?(?:gateway\.)?GatewayConfig) has no field or method (\w+)\)',out,re.M)
if not errs:
print(out[-3000:]); break
byfile=collections.defaultdict(list)
for f,l,c,expr,t,fld in errs: byfile[f].append((int(l),int(c),expr,fld))
for f,es in byfile.items():
lines=open(f).read().split('\n')
for l,c,expr,fld in sorted(set(es),reverse=True):
line=lines[l-1]
# expr like 'gw.IAMService' or 'x.y.IAMService'
new=f'gwfix.DepsOf({expr[:-len(fld)-1]}).{M[fld]}'
if expr not in line: print('MISS',f,l,expr); continue
lines[l-1]=line.replace(expr,new)
fixed+=1
s='\n'.join(lines)
if 'testkit/gwfix"' not in s:
s=re.sub(r'import \(\n','import (\n\t"github.com/mulgadc/spinifex/internal/testkit/gwfix"\n',s,count=1)
open(f,'w').write(s)
print('read sites fixed',fixed)
* docs: record the gateway mount hooks and config-field removal in the package boundary migration
* docs: fill in the 11d-1 PR number in the package boundary migration
* test: pin service-keyed gateway behaviour before it moves to registration metadata
* refactor: declare the XML throttle code on the registration
The pipeline picked Throttling for iam, sts, eks and route53 by name. EKS is a JSON registration and never reached that arm, so it is not carried over.
* refactor: declare the XML error shape on the registration
EC2 and the operator surface declare the EC2 shape. S3 is not served here, so the composition declares its scope traits and a rejected S3 request keeps S3's body.
* refactor: declare the global signing region on the registration
* refactor: supply the unsigned web-identity exchange as a pre-auth handler
STS composition now hands the pipeline a dispatch.PreAuth. The gateway no longer recognises STS or the action by name, and the layering allowlist row for gateway -> sts/awsapi is paid off.
* refactor: declare payload-body scopes as traits
The action recovery for a rejected request skipped S3 by name. The composition now declares the S3 scope as carrying object data, and the gateway reads the trait.
* test: wire the integration harness through PreAuth
* test: pin control-plane no-agent config, etcd member removal and CP isolation e2e
* test: tighten CP isolation e2e and assert a logged prune failure
* fix: run EKS control planes without a k3s agent and prune etcd members directly
Control-plane VMs no longer register as Nodes, so pods cannot schedule onto a
network that cannot reach the workers or the advertised endpoint. With no Node
to delete, a replaced peer's etcd member is removed by a new eks-etcd-member
helper that matches exactly one member by peer-URL host.
* fix: refuse to remove the local etcd member by ID, not only by IP
Self-protection no longer depends on first boot deriving its own address: the
helper asks the connected endpoint for its member ID and refuses a match on it.
* test(e2e): align EKS suite with control planes that are not Nodes
Assert zero Nodes and delivered addon objects before any nodegroup. Check Ready worker, metrics APIService, addon deletion and pod logs once a worker exists, scoping logs to Ready nodes and waiting 10m for metrics-server to reschedule.
* docs: EKS control-plane hosts are not Kubernetes Nodes
Document that kubectl get nodes lists workers only, add upgrade notes for existing clusters, and list the eks-etcd-member helper.
* docs: explain why addon manifests keep the CriticalAddonsOnly toleration
Comment-only. The toleration stays for clusters created before control-plane hosts stopped registering as Nodes.
* test: cover the etcd member client and CLI against a TLS fake etcd
The real etcd client and the command had no tests, leaving the PR below the
diff coverage threshold. A fake etcd serving the Cluster and Maintenance APIs
over mutual TLS drives the client with the K3s certificate layout, and main
becomes a testable run function.
* fix: bound the console's instance-type payload and stop polling it
DescribeInstanceTypes with capacity=true returned one entry per free slot per
node: 3,528,765 bytes of XML for 50 distinct types on prod, refetched every
five seconds. The response was sized by free capacity, so the emptier the
cluster the larger it got, and at 44,346 slots it exceeded the NATS maximum
payload and the console reported no compute at all.
The listing now names each admissible type once. The free-slot figure crosses
as a number through a new GetInstanceTypeCapacity action, open to any account
because the launch form a tenant uses is what needs it. The catalogue query no
longer polls, and the EKS and ECS dialogs read it only when open.
The gateway negotiates gzip inside the SigV4 group, which excludes the OCI
Distribution registry structurally. A response is compressed only if the client
offered gzip, the handler declared neither a length nor an encoding, the
content type is neither streaming nor already compressed, the status carries a
body, and 1 KiB was written before any flush.
An unknown capacity figure no longer caps the count at 1. The launch decides,
and RunInstances answers InsufficientInstanceCapacity if it cannot.
* test: read the free slot count from its own action in the console e2e
Two e2e tests encoded capacity as repeated instance-type entries, so one
expected a caption the deduplicated listing can no longer produce. The fixture
answers GetInstanceTypeCapacity instead, which lets a test give the catalogue
and the figure different answers.
A third test covers the case the change introduces: a session whose policy does
not grant the action reads no figure, so the form offers the type with no
capacity caption and leaves the launch enabled.
The committed bundle is rebuilt, which rehashes every chunk reaching the two
query modules.
* fix: bound the capacity describe reply by instance-type count, not free slots
A node answering `DescribeInstanceTypes` with `capacity=true` sent one full copy of an instance type's description per free slot, so the reply grew with how much capacity the host had free.
At 1344 bytes per slot the four-node production cluster, holding no guests, produced 874 slots and a reply over the 1 MB NATS maximum payload.
Every node's reply was dropped, the gateway collected zero frames, and it returned an empty list as a success.
The console reads an empty capacity list as no capacity and refuses every launch, which is how a cluster with hundreds of free vCPU reported "No compute available".
The emptier the cluster, the more likely the failure, which is the opposite of what the shape should be.
A node now returns each admissible type once with the number of instances of it the node can take, so its reply is bounded by the size of the instance-type catalogue and not by free capacity.
On the production catalogue that is 84 KB for 62 types whatever the slot count, against 59 MB for the same reply on a large idle host under the old shape.
The gateway sums the per-type counts across nodes and expands them on the last hop, because the client SDK drops response fields it does not model and the console displays the figure to the customer.
A capacity fan-out that collected no frames now returns a retryable ServiceUnavailable.
Reporting an empty list as a success is what turned a transport failure into a false statement about the cluster.
Two tests pin the shape: the node reply for an idle 4096-vCPU host stays under the NATS payload limit, and a capacity fan-out with no responders does not return an empty success.
* test: cover the console against a real cluster, and pin the capacity banner
The console has never been exercised against a real control plane.
`playwright.config.ts` pins `localhost:3101` and `:3102` and intercepts every API call, so the suite cannot be pointed at a cluster and cannot see a defect whose whole content is what the control plane actually sends.
That is why an idle production cluster came to tell customers it had no compute: the capacity listing was dropped in transport, the empty result read as no capacity, and nothing in CI could reach the page that says so.
`playwright.live.config.ts` runs the same console against a cluster named by `SPX_CONSOLE_URL`, signing in with `SPX_ACCESS_KEY_ID` and `SPX_SECRET_ACCESS_KEY`.
It starts no server, mocks nothing, runs one worker because it launches real guests, and is never part of `pnpm e2e`.
The suite signs in, asserts a cluster with free capacity offers instance types rather than reporting none, reads the per-type capacity figure, launches an instance and waits for `running`, reads its live metric graphs, terminates it in a `finally` block, and loads the nine listing pages a customer reaches from the sidebar.
Three tests in the mocked suite pin the banner's own logic, which is cheap and catches the UI side of the same failure: the form offers what the capacity listing reports, it shows the free slot count for the selected type, and an account with genuinely no capacity is told so and cannot submit.
The third passes on the current code and is the one that documents why the banner exists at all.
* test: correct three live-console selectors found by running it against prod
The first run of the live suite was 11 passed and 2 failed, both in the suite rather than in the console.
The dashboard check looked for node status on `/`, where it is not shown.
It now loads `/nodes` and asserts the `Resource usage by node` and `Instance type capacity` tables, which is where the daemon reports a node's own capacity.
The OS tile's radio is `sr-only` behind the distribution logo, so `check()` failed its actionability test on the `<img>` covering it.
The click goes to the enclosing `<label>` instead and the browser forwards it.
The terminate dialog holds its confirm disabled until the phrase is typed, so the teardown waited out its timeout on a disabled button and left the instance running.
The helper types `terminate` first.
With those three corrected the launch, running, live-metrics and terminate path passes against the production console in 34 s.
* fix: let a snapshot go once every volume cloned from it is already deleting
DeleteVolume now records State=deleting and leaves the teardown to the
reclaim reaper, so a clone stays listed after the tenant has deleted it.
DeleteSnapshot counted that clone as still reading the snapshot and refused
with InvalidSnapshot.InUse, which broke DeleteDBSnapshot after deleting the
DB instance restored from it and left the retained source volume behind.
A deleting volume's teardown removes only its own objects and nothing moves
it out of deleting, so it no longer holds the snapshot's chunks.
* test(e2e/rds): delete a snapshot straight after the instance restored from it
The subtest deleted the source instance between the two, which gave the
volume reclaim reaper time to finish the restored volume on a fast node and
hid the refusal the nightly hit. Deleting the snapshot immediately makes the
regression deterministic.
The release wait now covers a full reclaim sweep: since deletion became
asynchronous a released volume is gone only after the reaper's next pass.
* refactor: one authorization gate in ingress/aws/pipeline
The key merge and the caller guards move out of the gateway so tests that
drive a registration directly use the same gate as the pipeline.
* test: run ACM handler and policy tests in domains/acm/awsapi
Adds internal/testkit/gwtest, which invokes a registration through the
pipeline's own authorization gate with real policy evaluation.
* test: share policy assertion helpers through gwtest
* test: run ELBv2 handler and policy tests in domains/elbv2/awsapi
* refactor: one actor projection in ingress/aws/pipeline
* test: run RDS handler and policy tests in domains/rds/awsapi
* test: run ECS handler and policy tests in domains/ecs/awsapi
* test: assert the exact policy gate verdict in gwtest
* test: run EKS authz and wire tests in domains/eks/awsapi
* test: run IAM authorization tests in domains/iam/awsapi
The policy-driven DeleteRole and AddRoleToInstanceProfile tests run in the IAM package through gwtest, with PassRoleTo shared. The HTTP-handler, examples and signed-pipeline tests stay in gateway.
* test: run ECR authorization tests in domains/ecr/awsapi
* test: run ECR repository, image and lifecycle-preview tests in domains/ecr/awsapi
* test: run ECR dispatch error tests in domains/ecr/awsapi
* test: run Bedrock authorization tests in domains/ochre/awsapi
* refactor: move EC2 authz tests into domains/ec2/awsapi
* test: restore the ECS scope-table completeness gate in domains/ecs/awsapi
* Revert "test: restore the ECS scope-table completeness gate in domains/ecs/awsapi"
This reverts commit 0211a0a1bf.
Shows deploy-vultr.sh building a private Vultr VPC with one node or three,
the nodes clustering over the private network for the control plane, EBS
and S3, Spinifex calling the Vultr API for reserved IPs, and the AWS
services running across the fleet.
Drops the horizontal rules the docs site rendered as stray text, links
the AWS CLI install page, captions each screenshot, shows the expected
spx get nodes output for three nodes, explains what a three-node
deployment protects against, and simplifies the limits to the sizing
and quorum a reader needs.
* feat(sqs): queue and Lambda-facing message API foundation
Adds the SQS JSON 1.0 adapter (unregistered in production) with queue
create, lookup, attributes, list, tags and the message operations a
Lambda poller uses: SendMessage(Batch), ReceiveMessage with long
polling and attribute selection, DeleteMessage(Batch) and
ChangeMessageVisibility(Batch), backed by the queue record owner and
the JetStream message store. String and Binary message attributes are
validated, stored and checksummed; Number attributes are refused until
AWS behaviour is confirmed.
* test(sqs): poller lifecycle through the message owners
Covers receive, release, redelivery, delete, visibility extension, queue
binding of receipt handles and long polling against the real queue and
JetStream message stores, from the message package itself.
* test(sqs): move SDK coverage from the gateway to the SQS domain
Drives the registration through dispatch.Invocation over the real queue
and message owners, so the gateway package carries no SQS changes.
* test: pin gateway pipeline order, request ids and region
* refactor: carry request facts in one ingress request record
* fix: seal the request principal once
* refactor: build the caller ARN once in credverify
* refactor: gateway takes policy and admission as capabilities
* refactor: gateway takes cluster readiness as a capability
* chore: gofmt GatewayConfig field alignment
* docs: record slice 11a request record and capabilities
* docs: add PR number to slice 11a migration row
* refactor: move pipeline capability interfaces to ingress/aws/pipeline
* feat: Vultr reserved-IP provider and floating external-address pool
Adds a source="vultr" external pool backed by Vultr reserved IPs: an API
client pinned by captured responses, an in-memory fake with the measured
semantics, and a floating allocator that creates, claims, reconciles and
releases labelled addresses. OCI and Vultr claims now share one
AddressClaimer pass.
Also fixes the host firewall dropping guest IMDS when imds_host_meta_ip is
set, by matching the pre-DNAT destination, and arms the Spinifex firewall in
place of an active ufw.
* fix: detach a Vultr reserved IP before deleting it
Vultr was measured refusing delete of an attached address with 400 "Unable to detach subnet", so release, rollback and reconcile now detach first. A first claim after allocation logs at info rather than warn.
* fix: release the auto-assigned public IP an Elastic IP displaces
AssociateAddress restamped the ENI with the EIP and never handed back the
address launch had assigned. Nothing referred to it afterwards, so it stayed
allocated for good, and on a cloud pool every associate left a billed address.
AWS releases it on associate; withdraw its NAT rule and release it, scoped to
the ENI, leaving any address another associated EIP holds alone.
* fix: retry Vultr's refused detach on reserved-IP detach and delete
Vultr has answered a detach with 400 "Unable to detach subnet" and accepted
the same detach moments later. Surfacing it left the address for reconcile
to collect, up to twenty billed minutes later; retry it like a 5xx instead.
* fix: stop reporting the released launch address after an EIP is disassociated
Associating an EIP now releases the auto-assigned public IP, but the stored
instance kept its launch stamp, and DescribeInstances fell back to it once the
EIP was disassociated. The public-IP change handler now moves the stamp with
the runtime address.
* feat: add a Vultr deploy script for a single-node Spinifex on a VX VM
deploy.sh up/destroy/status drives Terraform for the VPC and VM, installs and
forms Spinifex over SSH, copies the API key to the node rather than through
user-data, and sweeps boot volumes and reserved IPs Terraform does not own.
Cloud-init keeps needrestart off the Spinifex, OVS and OVN units and disables
unattended upgrades on these disposable hosts.
* feat: form a three-node Vultr deployment with install-node.sh over the VPC
Each node comes up as an armed single node, the state install-node.sh forms
from, with the cluster and Geneve planes on the VPC NIC. The pool and every
node's provider_host_id go on each node after formation.
* fix: read the pinned host key with ssh-keygen -F when forming a Vultr cluster
ssh_config hashes known_hosts entries by default on Debian, so a grep for the
address found nothing and set -e ended the run without a message.
* docs: add the Vultr guide, one command to deploy one or three nodes and one to remove them
* docs: drop the failover write note from the Vultr guide
* fix: keep needrestart from restarting the Spinifex, OVS and OVN units
Guest QEMU runs in spinifex-daemon's cgroup, so needrestart charged QEMU's
replaced libraries to the daemon and restarted it after every dpkg run. The
restart never cleared them, so an unattended upgrade ran the unit into its
start limit and left the control plane down.
setup.sh now writes /etc/needrestart/conf.d/90-spinifex.conf, deselecting
spinifex-, ovs-, ovsdb-, ovn- and openvswitch- units. needrestart still lists
them, so restarting after an upgrade stays the operator's choice.
* chore: drop the Vultr cloud-init needrestart override now setup.sh writes it
* docs: replace the Vultr guide's apt upgrade warning now the installer keeps needrestart off the stack
* docs: install the Vultr guide's build from the dev channel until Vultr support is released
* fix: answer ARP for Vultr reserved IPs on bare metal
Vultr routes a reserved IP to a VM by MAC but ARPs for it on a bare-metal
server's public segment, and a Vultr pool gave vpcd neither a gateway nor a
bind bridge to proxy-ARP on, so nothing answered and no traffic arrived. A
Vultr pool now proxy-ARPs on the interface the node routes the internet
through. setup.sh installs arping, which the gratuitous ARP already called.
* feat: deploy Spinifex on Vultr bare metal, renamed deploy-vultr.sh
A vbm- plan creates bare-metal servers, one or three. Without --name the
deployment writes the AWS profile spinifex, up prints every node's address
and the API key allowlist, credentials and ssh subcommands are added, no AMI
is imported unless --image is given, and OpenTofu or Terraform is used. The
host key is pinned only once cloud-init is done, since bare metal first
answers SSH from Vultr's installer with a different key.
* docs: rework the Vultr guide into the OCI guide's flow, with bare metal
* fix: restamp LaunchTime when a stopped or recovered instance relaunches
The pending watchdog times a launch from LaunchTime, which still held the
original launch. An instance recovered from a failed node, or started more
than five minutes after it was first launched, was terminated as stuck in
pending seconds after QEMU started. AWS restamps LaunchTime on every start.
* feat: spread the nginx-alb workbook across nodes, and power Vultr nodes off and on from deploy-vultr.sh
The workbook takes server_count and placement_strategy, so three web servers
can run one per node. Its README now matches the template (public subnets,
no NAT gateway) and the diagram shows the spread. deploy-vultr.sh gains
stop and start, which halt and start a node through the Vultr API.
* fix: let a terminate finish tearing down before the reaper calls it wedged
OrphanQEMUReaper finalized any shutting-down instance whose QEMU had exited,
on the premise that a live terminate still has a live QEMU. A terminate is
still deleting volumes, releasing its IP and leaving its placement group for
tens of seconds after QEMU exits, so the instance was reported terminated
first. Terraform then deleted the placement group and got
InvalidPlacementGroup.InUse. The reconcile now waits two minutes from
shutting-down, well inside the ten-minute stuck-terminate backstop.
* fix: release an ovs-vswitchd left in flow-restore-wait after a node boots
ovn-controller clears flow-restore-wait once its flows are installed and
sets it again straight after. When both writes reach ovs-vswitchd as one
update, it only sees true, starts no upcall threads and forwards nothing.
Seen on Vultr bare metal after a hard power-off: instances recovered onto
the rebooted node ran but were unreachable. vpcd now clears the flag on any
node held that way for a minute, longer than ovs-ctl's own restore takes.
* docs: add a resiliency test to the Vultr guide and the nginx-alb workbook
The Vultr guide gains step 8, which spreads three web servers across a
three-node deployment, powers a node off and back on, and shows what was
measured on VMs and bare metal. The workbook gains the same test as step 6.
The guide's table of contents is repaired, and its limits now cover losing
two nodes and recovery inside a spread placement group.
* fix: never terminate an instance that has run because a relaunch failed
The pending watchdog terminated any instance pending past five minutes, and
terminating deletes DeleteOnTermination volumes. A start of a stopped
instance, a recovery from a failed node and a daemon relaunch all pass
through pending, so a slow one destroyed an existing instance and its root
disk. On Vultr a recovered instance was terminated seconds after QEMU
started and its root volume deleted.
An instance is now marked FirstLaunch from RunInstances until it first
reaches running, cleared before the running state is written. The watchdog
acts only on first launches, as AWS terminates only a launch that never
ran. MarkFailed refuses to terminate any other instance and marks it
recovery-failed instead, which keeps its volumes, ENI and addresses and
shows it as stopped. A claimed relaunch is never a first launch.
Invariant tests pin ADR-0003:S7. The instancerecovery e2e suite gains a
test that ages its guest past the watchdog before recovering it, then
stops and starts it, and requires the instance, its root volume and the
data on it to survive both.
* feat: run the Terraform workbooks against a Vultr deployment on demand, and show the Vultr portal in the guide
e2e-cloudvendor-vultr.yml is dispatch-only. It builds the ref, deploys it
with deploy-vultr.sh on one or three VMs or bare-metal servers, runs the
published workbooks from node1 with the same driver as the hypervisor
nightly and OCI, and destroys the deployment, failing if anything is left
billing. It needs one secret, VULTR_API_KEY, and generates its SSH key per
run. The guide gains screenshots of the API key's access control list and
of the nodes in the Vultr portal, and its portal navigation now matches.
* fix: install setup.sh on Vultr nodes when the tarball does not carry it
build-install-artifacts.sh writes setup.sh beside the tarball, so a node
installed from its tarball had no /usr/local/share/spinifex/setup.sh and
arming the firewall failed.
* fix: pass the remote command after the destination in the Vultr e2e driver
* fix: run each workbook in the cluster's own region
The workbook driver passed no region, so every workbook used its default,
ap-southeast-2, and on a cluster in any other region every request failed
SignatureDoesNotMatch. It now passes the region of the node's spinifex
profile.
* feat: count a node's vCPUs as online CPUs and reserve by host size (ADR-0015)
A node counted physical cores, collapsing SMT threads, and held back a
fixed 2 vCPU, so a 16-thread host offered 6 vCPU to instances. A vCPU is
now one online CPU, as AWS counts it: a thread with SMT on, a core with
SMT off or on ARM. The OS and Spinifex keep 1 CPU up to 8, 2 up to 16, 4
up to 63 and 8 from 64, and the rest is allocated one to one. The
existing overrides stay operator-only. Invariant tests pin every tier
boundary.
* docs: state the Vultr guide's sizing under the online-CPU count
* docs: drop an unmeasured EKS sizing claim from the Vultr README
* fix: let a guest reach an Elastic IP hosted on its own node
A guest's packet to an EIP on its own node left its VPC from the transit
range, was masqueraded to the transit host address, and went back into
OVN, which exempts that range from EIP DNAT, so it was never answered.
On Vultr an EKS worker could not reach its control plane's endpoint, so
it never joined. AWS lets an instance reach a public address in its own
VPC.
Hairpinned transit traffic now jumps to SPX-EIP-HAIRPIN, above the egress
MASQUERADE, where each EIP's rule sends it from the uplink address, as the
host's own traffic to the EIP already is. The rule follows the EIP's
ingress plumbing in and out, and anything else returns to the MASQUERADE.
* fix: resolve Spinifex's LB and EKS names on Vultr nodes
Vultr's Ubuntu image writes a static resolv.conf naming Vultr's resolvers,
so lookups bypassed the split DNS Spinifex configures in systemd-resolved
and an EKS endpoint name did not resolve on the node. deploy-vultr.sh
points resolv.conf at the resolved stub, as stock Ubuntu does.
* test: prove a guest reaches an EIP on its own node in the natuplink e2e
The nat-single cells run it with a public pool, so the hairpin path that
left EKS workers unable to reach their control plane on Vultr is now
covered nightly.
* docs: record the natuplink hairpin check in the e2e coverage index
* fix: keep each Vultr node's logs when an e2e run fails
The driver destroyed a node whose install failed together with the log
that said why. It now copies /root/*.log from every node into the run's
artifacts before the teardown, and the workflow uploads them.
---------
* test: pin EKS worker IMDS metadata options and launch template resolution
* fix: launch EKS workers with IMDS hop limit 2 and honour launch template metadata options
* fix: default unset launch template metadata fields for EKS workers
* test: e2e coverage for EKS worker IMDS hop limit and launch template metadata options
* test: drop the unneeded GetCallerIdentity policy from the worker IMDS e2e
* docs: describe the EKS worker IMDS defaults and launch template overrides
* test: drop the stale GetCallerIdentity policy from the IRSA pod e2e
A node counted physical cores, collapsing SMT threads, and held back a
fixed 2 vCPU, so a 16-thread host offered 6 vCPU to instances. A vCPU is
now one online CPU, as AWS counts it: a thread with SMT on, a core with
SMT off or on ARM. The OS and Spinifex keep 1 CPU up to 8, 2 up to 16, 4
up to 63 and 8 from 64, and the rest is allocated one to one. The
existing overrides stay operator-only. Invariant tests pin every tier
boundary.
* feat: lay the Spinifex UI design-system foundation
Brand tokens in OKLCH with semantic status and a colour-blind-checked
chart palette, Archivo headings and a 13px type scale, and a pre-paint
theme script so dark mode never flashes white under the console CSP.
Adds the missing Base UI primitives, a /design catalogue route, a route
ratchet against hand-written tables, a ban on chart-library imports in
routes, and lets the content column shrink so wide tables scroll in
place. The dev server now forwards /api and /proxy to the Go console.
Sonner is removed: its inline style element is blocked by the CSP.
* feat(ui): side panels, TanStack table, uPlot charts and e2e gate
- DataTable renders @tanstack/react-table, with column hiding as container
queries so columns drop when a side panel narrows the list.
- UniFi-style DetailPanel and ResourceView: Instances, VPCs and Internet
gateways open in a panel beside the list from 1024px and as a full page
below; selected, tab and create live in the URL. Create flows for
instances, VPCs and gateways open in the same panel.
- Instance monitoring charts on uPlot (CPU, network, packets), on simulated
data and badged as such.
- Dates in the AWS console form, e.g. 2026/06/04 02:00 GMT-5.
- Tabs style Base UI's data-active, so the selected tab now shows.
- Playwright suite gates CI as the UI End-to-End job in ui.yml.
* feat(ui): one details component for panel and page, UniFi-style overlay panel
The side panel now lies over the list at full height with a shaded edge, and a
click on the list's background closes it. Instances, VPCs and internet
gateways each render one details component as either the panel or the
resource's own page, so the page gains the monitoring graphs and the panel
gains IAM role, GPU, console output, change type and create image. SSH moves
into Instance information as rows. Internet gateways attach and detach from
the panel.
* feat(ui): roll the panel pattern out to every service and gate screenshots in CI
Every EC2, IAM, Route 53, Bedrock, EKS, ECS and RDS resource is now a
list plus a side panel, with one details component serving as both the
panel and the one-column page. DetailCard, DetailRow, ListCard,
ResourceList and DeleteResourceButton are deleted.
- Panel: slide-out close, row selection closes it, expand keeps the tab
and fetched console output, PanelSpinner while loading.
- Instances: info tips, theme-aware coloured console output, power
button group plus Actions menu, Stop and Reboot confirms, security
group, Elastic IP and network interface sections, and a live
one-second monitoring window on simulated data.
- Launch instance: OS tiles picking the newest version, architecture
choice, location tile, expanded root volume, honest default subnet.
- On/off settings are switches app-wide; disabled controls show
cursor: not-allowed.
- The e2e job runs in the pinned Playwright image with screenshots
gating; make ui-baselines and make ui-e2e-container use the same image.
* feat: bulk select and delete on every resource list, OS marks and instance type sizes
Every list with a delete action gets row checkboxes and a selection bar.
Delete confirms in a dialog naming each resource, makes one call per
resource, keeps each refusal against its row and retries only failures.
The Launch form shows distribution marks for Ubuntu, Debian, Rocky,
Alpine and CentOS, served as files under the CSP, and states the
selected instance type's vCPUs and memory.
* feat: sign-in overlay, larger type scale, typed confirms on irreversible deletes and dialog shortcuts
The sign-in card sits over an inert shell. Any sidebar group folds, and entering one unfolds it. Instance DNS names copy. Escape cancels and Cmd/Ctrl+Enter confirms every dialog. Volume, snapshot, RDS, EKS, ECS and hosted zone deletes need "delete" typed, and terminate needs "terminate".
* feat: fade sign-in and sign-out in place, and show the confirm shortcut
Signing in no longer reloads the page: the card and a lighter veil fade out and the console routes to the dashboard. Signing out cross-fades into the sign-in card with a Signed out notice. Confirm buttons show Cmd or Ctrl Enter for the platform.
* feat(ui): generic sidebar group names and a paged S3 object browser
The sidebar names its groups Compute, Databases, Authentication and Object
Storage; the command palette still finds each by its AWS service name.
Buckets get row selection and delete. Inside a bucket, folders and objects
share one table paged by ListObjectsV2 continuation tokens, with a find box
that queries by prefix, multi-select delete through DeleteObjects (folders
with everything under them), and an object panel showing HeadObject
properties, user metadata and a download.
* feat(ui): show upload progress and stop the S3 proxy cutting off slow transfers
The Upload button shows a running batch's percentage, and a large or slow
batch opens a dialog with bytes sent, bit-rate, time left and per-file
cancel and retry. Uploads run in a store outside React, so they continue
while the user browses.
The console's S3 proxy now allows an hour per request. The server's 15 s
ReadTimeout cancelled every upload part that took longer, so large uploads
failed on links below roughly 18 Mbit/s.
* feat(ui): stream live EC2 metrics to the Monitoring tab
The Monitoring tab drew simulated data. It now streams CPU, network
bytes and packets from the owning node's qmp-collector every 500 ms
while someone is watching, with nothing stored.
- qmp-collector: a watched sampler on schedstat and tap counters, on a
30 s lease, keeping five minutes in memory for reconnects. It answers
only watches for the account in the VM's discovery file.
- awsgw: spinifex:GetInstanceMetrics, checked against the instance ARN,
answered as text/event-stream in 60 s segments. Another account's
instance is InvalidInstanceID.NotFound.
- console: a signed stream reader that reconnects with Since and backs
off on failure or an empty segment, stops on InvalidAction, and a
chart axis that scrolls between samples. The simulator and its badge
are removed.
* feat: catch refused creates in every form, answer the third review, and guard tables
Every create form now catches a refused mutation, so the failure shows beside
the submit button and no longer escapes as an unhandled rejection. Three EKS
tabs that swallowed a refused create now show it.
Third-review answers: the attach dialog offers running or stopped instances in
the volume's zone; the default security group's delete is disabled with its
reason; the load balancer panel splits into Listeners and Attributes tabs; a
main route table lists its implicit subnets; IAM single delete dialogs name the
same dependants as the bulk ones; a snapshot can be deleted in any state.
Every list in a route renders through DataTable, enforced by a unit test with a
named allowance, and the /design baselines are redrawn in the pinned container.
* feat: cap live metrics streams, prune hidden selections, and move panel focus
Each gateway holds at most 10 live metrics streams per account and 200 in
total, refusing past either with RequestLimitExceeded, and counts opened and
refused streams. A row that leaves a list leaves its selection, the instances
page uses the shared selection hook, the detail panel takes focus on open and
returns it on close, and Escape yields only to the popup it was pressed in or
a modal. The UI workflow also runs on live metrics backend changes.
* test: authorise an ECR token from the live role once its session is gone
* feat: carry the role ID in an ECR token minted for an assumed role
* feat: rehydrate an assumed-role caller from the live role
* fix: authorise an ECR token from its live role after the session expires
* docs: note that an ECR token outlives its role session
* test: characterise EC2 request labels and the cluster-unavailable gate order
Pins the span, audit, throttle and policy-gate labels an EC2 request carries through the signed pipeline, and that the NATS gate answers before policy, action lookup and gateway-local actions.
* refactor(gateway): register EC2 through the ADR-0002 seam
EC2 is composed in awsgw from domains/ec2/awsapi and served by the
registration seam. The legacy service switch and its tables are removed,
along with the two paid-off gateway allowlist rows.
* docs: record the EC2 seam registration in the package boundary migration
* docs: record the EC2 seam registration PR number
DeleteVolume returned only once every object behind the volume was gone, which
made the call's latency a function of the volume's size. Object removal costs
one round trip per object, so no timeout is both short enough for a control
plane request and long enough for a large volume: on a 100 GiB volume the
provider request overran its 30 s budget at 52 s, the tenant was told the
delete failed, and the teardown that had already started was left with no
record accountable for it.
AWS does not work this way. DeleteVolume's output in the EC2 model is
smithy.api#Unit, VolumeState includes deleting, and the documentation says a
volume can remain in that state for several minutes.
DeleteVolume now validates, decides every AWS-returnable error, records
State=deleting and returns. VolumeReclaimReaper converges the teardown and
removes the record only once it is complete, so an unreachable or saturated
blob node delays reclamation instead of failing a delete the tenant has already
been told succeeded. The reaper is cluster-wide and leader-gated, which is also
what serialises the teardown: three concurrent destructive handlers ran one
volume's objects on prod because a retry had nothing to collapse against.
A volume already recorded as deleting answers a second delete from the record,
which collapses both the tenant's retry and DeleteOnTermination's own delete.
Two ordering fixes behind it. The provider handler now tears down local state
before deleting objects, because the local half is fixed-cost and sat behind the
half that scales, so a timeout left this node's WAL and dirty marker behind for
good. And volume.delete and snapshot.delete are classified as data movement,
since both reference real data and neither answers from state already held.
Guest QEMU runs in spinifex-daemon's cgroup, so needrestart charged QEMU's
replaced libraries to the daemon and restarted it after every dpkg run. The
restart never cleared them, so an unattended upgrade ran the unit into its
start limit and left the control plane down.
setup.sh now writes /etc/needrestart/conf.d/90-spinifex.conf, deselecting
spinifex-, ovs-, ovsdb-, ovn- and openvswitch- units. needrestart still lists
them, so restarting after an upgrade stays the operator's choice.
* test: characterise EC2 through the signed gateway pipeline
* refactor: serve EC2 actions from domains/ec2/awsapi over composed capabilities
The EC2 action table, idempotency and validation move out of the gateway behind a Deps struct. Quota is a consumer-side interface and the instance adapters take a narrow InstanceProfiles interface instead of the whole IAM service. The gateway only composes Deps and calls Dispatch. The 18 gateway to EC2 awsapi allowlist rows are deleted.
* test: drive every EC2 action through Dispatch in its own package
Covers the request errors, the gateway-local actions without NATS and each routed action reaching its handler against a stub daemon.
* docs: record the EC2 capability move in the package boundary migration
* docs: record the EC2 capability move PR number
* feat: persist the SQS worker's store key on its own disk
The shared SQS worker generates its store-client NKey inside the guest and
keeps it in an owner-only directory on its own persistent disk, so no platform
component creates or unseals the seed. The key is synced and linked into place
without replacing anything; a missing, mismatched, unreadable or loosely
permissioned key fails closed instead of being regenerated.
* fix: make the SQS worker key durably named before returning it
Every successful call now syncs the key directory and its entry in the parent
before handing out a key, so a retry after a failed directory sync cannot
return a key whose name is not yet durable. Only the key directory is created,
inside a parent that must exist, so an unmounted worker disk fails closed. The
first-start decision is explicit: an existing store's authorized key is
required and never defaults to generating a new one.
* docs: state that workerkey cannot detect an unmounted worker disk
An existing parent directory does not prove the worker disk is mounted, since
the mountpoint can exist on the initramfs after a failed mount. The error now
says only that the parent is missing, and the package documents that its
caller must verify the expected data volume is mounted first.
* feat: add the SQS worker guest bootstrap
The shared SQS worker microVM boots with sqs-worker boot, which selects
exactly the disk whose serial fw_cfg names, checks and mounts it at /data,
and proves from the mount table that this disk is what is mounted before
loading or creating its store key there. A missing, empty or unknown store
status, a contradictory or invalid store key, a missing or ambiguous disk, a
failed check or mount, an empty mountpoint or a foreign mount refuses without
creating a key. Only the public key is logged and published.
The image is built by the store image script under SQS_MICROVM_PROFILE=worker,
with the worker binary, no nats-server and an empty /data mountpoint; the
store image's contents are unchanged.
* fix: refuse replayable new-store status at SQS worker boot
A static fw_cfg store-status of "new" survives a guest reset, so after the
store exists it could still reach key creation and replace a lost worker key.
Boot now accepts only "existing" with the store's authorized public key and
refuses "new" before touching any disk, until a current authoritative
first-store decision exists. workerkey.ForNewStore remains, uncalled.
* feat: add SQS first-store claim and worker key acquisition
The SQS domain records one first-store decision per Region in its own KV
bucket, moving claimed -> key-recorded -> launch-intent under CAS and bound
to the worker's instance ID, claim ID and generation. Key creation is
authorized only while no public key is recorded; afterwards only the
recorded key is returned, and a launch intent requires it. Older
generations, same-generation rivals and unreadable records authorize
nothing, and records hold no private key material.
workerkey.Acquire syncs a new key to disk and returns it only after its
public key is recorded, reusing the on-disk key after a crash before
recording and failing closed on a missing or different key afterwards.
No transport carries the decision yet.
* fix: read SQS first-store decisions from the stream leader
KV.Get uses a direct get, which a lagging replica can answer, so a stale
claimed read could authorize key creation after the leader had recorded
the worker's public key, or make a superseded generation look current.
Reads now use the SQS store's leader-answered stream message get, and
every authorizing answer is also confirmed by a compare-and-swap at the
revision read, so a stale or unavailable read authorizes nothing.
* feat(sqs): add regional worker identity record and instance index
The SQS domain keeps one worker identity record per Region with a monotonic
generation, claim ID, pinned node and encrypted data volume, moving from
launching to bound or failed under compare-and-swap. A launch intent takes
the next generation before any instance exists, so the previous worker stops
being current; binding writes the instance index entry first.
Authorize accepts an instance only while the Region's leader-read record is
bound to exactly that instance, generation and claim, confirmed by a
compare-and-swap at the read revision, so an old, orphaned, rewritten or
unbound index entry, or a lagging replica, authorizes nothing.
Both this bucket and the first-store bucket now open through the kvutil
chokepoint and refuse a stored configuration that could expire, cap or evict
records or holds fewer replicas than the cluster needs. Nothing calls the
store yet.
* fix(sqs): make worker launch exclusive and harden durable buckets
BeginLaunch now refuses a new claim while the previous attempt is launching
or bound, including one whose outcome is unknown, so only one caller ever
holds an intent to start a VM on the pinned volume. A replacement takes the
next generation only after FailLaunch or the new RecordStopped confirms that
nothing holds the volume, and each request names the generation it
supersedes, so a replayed request cannot take a generation it never saw.
The durable bucket opener also refuses non-limits retention, per-message
TTLs, asynchronous flush, a message-size cap, any discard policy other than
discard-new or a per-subject discard, unacknowledged writes, a sealed
stream, and a mirror or sources.
* feat: add internal SQS worker first-store gateway actions
Serve ClaimFirstStore and RecordWorkerPublicKey under the internal
spinifex-sqs signing service, apart from the public AmazonSQS inventory.
Only an sqsWorkerRole session in the system account passes the class
gate, then the role policy; the worker identity is resolved from the
current workerident record by the STS-set session name, never from the
request, and any extra parameter is refused. Seeds and other non-public
keys are refused without being recorded, logged or echoed.
The gateway binds the worker-identity and first-store buckets per
request and never creates them, so it provisions nothing and answers
ServiceUnavailable until a worker launch has created them.
* fix(sqs): fence replaced worker generations out of first-store
A request that passed worker authorization before a confirmed stop and
replacement could reach an unclaimed first-store record ahead of the
successor and create or record the old generation's key. The launch path
now raises a monotonic first-store fence past the replaced generation
before BeginLaunch, which refuses a replacement until a leader read shows
it; firststore refuses every call from a generation below the fence.
The internal adapter no longer logs caller-chosen parameter names.
* feat(sqs): launch the shared SQS worker on its pinned node
Add the SQS worker system-instance kind and a domain launcher that
realizes the Region's worker from its identity record without ever
starting a second VM:
- domains/sqs/workerlaunch returns a bound worker as is, prepares the
data volume on first launch only, refuses a volume whose own record
is not encrypted, marks the intent dispatched before launching and
never resends a dispatched launch: an ambiguous outcome stays
launching until reconciled. A replacement starts only after a
confirmed stop or failure and the first-store fence, reusing the
pinned volume.
- workerident gains MarkDispatched and Current; firststore gains a
leader-read RecordedKey.
- The daemon launches the worker through the shared direct-boot path
with its encrypted volume attached at boot and kept on terminate, a
required instance profile and a mandatory mgmt NIC. Its firmware
config carries only public values: the data serial, the store
decision, the gateway URL on this node's bridge and the daemon's own
cluster CA. A relaunch is refused on any other node.
- sqsWorkerRole is ensured in the system account granting exactly the
two internal actions; an existing role whose trust admits more than
EC2 is refused. The role contract lives in domains/sqs/workerrole so
the composition root does not import the gateway adapter.
The worker network allocator and any caller remain unwired, and the
guest still refuses to create a key for a new store.
* fix: verify SQS worker role trust before granting and report unclaimed volumes
Check that an existing or racing-created sqsWorkerRole trusts only EC2
before writing its policy, and accept the instance profile only when a
re-read shows it carries exactly that role. Refuse the first launch
before allocating when the cluster storage key is not configured, and
return the ID of any volume created but not confirmed in a launch
intent instead of dropping it.
* feat: add private SQS system VPC and worker ENI allocator
Add systemvpc.EnsurePrivate, which builds one VPC and one private subnet
from the VPC and subnet surface only, so it cannot create an internet
gateway, route table, NAT gateway or EIP. The daemon's SQS worker network
adapter uses it with a [sqs] supernet defaulting to 10.240.0.0/14 and
finds or creates one ENI per launch claim in the VPC default security
group, refusing a duplicated claim or an address outside the subnet.
Reword UnclaimedVolumeError: a lost BeginLaunch reply may still have
recorded the volume.
* feat: worker takes its first-store decision from the gateway at boot
The SQS worker guest no longer trusts a static fw_cfg store status. After
selecting, checking and verifying the mount of its own data disk, it asks
the internal spinifex-sqs gateway service for its current first-store
decision, signed with instance-metadata credentials only, over TLS 1.3
trusting only the cluster CA. It then creates and records or loads the
key on that disk; a key lost after recording is never replaced.
- internal/sqsworkergw: SigV4 client for ClaimFirstStore and
RecordWorkerPublicKey; refuses non-public keys, non-decisions and
redirects; errors carry only the parsed code
- gwsign.NewInstanceRole: IMDS-only credentials, no env/profile fallback
- workerboot: retries only lost replies and ServiceUnavailable/Throttling
- launch contract carries the Region instead of a store status or key
* fix: worker retries a denial while its launch is still binding it
The worker VM can reach the gateway before its launch binds the instance
identity, and that early call is legitimately AccessDenied. The guest
treated the denial as final and stayed unprepared even after binding.
AccessDenied is now a pending authorization the guest asks about again
within its existing bound; a denied claim creates nothing, and a worker
that is never bound, or was replaced, gives up at the bound.
* feat(sqs): add operator EnsureSQSWorker action and node launch handler
Add the super-admin EnsureSQSWorker(NodeId, InstanceType) action on the
internal spinifex operator service. It validates the node against the
cluster and as a single subject token, then requests
sqs.worker.Ensure.v1.<node> with no queue group, so only that node answers
and the worker stays pinned.
The node handler re-checks the named node and instance type and runs one
composed workerlaunch.Launcher (real volume preparer, role, private network
and direct-boot launch), one launch at a time, in the daemon's own context
with a 30-minute bound. A caller timeout does not cancel the launch; a
retry reports the bound worker, or IncorrectState for a dispatched launch
whose outcome is unknown, which is never resent. The reply names the bound
VM and does not claim the worker or SQS is ready. Nothing starts a worker
at daemon startup.
* feat: ship the SQS worker image and check it before allocating
Build the SQS worker microVM image in both release workflows, stage it into
the tarball as microvm-sqs-worker, and install it with setup.sh under the
store image's rules: a release without it removes an earlier one and an
incomplete one is refused. Add make install-sqs-worker-microvm.
The worker launcher now asks a Host capability whether this node can boot a
worker before it prepares a volume, records or resumes an intent, fences a
replaced generation, allocates the network or dispatches. The daemon checks
for a non-empty kernel and initramfs, its mgmt bridge and the cluster CA, and
the EnsureSQSWorker handler reports a refusal as ServiceUnavailable, so a
call after the image is installed launches instead of leaving an ambiguous
dispatched intent.
Cover missing and incomplete images through the admin action and handler,
a stopped worker left untouched, and an ENI create whose reply is lost.
* fix: never repeat an uncertain SQS worker ENI create
The launcher now looks for the claim's ENI without creating one, marks the
launch intent NetworkAttempted, and only then sends the single create. Once
marked, a missing ENI fails closed with IncorrectState instead of a second
create that could give the claim two interfaces; a visible ENI is reused.
Adds a test that buildSQSWorkerLauncher wires the daemon's real host check.
* feat(sqs): operator-triggered SQS store launch on the worker's node
Add the super-admin EnsureSQSStore action and its node handler. The
store is launched only on the node the Region's worker is bound to,
with the public key that worker recorded in the first-store decision;
the daemon never receives the worker's seed.
A per-Region store launch record marks each step before its uncertain
create (volume, ENI in the worker's private subnet, VM launch), so a
step whose result is not visible fails closed and is never resent.
Volumes are refused unless their own record is encrypted, and their
IDs are kept. The store guest runs under GOMEMLIMIT=512MiB and
instance types at or below that are refused; this is a floor, not a
validated size.
* fix: retry uncreated SQS store volumes and fix the store instance type
A volume preparation that failed before sending any create (a missing
tool, scratch or format failure, or no cluster storage key) now carries
storevolume.ErrNotCreated. The store launcher withdraws its volume mark
and returns ServiceUnavailable, so a retry may try again once the host
is fixed; any create that was sent stays one-shot.
The store launch record now keeps the operator's instance type. A
request for a different type is refused as IncorrectState before
dispatch and after binding, and the reply names the launched type,
which the admin action checks.
* feat: SQS worker store readiness and store size check
The worker's one-shot ready step mounts and verifies its disk, asks the
internal spinifex-sqs DescribeStore action for the bound store, loads its
key only if it matches the recorded public key, and checks the store's
JetStream with it. It retries within a bound while the store is pending or
not answering, and stops on a mismatched or refused key. The NKey
authenticates the worker only; the store server is not authenticated.
EnsureSQSStore now also refuses a different volume size once the store is
bound, instead of returning the existing store.
* fix(sqs): keep worker store readiness current until shutdown
The readiness step now runs as a guest-local watch loop with no overall
deadline: each pass is bounded, a pending or unreachable store is retried
every few seconds, and a ready store is rechecked periodically. The
store-ready marker is removed on any failed pass, cleared at start and
removed on exit; shutdown stops the watcher by its recorded PID. Terminal
refusals such as a rejected or mismatched key back off for minutes rather
than spinning. The NKey still authenticates only the worker to the store.
* feat(sqs): supervise the worker readiness watcher and add spx admin sqs
Run the worker guest's readiness watcher under init respawn, rate-limited,
and remove any stale store-ready marker before each start, so a killed or
crashed watcher cannot leave a false readiness marker. Shutdown blocks
respawn, removes the marker, stops the recorded watcher by PID and start
time with SIGKILL escalation, and unmounts /data only once it has exited;
a surviving watcher leaves every mount in place.
Add spx admin sqs ensure-worker and ensure-store, which call the existing
EnsureSQSWorker and EnsureSQSStore actions through the signed spinifex
gateway path with the standard AWS credential chain. Both refuse missing
inputs before sending anything and report only the bound VM, never
readiness.
* fix: park SQS worker readiness and fix the worker instance type
A readiness start that finds shutdown in progress or no worker key now
logs once and waits in place instead of exiting, so init's respawn does
not restart and log it in a loop; shutdown stops it like a watcher.
The worker identity record keeps each generation's instance type. A
request for another type is refused as IncorrectState before dispatch
and after binding, and only a replacement may change it. The ensure
reply, admin action and spx admin sqs ensure-worker report and check
the recorded type.
* fix: record the SQS worker volume intent before creating it
The first worker launch prepared its data volume before any record
existed, so a create that applied but returned no ID left nothing for the
next call to find, and it could prepare a second volume.
The launch intent is now recorded first, and its one volume create is
marked before it is sent. A Prepare proven to have sent nothing withdraws
the mark and is retryable; any other outcome is never resent and returns
IncorrectState, naming the volume when its ID is known. An intent that
has sent no create may still be re-typed.
* docs: document spx admin sqs ensure-worker and ensure-store
Record the flags, the caller and owning accounts, the bound-versus-ready
distinction and the current retry and recovery limits.
* docs: distinguish SQS ensure timeouts from refusals that created nothing
A gateway timeout means only that no reply arrived; the launch may still be
running. The worker type locks once any step has been attempted.
* docs: state what each SQS ensure ServiceUnavailable establishes
No refusal proves earlier attempts created nothing; a retry checks the
durable attempt and continues only when that is safe.
* test: relay a capacity refusal from an owner holding unsealed writes
* fix: refuse a start elsewhere when the owner holds unsealed writes
* fix: bound the unsealed-writes check well under the start forward timeout
* test: refuse a volume takeover while the marker owner is live
* fix: refuse a volume takeover while the previous owner is live
* fix: retry EKS control-plane starts refused for capacity or a held volume
* docs: describe the interim unsealed-writes takeover guards
* test: give the viperblockd conformance harness a cluster-state bucket
The takeover guard reads node liveness from the cluster-state bucket the
daemon creates. The harness had none, so the real reader errored and the
guard refused every takeover. Create the bucket the way the daemon does so
the real reader runs and finds no heartbeat.
* fix: leave no dirty marker behind when opening a volume that does not exist
An open takes the lease and writes the dirty marker before the volume's
state is read. When the volume turned out to be absent the marker stayed,
so a later open on another node met the takeover guard and was refused
as held elsewhere instead of answering not found. Clear the marker this
open wrote when the state load reports the volume missing.
* test: characterise the four Bedrock services through the SigV4 pipeline
* refactor: serve the four Bedrock services from one ochre registration
The Bedrock adapters move from gateway to domains/ochre/awsapi behind a
single dispatch registration. The sub-service scopes are aliases, a
sub-service scope selects its dispatcher outright and only the shared
bedrock scope is split by path. The thirteen Bedrock fields leave
GatewayConfig for awsapi.Deps, and the bodyscope allowlist row for
domains/ochre is deleted.
* docs: record the Bedrock registration in the package boundary migration
* docs: record the Bedrock registration PR number
* test: pin the ECR /v2 pipeline through the gateway handler
* feat: rehydrate a principal from an access key id in credverify
Adds credential.Rehydrator and CallerARN on Principal, implemented beside Authenticate and Resolve on the same key, session and user lookups, with a parity table against the gateway's ECR rehydrate.
* refactor: share the credverify key and session lookups with Rehydrate
Authenticate, Resolve and Rehydrate now call one lookup per credential kind, so the checks cannot drift. The parity table also asserts the gateway and credverify caller ARN builders agree.
* refactor: move registry host matching to ingress/http
The matcher takes its service labels from the caller and exposes the parsed account and region through accessors, so ingress names no service.
* refactor: extract the identity-policy evaluator into policyauth
The SigV4 gateway path and the ECR registry authorize through one evaluator and one set of request condition keys, over the credential principal.
* refactor: extract account admission into accountadmission
The SigV4 gateway path asks one Admitter for account status, with the ACTIVE-only cache moved with it, so the ECR registry can use the same capability.
* refactor: move the ECR /v2 bridge out of the gateway
The bridge, token exchange and per-operation authorization now live in
domains/ecr/registry behind registry-owned Principal, Rehydrator and
Authorizer types, so the package imports no ingress code. The awsgw role
adapts credverify, policyauth and host matching to them, and the gateway
keeps only a RegistryHandler mounted at /v2. The bridge refuses to
compose without a verifier, issuer, rehydrator, authorizer or admission,
so the open-registry mode for a nil verifier is gone.
* test: carry the retired ECR principal parity rows into credverify
Global root, a verifier with no IAM, a record with no principal type and
the gone, replaced and legacy session verdicts for user and role sessions
were only exercised by the gateway tests that went with the old path.
* docs: record the ECR /v2 bridge move in the package boundary migration
* docs: record the ECR /v2 bridge PR number
* test: cover AWS-form IRSA condition keys and StringLike on web identity
* fix: match AWS-form IRSA condition keys and allow StringLike on web identity
* fix: keep the provider ARN host-path unnormalised for web identity
* feat: log the reason a web identity trust policy denies
* fix: satisfy errname, modernize and gofmt on the denial error
* test: cover the stock AWS IRSA trust policy in the EKS e2e
* test: make the IRSA denial prove the policy was evaluated
* test: pin ECS and EKS dispatch through the SigV4 pipeline
* refactor: register ECS and EKS on the ingress seam
EKS resolves the cluster and nodegroup role through passrole.Check with the
IAM role resolver, in the order it had. Responses and check order are unchanged.
* fix: match AWS role handling for ECS and EKS PassRole cases
ECS refuses a foreign task role before PassRole, accepts a role that is not an ARN or does not exist after PassRole, and reports a RunTask override role it cannot assume. EKS CreateCluster and CreateNodegroup refuse a foreign or non-role ARN with AccessDeniedException before PassRole.
* test: cover ECS and EKS awsapi registration dispatch
* fix: report EKS parameter faults as InvalidParameterException
* docs: record ECS and EKS gateway migration and EKS divergences
* test: pin the current role-resolution gaps in EC2, ECS and EKS
* feat: add the shared iam:PassRole flow with classified role resolution
* feat: resolve same-account roles for iam:PassRole in domains/iam
* docs: point the /admin migration row at spinifex#1345
* test: characterise IAM and STS dispatch through the SigV4 pipeline
* refactor: register IAM through the ingress seam
* refactor: register STS through the ingress seam
* docs: point the IAM and STS migration rows at spinifex#1346
* refactor: move the /admin handlers and wire types into operator/admin
* refactor: serve /admin through the spinifex registration
* docs: record the /admin move into the spinifex registration
* refactor(gateway): register RDS through the ingress seam
RDS dispatch moves from gateway/rds.go to domains/rds/awsapi.NewRegistration, composed by awsgw and the integration harness.
The caller comes from dispatch.Actor and the principal-class gate still runs before the policy check.
* docs: point the RDS migration row at spinifex#1344
* chore: group the imports merged in the actor projection test
* feat: build a separate SQS store microVM image
Package the go.mod-pinned nats-server, ext4 support and a clean ACPI
power-down into a microVM kernel and initramfs of their own. The build
checks the packed archive's contents and refuses a nats-server whose
version differs from the pin. The image does not start the store and
nothing installs or releases it; the LB image is unchanged.
* fix: publish the SQS store image only after its gates pass
Stage the kernel and initramfs and move them into place only once the
content, size and guest-RAM gates pass, so a failed build cannot leave a
pair make takes as up to date. The old initramfs is removed first and the
new one lands last, so an interrupted publish never leaves a mixed pair.
Check the module list without piping into grep -q, which under pipefail
reported a present module as missing.
* feat: start the SQS store guest on its checked data disk
The SQS store image now prepares, runs and stops a dedicated JetStream
server. It reads a narrow fw_cfg contract (data disk serial, listen
address, one user NKey public key), refuses anything absent, malformed
or ambiguous, has the pinned server test the config, then checks and
mounts exactly one matching ext4 disk. It never formats and has no
ephemeral fallback. The server listens only on the supplied guest NIC
address, admits only that NKey and syncs every write.
Shutdown stops the recorded run process first, whether it is the server
or a start not yet exec'd, and runs the generic read-only remount only
once it is gone. A store that survives SIGKILL leaves every mount alone.
* fix: report the SQS store as prepared, not listening, before it starts
* feat: launch the SQS store guest on its own versioned subject
An explicit SQS store kind carries an existing system-account data volume
and the one NATS user public key the store accepts. It is served only on
system.LaunchSQSStore.v1, so a daemon without this code has no responder
and the launch fails instead of booting a load balancer without its disk;
the legacy launch path refuses the kind on both send and receive.
Every refusal happens before capacity, an ENI or a VM record exists. The
volume goes through the normal EBS mount and terminate detach and is
never deleted on termination. The store boots its own image with CPU
mitigations on, gets only its network, disk serial, listen address and
key, and cannot be relaunched after a host restart yet.
* feat: ship the SQS store microVM image in releases and installs
Release builds now build the SQS store image and stage it beside the load
balancer's as microvm-sqs-store/, so an older installer that copies
microvm/* without -r still works. setup.sh installs it to
/usr/share/spinifex/microvm/sqs-store/, where the daemon looks for it.
A release without a complete image never leaves an older one usable:
staging drops a previously staged image, and the installer removes an
earlier installed pair before warning (missing) or failing (incomplete).
Only the two image files are removed. The initramfs is installed last, so
an interrupted install leaves no bootable mismatched pair.
* feat: restart the SQS store guest on its launching node
Persist the non-secret guest config of an SQS store instance (launching
node, data volume, listen address, client public NKey and rendered
network config) in the instance record, and rebuild its fw_cfg files
from it on relaunch. The relaunch refuses on any other node, or when the
record is missing or malformed, before writing any file; the data volume
is remounted through the normal EBS path and kept on terminate.
Cross-node recovery now never selects or claims an SQS store record, so
no other node spends retry budget on it or relaunches it. Same-node
restore is unchanged, as are load balancer restarts. Older daemons know
neither the exclusion nor the record field, so this does not prevent a
cross-node relaunch by a mixed-version cluster.
* feat: hold prepared system volumes in creating until released
Add an internal CreateVolumeForPreparation path that writes a new
volume's first record as creating, and CompleteVolumePreparation to
release an unattached creating volume in the caller's account to
available. Ordinary CreateVolume is unchanged.
Attach and delete already refuse a creating volume. ModifyVolume,
ForceDetachVolume and CreateSnapshot now refuse it with IncorrectState
so nothing can expand, release or capture a volume mid-preparation.
* feat: prepare fresh SQS store volumes with an ext4 filesystem
The SQS store guest never formats a disk, so a new volume gets its
filesystem on the host. The preparer checks for mkfs.ext4 and qemu-img,
builds an ext4 image in private scratch, creates the volume in
"creating", publishes it on this node, copies the image with the NBD
import helper, unpublishes, and only then releases it as available.
It never writes a volume it did not just create. A volume is deleted
only when no export can still be live: after a confirmed unpublish, or
when the export was not NBD. Any publish error, a failed or ambiguous
unpublish, or a failed release retains the volume and reports its ID.
A publish refused as in-use or already-exists sends no unpublish, since
another export may exist. Cleanup ignores caller cancellation but is
bounded at 30s.
The raw-image copy moves to runtime/compute/nbd unchanged, and the
domain receives it as an injected writer. The volume service gains a
discard path for an unattached volume still in "creating".
* fix: report unknown state and match qemu-img lookup in volume prep
A failed release may still have made the volume available, so the
retained-volume error now says its state is unknown rather than
claiming it is still creating.
The copy runs qemu-img by name through PATH, so the pre-allocation
check now uses a PATH lookup for it too. mkfs.ext4 keeps the sbin
fallback because it runs from the resolved path.
* docs: describe the unwired SQS store system instance
Records what the guest image, versioned launch, same-node restart and
fresh-volume preparation provide, that nothing calls them, and what is
untested or missing before production use.
A dispatch failure was logged at ERROR whatever the caller was about to
be given, so DryRunOperation, which AWS documents as the success answer
to a dry run, read as a fault of ours along with every NotFound and
malformed parameter. The level now comes from the status the caller
receives, resolved the way ErrorHandler resolves it.
QEMU announces its own exit on stderr, so a guest stopping on the
SIGTERM the daemon sent it was logged at ERROR too.
* fix: count volume lease validity from the write, not its reply
The server starts a lease entry's TTL when it applies the write, but the
holder started its validity window when the acknowledgement arrived, so a
slow reply stretched validity past the server's expiry and a peer could
acquire the volume while the holder still considered it valid.
Validity now counts from when each create, generation publish and renewal
was sent. An acquisition acknowledged after validity lapsed is refused as
retryable, and a lost or lapsed lease still awaiting its fence is no
longer shared with another open on the same node. A renewal that lands
after the lease was given up, or after its validity elapsed, cannot revive
it: the lapse check and the update run under the lease lock, and fencing
runs once. The renew loop also fences at the validity deadline itself
rather than at the next check, which could consume the whole margin.
This bounds local validity only; it is not backend-enforced fencing, and a
paused host can still write after its validity ends.
* docs: describe volume lease validity, fencing and their limits
* refactor(gateway): verify SigV4 credentials behind a CredentialVerifier
The authenticator no longer imports the IAM and STS domains. It hands the
signature check to a verifier as a callback, so the secret never leaves the
adapter, and runs the account-active check between Authenticate and Resolve.
A nil verifier fails every request closed.
The adapter lives in runtime/roles/awsgw/credverify. ResolveCallerUser and the
principal-type constants move from domains/sts/awsapi to domains/sts so the
adapter can use them without importing an awsapi package.
* fix(gateway): guard a nil credential verifier and tighten its tests
A typed-nil credverify.Verifier now fails closed instead of panicking.
The session verdict test goes through the real verifier into the lockout boundary.
Test setups build the verifier from the same mock as the gateway services.
* test(gateway): commit the verifier test helper
* test: pin the caller facts the gateway gives a registered service per principal kind
* refactor(gateway): seal the verified caller into one request-context value
* feat(dispatch): give registered services the caller's principal ARN, user ID, user name and session name
Actor gains PrincipalARN, UserID, UserName and SessionName, filled from the
sealed principal, and RoleName(), which keeps the account-checked role ARN
rule in one place. No consumer reads them yet.
* refactor(gateway): read the caller from dispatch.Actor in the RDS, EKS, STS, IAM and admin handlers
callerActor builds the Actor the seam hands services, and the seven readers that
took fields off the sealed principal now take them from it. RDS and EKS take the
role name from Actor.RoleName(). No behaviour change.
* test: characterize the spinifex Query and /admin surfaces through the full gateway pipeline
* refactor: register the spinifex Query service from operator/admin
* docs: point the spinifex registration migration row at the PR
The ELBv2 move to the ingress seam added its inventory to the coverage tool but not to TestRequestConformance, so 36 operations and 45 findings dropped out while the suite passed. A served service with no implemented operations now fails the sweep.
* test: characterize Tagging dispatch, authorization and GetResources responses through the gateway pipeline
* refactor: register Resource Groups Tagging through the ingress seam
* test: characterize ELBv2 through the full gateway pipeline
* refactor: register ELBv2 through the ingress seam
* test: cover the ELBv2 registration, query parser and load balancer limit
* test: characterize RDS through the full gateway pipeline
* docs: point the Tagging and ELBv2 migration rows at the PR
* test: register Tagging and ELBv2 on the integration harness gateway
AWS's EC2 error reference documents it, and DeleteEgressOnlyInternetGateway and ReplaceRoute answer it, so the integration suite reported two false violations.
* refactor: move gateway/spx to operator/admin
* test: pin the admin wire types and method list in golden JSON
* refactor: move the admin wire types and method list to contracts/operator/v1
* refactor: serve EKS OIDC discovery and JWKS from the EKS domain
* refactor: assert the EKS OIDC publisher satisfies the gateway interface
* docs: cite the pull request in the gateway root-move rows
The captures are about 90% of each E2E artifact and nothing reads them unless
a run failed, so a clean single-node or multi-node pass drops them before the
artifact upload.
* feat: accept EnaSupport on RegisterImage and name the refused parameter in its errors
EnaSupport true and false are stored on the image record, returned by DescribeImages when set and carried through CopyImage. Every validation error in ValidateRegisterImageInput keeps its code and now says which parameter and value it refused.
* feat: set EnaSupport on images created from an instance
AWS takes an image's EnaSupport from the instance it was made from, and every instance here reports it, so the source AMI's value is not inherited.
* docs: record RegisterImage EnaSupport support and the refused parameters
* fix: report EnaSupport on DescribeInstances and RunInstances
* refactor: remove the host SQS store systemd unit
The SQS message store will run inside system-owned instances rather than
as a host daemon, so spinifex.target no longer starts it. Bump the target
to version 4 and drop the unit from the embedded set and its invariants.
* refactor: remove host SQS store config provisioning
The SQS store will run in system-owned instances, so admin init and join
no longer generate or distribute a host store token, routes or config,
and the formation join payload drops the store token field.
* refactor: remove host SQS store service command and role
The SQS store will run in system-owned instances, so drop the unused
spx service sqs-store command, its service registration and the host
sqsstore role. The domains/sqs/message JetStream store is unaffected.
* refactor: remove host SQS store provisioning from install scripts
The SQS store will run in system-owned instances, so setup no longer
creates the spinifex-sqsstore user, its directories or the 4223/4249
peer firewall ports, and install, reset and uninstall stop handling the
retired unit, user and data directory.
* docs: drop the host SQS store from firewall and connection tables
Spinifex no longer ships a host SQS store service, so remove its ports
from the host firewall and network connection references and say in
DESIGN.md that no host service, unit, user or firewall rule exists for it.
The targeted suite selector (select-suites) has not run in CI since its shadow-mode step was removed in #556.
The service-interfaces manifest, manifest-check and manifest-lint existed only to feed it, so they go too.
Also drops the now-meaningless e2e:allow-create markers, keeping their rationale as plain comments.
* feat: render X-Forwarded-For, -Proto and -Port headers in ALB HAProxy frontends
Append joins every client X-Forwarded-For line and adds the client address as AWS does, preserve and remove follow the load balancer attribute, and Proto and Port are always overwritten.
* feat: validate the ALB forwarding attributes and re-render config when they change
ModifyLoadBalancerAttributes rejects bad xff_header_processing.mode and xff_client_port.enabled values with ValidationError before storing anything, and regenerates the stored config when either key changes.
* test: assert the ALB X-Forwarded headers and the remove mode in the lb e2e
The app instances echo the request headers on /__headers. The internal ALB is driven from the probe client and the HTTPS ALB from the runner, covering the caller address, a client value, spoofed Proto and Port, mode remove and a rejected mode.
* test: let the lb probe client trigger accept a null header list
* docs: document the ALB forwarding headers and their attributes
* feat: add the store-neutral SQS message store contract and conformance suite
Defines message.Store over opaque shard and queue ID namespaces with no
create call, plus a reusable conformance runner exercised against a
test-only in-memory reference store.
* feat: add the SQS message store's JetStream layout, append and state CAS
Provisions the fixed set of shared message streams and state buckets,
publishes each queue on sqs.m.<shard>.<queue>, dedupes idempotent appends
with a queue-scoped Nats-Msg-Id, and maps state revision CAS onto KV.
* fix: give each SQS queue immutable message and state shard allocations
A changed stream or bucket count applies only to new queues: the namespace
names its message shard and state shard directly, retained streams and
buckets stay addressable, and the store refuses cluster sizes other than
one node (R1) or three (R3).
* fix: enumerate complete SQS namespaces and backstop state buckets
Enumeration now returns each namespace with both allocations, so a
state-only namespace can be counted and cleaned without its queue record,
and state buckets expire records after 14 days plus 1 hour.
* feat: add the SQS JetStream store's reads, counts and cleanup
Get and Delete are scoped to the namespace's subject, DeleteThrough and
DeleteNamespace are subject-filtered purges that leave shard neighbours
intact, and Count reads the per-subject message total and live state keys.
* fix: count distinct SQS state keys and validate before namespace purge
The key lister can repeat a key under concurrent writes, so Count tallies
distinct names; DeleteNamespace resolves both the message stream and the
state bucket before purging either, so a wrong state shard deletes nothing.
* feat: add the SQS JetStream store's ordered message-shard feed
Append records the namespace's state shard in an internal header that is
never returned as metadata. Feed reads one shared message stream through
an ephemeral, headers-only ordered consumer with no acknowledgement state,
rebuilds each change's complete namespace, refuses a malformed header with
ErrCorruptRecord, and deletes its consumer on Close.
* feat: enumerate SQS namespaces and run conformance on the JetStream store
Namespaces joins per-subject body counts, whose internal header names the
state shard, with live state keys from every state bucket that exists,
found by listing so a historical bucket counts and none is created. An
allocation conflict is reported as ErrCorruptRecord. The store now
implements message.Store and runs the conformance suite on R1, per case
with crash reopen and on one shared instance.
* fix: check every SQS body's state shard when enumerating namespaces
Sampling the first and last body let a middle body naming another state
shard pass. Namespaces now reads every body's header in one headers-only
ordered pass over the shard's stream, counting each queue's bodies from
that pass, and deletes its consumer when done.
* test: run SQS store conformance on R3 and guard against consumer state
TestR3Conformance runs the conformance suite on a real three-node
JetStream cluster in process, with Reopen restarting every node over the
same data, and a check that every stream and state bucket is R3.
TestS5_NoConsumerDeliveryStateInSQS fails on any acknowledgement,
redelivery, durable-consumer or ack-retention mechanism in SQS source.
* test: bar legacy NATS acknowledgement helpers from SQS source
Adds AckSync, ManualAck, AckAll and AckExplicit to the ADR-0011:S5
consumer-state guard.
* feat: add a bounded SQS store scan and the tombstoned-body sweeper
Store gains Scan: a paginated, headers-only pass over a message shard,
bounded at the shard's last position when it starts, skipping deleted
positions and never returning a later append. The conformance suite and
fake cover continuation, deleted runs and the bound.
The JetStream adapter reads every bounded range (scan, body enumeration
and state keys) through one reader whose completion rests on a direct get
of the next match, not on consumer state; the client's key lister and
pending counts are no longer trusted to end a pass.
message.Sweep walks a shard with Scan and, for each body whose record the
domain reports terminal, deletes the body then the record by revision.
* test: add an opt-in SQS store conformance run on an external R3 cluster
TestR3ExternalConformance runs the conformance suite against the
dedicated three-node SQS JetStream cluster named by SQS_STORE_R3_URLS,
checks every stream is R3 with two peers, and uses SQS_STORE_R3_RESTART
to restart every node for the crash cases. It skips when unset.
* test: run the SQS sweeper against the reference store fake
Covers live records kept, tombstones swept, another shard untouched, a
no-op rerun, and a retry after the body delete succeeds and the state
delete fails.
* test: harden the SQS store R3 harnesses against restart and placement races
The external R3 harness reused one connection across restarts, so a case's
cleanup closed it for every later case; each case and restart now opens its
own connection and waits for stream leaders.
The in-process cluster released its free ports before the servers bound them,
and R3 placement can fail until the meta leader has every peer's config and
stats. Client ports are now server-chosen, route ports are reserved together,
and provisioning retries the idempotent New until placement succeeds.
* fix: stop the SQS store feed idling for a whole pull expiry
Under load an ordered feed on a three-node cluster sat for nats.go's 30s
default pull expiry, once 60s, with stored changes pending and no pull
waiting on its consumer, both on feeds opened after a full restart and on
feeds kept open across one. Feed now sets a 2s pull expiry.
Adds restart tests for a feed opened after a full-cluster restart and one
open across it, each replaying a shared-shard backlog then a live append and
failing on any 5s gap. The in-process cluster keeps client ports across
restarts so open connections reconnect, and can write node logs.
* fix: read SQS message state from the stream leader
GetState used nats.go's KV Get, which switches to a direct get whenever
the bucket allows it. On a replicated bucket a follower can answer that
from state it has not applied yet, so a record the leader had already
deleted or purged could still be returned.
Read the key's last message with a stream message get instead, which
only the stream leader answers, and apply KV Get's delete and purge
marker rules to the result. Bucket configuration is unchanged. Missing,
deleted and purged records still return ErrNotFound; values, revisions,
context errors and the API prefix or domain of the caller's JetStream
context are preserved.
* docs: narrow the SQS GetState leader-read comment to what it avoids
* fix: enumerate SQS state keys from the stream leader
Count and Namespaces found live state keys with a direct-get probe and a
headers-only ordered consumer on the state bucket's stream. On a
replicated bucket a follower can answer either from state it has not
applied, so keys the leader had already deleted could still be counted.
Walk the key prefix with stream message gets by next subject, which
only the stream leader answers, through the stream's last sequence at
the call. Delete and purge markers drop a key, as GetState reads them.
This costs one leader request per record under the prefix. Bucket
configuration, historical state shards, the message-body scans and the
feed are unchanged.
* test: keep SQS API recorder deliveries in arrival order
The recorder fed API subjects and its sentinel to two async
subscriptions, which nats.go delivers on separate goroutines, so drain
could return before the last request was recorded. Both now share one
channel that the connection fills in arrival order.
* fix: read SQS body enumerations from the stream leader
Namespaces and Scan read bodies through an ephemeral ordered consumer,
which the server may place on a follower that has not applied a purge,
so a deleted queue could still be listed or scanned. Each stored match
through the bound is now a leader stream message get. That transfers
every full body, where the consumer read headers only.
* test: stop the SQS API recorder goroutine with its test
The recorder ranged over a caller-owned ChanSubscribe channel, which
nats.go never closes, so its goroutine outlived every test. Cleanup now
unsubscribes both subscriptions, then signals the goroutine and waits
for it to return; the sentinel send also watches that signal.
* fix: recover a stalled SQS feed from its cursor
An ordered feed was seen to idle with stored changes pending after a
cluster restart. Next now waits at most an idle check on the consumer,
then asks the stream leader whether a change follows the cursor; if one
does, the consumer is deleted and reopened just after the cursor, so no
change is skipped or repeated. The root cause of the stall is unknown.
* fix: make closing an SQS feed terminal and safe against Next
Close now marks the feed closed under a lock, cancels any in-flight
Next, stops the iterator and deletes the consumer. A later or in-flight
Next returns ErrFeedClosed and never opens a consumer; one opened by a
recovery racing Close is deleted rather than adopted. Repeated Close
returns nil.
* fix: define SQS feed close in the store contract
message.Feed now documents one Next reader with Close callable at any
time and more than once, and message.ErrFeedClosed replaces the
adapter's own error. Once Close has begun, Next returns ErrFeedClosed
and withholds any change it had received; a feed from the last cursor
still delivers it. Shared conformance cases and the reference fake
enforce this.
The JetStream feed's Close also waits, within a bound, for a consumer
open already in progress, so a consumer created during Close is deleted
before Close returns nil; past the bound, or if that delete fails, Close
returns an error and a repeated Close waits again.
* fix: track SQS feed consumer cleanup until Close succeeds
Every consumer open and release is now tracked work recorded under the
lock that takes the consumer, and a delete that fails is kept for Close
to retry. Close returns nil only once no such work is in progress and no
failed delete remains, waiting within a bound and otherwise returning an
error; concurrent and repeated Close calls wait the same way.
Next claims a change under that lock and advances its cursor there, so a
change is either returned before Close begins or withheld for a feed
resumed from the last cursor.
A consumer create no longer inherits the caller's cancellation, which
was seen to leave most cancelled creates on the server with no owner;
it has its own timeout, and the name of one that fails is deleted and
kept for Close to delete again.
* fix: release SQS feed consumers opened after the caller's context ends
Feed and recovery now check the caller's context before starting a
consumer open and after it completes. A create that finishes after the
context ended is released as tracked work and the context's error is
returned, or ErrFeedClosed when Close won; no change is returned.
The feed contract and adapter docs now describe cleanup as complete for
consumers the feed knows of, with the server's one-minute inactive
threshold removing one an ambiguous create lands after its last delete.
* fix: report cancellation when an SQS feed open fails after it
A first consumer open that fails after the caller's context ended now
returns the context's error joined with the failure, so cancellation
stays detectable. The feed contract's cancellation text is narrowed to
opening and reopening, whose cleanup is best effort.
* docs: describe the internal SQS message store and its limits
DESIGN.md gains a section on the store-neutral SQS message store, its
JetStream adapter and conformance suite: an internal primitive with no
service wired to it, its shared stream and bucket layout, leader-directed
reads and their full-body cost, feed recovery and its best-effort
consumer cleanup, and the absence of end-to-end or multi-host
validation. The Scan contract says metadata only, not headers only.
* feat: record manifest subject and artifactType and return OCI-Subject on push
* fix: resolve the manifest media type once so a parameterised Content-Type cannot misclassify an index
* feat: index referrers per subject and serve them through the metadata service
* feat: serve the OCI referrers API for ECR repositories
* test: cover the ECR referrers route in the gateway authorization matrices
* feat: delete a subject image's referrers with it
* fix: satisfy nilerr in the referrers manifest parser
* fix: return the manifest parse error and format the referrers code
* docs: describe the ECR OCI referrers API and cosign support
* docs: name oras cp and crane copy as the way to carry v3 signatures to an ECR mirror
* docs: present oras cp and crane copy as one remedy for copied v3 signatures
* feat: add a gateway readiness endpoint to the EKS token webhook
* feat: serve token webhook readiness beyond loopback and admit it to the control-plane SG
* feat: health-check the EKS apiserver target group on the token webhook readiness port
* style: order the strconv import
* fix: export the readiness listener address to the EKS token webhook
* docs: regenerate the ELB coverage README
* docs: describe EKS control plane readiness and what happens when a gateway stops
* test: range over SplitSeq in the webhook initd guard
* feat: add routableTargets helper that skips unhealthy targets and fails open
* fix: drop unhealthy targets from the NLB nginx upstream, failing open when none are healthy
* fix: re-render load balancer config when a target health state changes
* fix: write load balancer config and heartbeat fields under CAS so a re-render is never rolled back
* fix: write live load balancer modifications under CAS so they cannot roll back a re-rendered config
* fix: let ALB backends fail open to unchecked backup servers when every target is down
* test: cover NLB unhealthy-target routing and ALB fail open in the lb e2e suite
* test: flip ALB backend health per instance and log how long each health phase takes
* style: gofmt haproxy backend struct and the attributes fetch table
* fix: re-render load balancer config when a target group's health check changes
* fix: return the health-target marshal error instead of discarding it
* test: mark the health-check target group create as an intentional create-path test
* docs: describe target health, fail open and time to effect in the ELBv2 guide
* feat: render a data volume as an MMIO virtio-blk-device
A package-private helper reuses the PCI volume device's id, drive,
iothread, serial and stop-on-error options, without a PCI bus.
* feat: cold-boot microvm data volumes on MMIO without a PCI hot-plug port
microvm has no PCIe root ports, so its data volumes render as
virtio-blk-device and take no hot-plug port; a recorded port is kept.
NBD and host-block exports and q35 PCI allocation are unchanged.
* feat: compose a launch-only microvm config with EBS data volumes
directBootLaunchConfig copies a clean direct-boot base and appends the
mounted data volumes after its devices, rejecting boot and EFI volumes
and non-microvm bases. Neither input is modified, so repeated launches
never accumulate entries or keep a stale endpoint.
* feat: launch direct-boot microvms with mounted data volumes from a launch-only config
startQEMU composes the direct-boot command from the saved base and one
snapshot of the mounted requests, which also feeds the readiness probes,
so the launched endpoint is the one checked and is never saved.
* fix: refuse volume hot-attach on direct-boot microvms before any mount
A microvm's MMIO bus has no PCIe root port to hot-plug into, so
AttachVolume returns ErrHotAttachUnsupported before mounting the volume
or changing instance or volume state.
* fix: return UnsupportedOperation for microvm volume hot-attach
AttachVolume's ErrHotAttachUnsupported is a caller-fixable refusal, so
the attach/detach error boundary maps it to a 400 UnsupportedOperation
instead of a generic server error.
* docs: describe direct-boot microvm data volumes and refused hot-attach
Enables the rule with functions: false, since hoisted declarations are safe, and defines CONDITION_FIELD_TEXT and the handoff message listener before their first use.
Turns on oxlint's eslint/complexity rule (max 20) and brings the 18 offending functions under the limit by extracting helpers, hooks and subcomponents with no behaviour change.
Shares the RDS subnet and parameter group option builders through rds-form-fields, and rebuilds dist.
* refactor: share the ECR registry address precedence in domains/ecr
* feat: expose every gateway's ECR registry host to the EKS service
* fix: skip wildcard peer hosts when listing ECR registry hosts
* feat: trust every gateway's ECR registry host on EKS workers
* docs: describe how EKS workers trust every gateway's ECR registry host
* docs: split the ECR gateway limits to one sentence per line
* refactor: format the registry URI host in domains/ecr so the daemon avoids the awsapi adapter
* docs: state that a refused gateway fails over at once and record the 3-node ECR measurements
* fix: add GatewayMTU capping the gateway MTU at the internet path MTU
* fix: set gateway_mtu on the VPC gateway router port so oversized packets get ICMP frag-needed
* fix: wire the gateway MTU into vpcd and cover reconcile convergence
* feat: add host MSS clamp tables for the veth and routed uplinks
* fix: shorten MSS clamp comments and log the periodic ensure at debug
* feat: run the host MSS clamp loop in vpcd
* fix: run nft under the ambient CAP_NET_ADMIN instead of sudo
* docs: describe the gateway MTU and the host MSS clamp for the internet path
Bound the zone reload wait with a test context, generate 2048-bit keys in
cert renew tests via the existing seam, replace 100ms negative waits with
deterministic checks, and run tests bound by production timeouts in parallel.
Ubuntu 26.04's openvswitch-switch declares Alias=openvswitch.service, so
enabling it leaves an /etc symlink that distro_ovs_unit read as a second,
EL-style umbrella unit. setup-ovn.sh then refused to pick one and every
ubuntu-26.04 bootstrap failed. A symlink back to openvswitch-switch.service
now counts as the Debian unit; a real or masked openvswitch.service still
does not.
ResourcesPerPage outside 1-100 and a malformed PaginationToken now return
InvalidParameterException. An unbounded page size let start+size overflow
negative and panic on the slice.
The test checked IPsec config and that AES-GCM SAs exist, and only warned when
a 5s idle capture saw no ESP, so a cluster sending plaintext Geneve passed.
It now launches an instance pair on distinct nodes, pings 200 x 1000 bytes
between them, and for each direction fails unless the outbound SA counters to
the peer's ovn-encap-ip rose by the payload, an outbound-only capture on the
interface routing to the peer saw ESP or UDP/4500 ESP, and saw no UDP/6081.
The distinct-node launch loop moves from the baseline cross-host test into a
shared helper. The test is no longer parallel since it now launches instances.
Vendor the route-53 2013-04-01 Smithy model at the pinned commit, add the
Route53 service, and pass route53awsapi.OperationInventory() to the
generator. coverage.json and docs/coverage/route53 now list the 13 served
operations; the Spinifex activation extensions stay out of the model count.
The generator now fails when a served service has no embedded model, so
the next one cannot drop out of the report silently. Request encoding
skips rest-xml by protocol, which covers Route 53 as well as S3.
ensure_clustered_db_storage() collected only databases in standalone
format, so --recreate-db returned early when the NB/SB databases were
already clustered and removed nothing. ovn-ctl then served the stale
database and silently ignored the RAFT flags, which is the failure the
function exists to prevent, reached from the other direction. Because
the old database carries the previous member list and server ID, the
node rejoined the earlier cluster instead of replacing it: re-forming a
production cluster brought the create node back as a follower, and
ovn-nbctl over the leader-only unix socket failed with "database
connection failed ()".
install-node.sh passes --recreate-db on every database node on every
run, so formation already intended a purge. Remove whatever NB/SB
state is present, standalone or clustered, and keep refusing a
standalone database when the flag is absent.
Two further gaps in the same path. systemctl stop returns once the unit
is inactive rather than once the processes are gone, so the purge now
waits for the database servers and escalates to SIGKILL, failing the
run if one survives rather than unlinking files under it. The pattern
is anchored on the OVN database directory so it can never reap the OVS
conf.db server. The lock files, the compute-node move-asides and an
earlier run's standalone backup are cleared alongside the databases.
* feat: add a dedicated SQS message store JetStream server separate from control-plane NATS
* test: cover the SQS store server start, validation and memory limit
HAProxy's short 'option httpchk GET <path>' form sends HTTP/1.0 with no Host header, which Envoy rejects with 426 (and 400 for HTTP/1.1 without Host), so Envoy-fronted targets never became healthy. Match the AWS request shape: GET <path> HTTP/1.1, Host set to the target IP (with the port only when it is not the scheme default), and User-Agent ELB-HealthChecker/2.0.
MemoryObjectStore.DeleteObject silently succeeded for an absent key while
Predastore answers NoSuchKey, so handlers that treated a missing key as fatal
passed unit tests and failed in production. The fake now returns
NoSuchKeyError, and delete paths that must be idempotent (DeleteSnapshot's
sweep and document delete, DeleteVolume, DeregisterImage, the admin image
removal sweep, and ECR upload and GC cleanup) treat it as already deleted.
* feat: let a registration select REST-JSON or JSON 1.0 query-compatible error envelopes
* feat: add a per-registration internal error code and decode envelopes through the AWS SDK v2
* feat: let a JSON 1.0 registration namespace the __type of its errors
* feat: match captured SQS error bytes on the JSON 1.0 envelope
* refactor: move gateway request audit into ingress/aws/audit
First generic-ingress extraction from the root gateway under ADR-0001.
requestaudit.go and slogRequestLogger move unchanged in behaviour to
package audit. The gateway's auth flow and dispatch populate the record
through the exported From and Record setters at the same call sites.
Record.Fields is exported for the SigV4-path tests that stay in gateway.
The migration-record deferral bullet is replaced by a move row.
* docs: cite #1285 in the request-audit move row
UpdateKVRecordTags now goes through kvutil.Update, which commits at the
revision it read and retries on conflict, instead of a plain put that
reverted any tag or non-tag write landing between its read and write.
Volume and AMI tag mirrors write ebsmetadata documents to the object
store, which has no conditional put; characterisation tests pin that
they still lose interleaved writes.
aws iam get-user sends no UserName and got MissingParameter. The gateway now
applies the AWS default before authorization: an IAM user is treated as naming
themselves, root gets the account root shape, and a role session gets
ValidationError.
Refs mulgadc/spinifex#1275
* refactor: move accountteardown to operator/accountlifecycle
Mechanical relocation under the amended ADR-0001 accountteardown row.
Package renamed to accountlifecycle; importers keep the accountteardown
alias. No API, NATS subject, KV access or behaviour change. Replacing
direct domain/adapter cleanup calls with owner capabilities stays
recorded as deferred debt.
* docs: cite #1277 in the accountteardown move row
* refactor: relocate gateway/{ecs,elbv2} to domains/{ecs,elbv2}/awsapi
Mechanical ADR-0001 relocation of the ECS and ELBv2 AWS action adapters
to their domain homes, as #1265 did for IAM, STS, EKS and RDS. APIs, NATS
subjects, KV buckets and behaviour are unchanged. The root gateway
dispatch still imports both packages, recorded as gateway (service
registration) debt; the quota live ELBv2 counter import moves with it.
* docs: cite spinifex#1273 in the migration record
The IAM, STS, RDS, EKS and ELBv2 relocations moved their packages out of
spinifex/handlers, which the S4, S6 and S7 network invariant scans still
walked, so those packages silently dropped out of the scans. Re-add their
new homes alongside domains/ecs. The checked rules are unchanged.
* refactor: relocate handlers/ecs to domains/ecs
Mechanical move per the ADR-0001 handlers/ecs row (files already
organised by resource). Package renamed handlers_ecs -> ecs; importers
alias ecsdomain. APIs, NATS subjects, storage and behaviour unchanged.
handlers is now a retired legacy root; ECS allowlist debt retargeted to
domains/ecs and R5 holds it to the interim daemon-import ban.
* docs: cite spinifex#1271 in the ECS relocation row
* fix: point network invariant scans at domains/ecs
The ADR-0007 S4, S6 and S7 scans walked spinifex/handlers, which no
longer exists after the ECS move. Scan domains/ecs, the same code.
Mechanical move permitted by the ADR-0001 lbagent amendment. The package
becomes elbv2 under agents/; importers alias it lbagent. APIs, on-guest
paths, NATS subjects and behaviour are unchanged. Q-30's data-plane
boundary is recorded as deferred debt in the migration record.
Also brings docs/DESIGN.md up to date with the current code: gateway
dispatch and registrations, EC2 handler and NATS request signatures,
RunInstances placement, daemon subscription form, volume mount subject,
DescribeInstances scatter-gather, and the package layout.
Mechanical whole-package move under the amended ADR-0001 daemon row. Package name, APIs, NATS subjects, storage and behaviour are unchanged; only import paths and path comments outside the package change.
The interim daemon -> gateway -> handlers layering guardrail, the ADR-0007 network invariant scans and the instance-state ratchet locate the daemon by directory, so they now match runtime/daemon. Q-28's ownership and deployment refactor is recorded as deferred debt.
* refactor: relocate gateway/bedrock into domains/ochre
Mechanical ADR-0001 whole-package relocation. The Ochre domain core,
stores, inference engine and Bedrock action layer join package ochre
with their package-private code and tests intact; no identifier, API,
NATS subject, KV bucket, metric, error string or behaviour changes.
Separating the action adapters into domains/ochre/awsapi is deferred
and recorded as debt.
* docs: cite spinifex#1266 in the migration record
* refactor: move IAM, STS, EKS and RDS AWS adapters to domains/<svc>/awsapi
Mechanical ADR-0001 relocation of gateway/{iam,sts,eks,rds} to
domains/{iam,sts,eks,rds}/awsapi. APIs, NATS subjects, storage and
behaviour are unchanged. The root gateway dispatch still imports each
package and is recorded as 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.
* docs: cite spinifex#1265 in the migration record
* refactor(eks): move handlers/eks to domains/eks/cluster
Mechanical ADR-0001 relocation of the remaining EKS implementation,
nodegroups included, alongside the access and addon packages. APIs, NATS
subjects, KV buckets and behaviour are unchanged; only the package name
changes from handlers_eks to cluster (imported as ekscluster). The
frozen-legacy entry is removed, allowlist entries now name
domains/eks/cluster, and the stale operator/cli entry is removed.
Nodegroup service extraction is recorded as deferred.
* docs: name the EKS cluster relocation PR in the migration record
* feat(vm): attach host block device exports via raw over host_device
EBSRequest gains optional BlockDevice and BlockSizeBytes. A single
classifier decides whether a request is an NBD export, a block export or
invalid, and buildDrives, AttachVolume, launch readiness, the QMP greeting
deadline and restore all use it. A block data volume is attached as a
sized raw node over a host_device file node with the same node name,
iothread and virtio-blk device as an NBD volume; boot and EFI volumes
reject block exports. NBD output is unchanged.
* feat(ebsprovider): replace PublishedVolume.NBDURI with a typed export descriptor
PublishedVolume now carries an Export of kind nbd or block, checked by VolumeExport.Validate.
The NATS client rejects an invalid export in a PublishVolume reply, and the server refuses to relay one, reporting it as internal.
SchemaVersion moves to 2.
viperblockd, qemunbdd and the memory provider publish nbd exports exactly as before.
Image import refuses a non-nbd export and still unpublishes.
Conformance requires a valid export, and the NBD-dialling suites fail explicitly on a non-nbd export.
* fix(ebsprovider): preserve v1 export compatibility
---------
Co-authored-by: joshsiv-mulga <mrk@mulgadc.com>
* refactor(rds): move handlers/rds to domains/rds/instance
Mechanical ADR-0001 relocation of the remaining RDS instance resource to
its resource package, alongside engine, parametergroup and subnetgroup.
APIs, NATS subjects, KV buckets and behaviour are unchanged; only the
package name changes from handlers_rds to instance (imported as
rdsinstance). The frozen-legacy entry is removed and the allowlist
entries now name domains/rds/instance; the recorded debt remains.
* docs: name the RDS instance relocation PR in the migration record
* fix: commit launch template version deletes through the header revision so a concurrent default change cannot name a deleted version
* fix: keep version-delete rollbacks from leaving headerless bodies or a false default-version refusal
* fix: resolve a GetSessionToken session as its IAM user in presigned GetCallerIdentity, and reject an identity with no ARN
* fix: report the account ID as root's UserId for a root session in presigned GetCallerIdentity
* refactor(sts): move handlers/sts to domains/sts
Mechanical ADR-0001 relocation of the STS implementation. APIs, NATS
subjects, KV buckets and behaviour are unchanged; only the package name
changes from handlers_sts to sts (imported as stsdomain). The frozen-legacy
entry is removed and the allowlist entries now name domains/sts; the
per-consumer IAM capability debt remains.
* docs: name the STS relocation PR in the migration record
Bumps the docker-images group with 1 update in the /build directory: golang.
Bumps the docker-images group with 1 update in the /docs/terraform-workbooks/demo-app directory: distroless/static.
Bumps the docker-images group with 1 update in the /scripts directory: debian.
Updates `golang` from `5c3e9b4` to `972b411`
Updates `distroless/static` from `e2e927e` to `18f4cc2`
Updates `debian` from `a29215f` to `62bd423`
---
updated-dependencies:
- dependency-name: golang
dependency-version: 1.27.3-bookworm
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: docker-images
- dependency-name: distroless/static
dependency-version: nonroot
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: debian
dependency-version: trixie-slim
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* refactor(iam): move handlers/iam to domains/iam
Relocate IAM's implementation and its test double to the ADR-0001 home
with git mv. The package is renamed from handlers_iam to iam and its
importers use the iamdomain alias. APIs, NATS subjects, KV buckets and
behaviour are unchanged.
The cross-domain allowlist entries now target domains/iam; the two
frozen-legacy entries and the operator/cli entry, which no longer breaks
the legacy-implementation rule, are removed. The migration record states
that per-consumer IAM capabilities remain debt but were not a
prerequisite to the move.
* docs: record the IAM move against its PR
Store the override record with a state field kept separate from the admin
wire type. Every set and clear is revision-guarded and refuses a sealed
record with Blocked, and every read of a sealed record, including
admission, returns Blocked. The read-failure fallback to configured limits
is unchanged.
Add Service.SealAccountOverrides, accepted only when IAM already holds the
account as TERMINATING. It creates the override bucket if needed after that
check, creates or updates the record as sealed, and succeeds only on a
leader-accepted write, returning its revision. awsgw serves it on
admission.v1.SealAccountQuotaOverride. Nothing calls it yet.
Replace the unwired delete-based CleanupAccount.
Joining a coded error with RetryAfter rendered the bare code on a second
line, so logged and stored reasons read "...: ServiceUnavailable
ServiceUnavailable". Add WithRetryAfter, which wraps the error and keeps
its text while ResolveRetryAfter and ResolveErrorDetail still resolve the
hint and the code. Use it for the seal-capability refusal and the
incomplete DescribeInstances and DescribeInstanceTypes fan-out errors.
Wire the EC2 account-settings seal into account termination as a
StageAccount reaper. Immediately before every seal attempt the reaper
probes each configured node's new read-only, per-node seal-capability
subject and requires exactly one identified reply per node at capability
version 1 or later. Missing, duplicate, wrong-identity, malformed, error,
older-version or unreachable responders send no seal and return a
retryable ServiceUnavailable, so the account stays TERMINATING until the
cluster is compatible.
The reaper completes only on a sealed reply for the account with a
non-zero revision; a fresh termination run seals again. Daemons validate
their node ID before subscribing, so an invalid ID serves no capability
and reads as incompatible.
Replace the unwired delete-based CleanupAccount with an owner-local sealed
state in the existing account-settings KV value. All settings mutations now
use revision-guarded create/update; a sealed record rejects every read and
mutation with Blocked and cannot be recreated. Records without a state
field remain active.
SealAccount CAS-writes the sealed value whether the key is present, absent
or tombstoned, and succeeds only on a leader-accepted write, returning its
revision. It first checks, through a read-only IAM account-status reader,
that the account is already TERMINATING; otherwise it refuses with
IncorrectState and writes nothing.
The daemon serves the internal subject ec2.v1.SealAccountSettings. Nothing
calls it yet.
Pin that account teardown removes the usage counter but leaves the
per-account quota override, so a reused account ID inherits raised or
Unlimited limits. Add an unwired, idempotent CleanupAccount on the quota
service: a present override key is removed by a revision-guarded delete
and reported deleted with its revision; a missing key is reported only as
observed-absent-at-scan.
Characterise that EC2 account settings survive today's account teardown,
and add an owner-side CleanupAccount in domains/ec2/account that deletes
one account's settings key.
A present key is removed with a revision-guarded delete, which the stream
leader orders regardless of which replica served the preceding read, and
is reported deleted with that revision. A key that a direct get does not
find is reported only as observed absent at scan time, because KV direct
gets can be answered by a lagging follower. Store errors and exhausted
revision conflicts are failed, never absent. The capability refuses empty,
malformed and global account IDs and never falls back to the global key.
Nothing calls it yet: no NATS subject, no accountteardown wiring.
2026-10-23 11:03:38 +10:00
4008 changed files with 299166 additions and 126818 deletions
<textx="24"y="30"font-family="'Segoe UI', system-ui, sans-serif"font-size="11"fill="#a1a1aa"letter-spacing="2.5"font-weight="600">NGINX + ALB · VPC WITH PUBLIC AND PRIVATE SUBNETS</text>
<textx="772"y="394"font-family="'Segoe UI', sans-serif"font-size="9.5"fill="#d8b4fe"text-anchor="end">placement group · strategy spread · one server per node</text>
<textx="24"y="30"font-family="'Segoe UI', system-ui, sans-serif"font-size="11"fill="#a1a1aa"letter-spacing="2.5"font-weight="600">SPINIFEX ON VULTR · ONE NODE OR THREE</text>
<textx="876"y="30"font-family="'SF Mono', ui-monospace, monospace"font-size="10"fill="#71717a"text-anchor="end">--nodes 1 or 3 · vx1-g-16c-64g or bare metal</text>
<textx="450.0"y="94"font-family="'Segoe UI', system-ui, sans-serif"font-size="10"fill="#7ee787"text-anchor="middle">customers reach your EC2 and load balancers</text>
<textx="450"y="348"font-family="'Segoe UI', system-ui, sans-serif"font-size="10"fill="#79c0ff"text-anchor="middle">nodes cluster over the private network: control plane, EBS replicas and S3 shards</text>
<textx="80"y="394"font-family="'SF Mono', ui-monospace, monospace"font-size="12"fill="#c084fc"font-weight="700">AWS services across the fleet</text>
<textx="820"y="394"font-family="'Segoe UI', system-ui, sans-serif"font-size="9.5"fill="#d8b4fe"text-anchor="end">spread across the nodes; with three, moved to a survivor if one fails</text>
<textx="450"y="500"font-family="'Segoe UI', system-ui, sans-serif"font-size="10.5"fill="#a1a1aa"text-anchor="middle">Use the AWS CLI, SDKs and Terraform against your own endpoint, unmodified.</text>
<textx="450"y="520"font-family="'Segoe UI', system-ui, sans-serif"font-size="9.5"fill="#71717a"text-anchor="middle">Public addresses are Vultr reserved IPs, attached to the node each resource runs on.</text>
<!-- One line, so a reviewer knows whether the design records in the private mulga repository need to change. Write either "Updated: <mulga PR or bead id>" or "Not needed: <reason>". This is a reminder, not a gate. -->
## Community Contribution License
- [ ] All community contributions in this pull request are licensed to Mulga under the terms of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). By creating this pull request I represent that I have the right to license these contributions to Mulga under the Apache License 2.0.
echo"::error::hypervisor $host has $((root_kb /1024)) MiB free on the filesystem holding /etc/libvirt/qemu (need ${HV_MIN_ROOT_FREE_MB:-1024} MiB): libvirt cannot write a domain definition there. Free space on $host before re-running." >&2
echo"::error::hypervisor $host has $((pool_kb /1024/1024)) GiB free in storage pool $pool at $pool_path (need ${HV_MIN_POOL_FREE_GB:-20} GiB): the node's disk cannot grow. Free space on $host before re-running." >&2
description:"Comma-separated cell numbers to run, no spaces (blank = all). 1-3, 8-10, 17-19 tofu permutation matrix, 20 baremetal, 21-22 ISO, 23 predastore throughput, 24 predastore fault injection, 25-27 service suites (eks, ecs, rds), 28 guest disk performance, 29 storage fault injection, 30 routed NAT on three nodes, 31 instance recovery on three nodes, 32 load balancer recovery on three nodes, 33 KV bucket quorum under node loss on three nodes"
description:"Comma-separated cell numbers to run, no spaces (blank = all). 1-3, 8-10, 17-19 tofu permutation matrix, 34-36 Oracle Linux single node (basic, reboot, routed NAT), 37-38 Oracle Linux on three nodes (multi-node subset with IPsec, routed NAT), 20 baremetal, 21-22 ISO, 23 predastore throughput, 24 predastore fault injection, 25-27 service suites (eks, ecs, rds), 28 guest disk performance, 29 storage fault injection, 30 routed NAT on three nodes, 31 instance recovery on three nodes, 32 load balancer recovery on three nodes, 33 KV bucket quorum under node loss on three nodes"
@@ -54,7 +54,7 @@ For Terraform, start from the [example workbooks](https://docs.mulgadc.com/docs/
## Services
The platform serves **392 AWS API operations** across the services below. See exactly which ones in the [coverage report](https://docs.mulgadc.com/coverage).
The platform serves **405 AWS API operations** across the services below. See exactly which ones in the [coverage report](https://docs.mulgadc.com/coverage).
| Service | What it is | Built on | Status |
| --- | --- | --- | --- |
@@ -68,8 +68,10 @@ The platform serves **392 AWS API operations** across the services below. See ex
| **ECR** | Container registry | Predastore, deduplicated per account | Available |
| **ECS** | Container service | Spinifex instances | Available |
| **RDS** | Databases | PostgreSQL and MariaDB, with automated backups | Available |
| **Route 53** | DNS | [Northstar](https://github.com/mulgadc/northstar): authoritative DNS with hosted zones | Available |
| **Bedrock** | AI deployment | vLLM for text generation and Hugging Face TEI for embeddings and reranking | Q4 2026 |
| **Lambda** | Serverless functions | | Q1 2027 |
| **SQS** | Message queues | | Q1 2027 |
Roadmap services ship under the same AWS API surface, so code written for AWS today works the moment they land. [Track what's shipped in the release notes](https://github.com/mulgadc/spinifex/releases).
<img src=".github/assets/spinifex-ui.jpg" alt="Spinifex web console dashboard" width="900">
<img src=".github/assets/dashboard.webp" alt="Spinifex web console dashboard" width="900">
</p>
Spinifex ships with a built-in web console for your instances, volumes, buckets, networks, and IAM. See [Launching the Web UI](https://docs.mulgadc.com/docs/setting-up-your-cluster#7-launching-the-web-ui).
{From:"agents/ecs/agent",To:"handlers/ecs",Debt:"agents/ecs/agent",Reason:"assignment/state wire types from the ECS implementation; needs a guest/controller contract"},
{From:"agents/ecs/agent",To:"domains/ecs",Debt:"agents/ecs/agent",Reason:"assignment/state wire types from the ECS implementation; needs a guest/controller contract"},
{From:"agents/eks/tokenwebhook",To:"handlers/eks",Debt:"agents/eks/tokenwebhook",Reason:"WebhookTokenReviewResult from the EKS implementation; needs an EKS contract or projection"},
{From:"agents/eks/tokenwebhook",To:"domains/eks/cluster",Debt:"agents/eks/tokenwebhook",Reason:"WebhookTokenReviewResult from the EKS implementation; needs an EKS contract or projection"},
{From:"agents/rds/agent",To:"handlers/rds",Debt:"agents/rds/agent (dependency, narrowed)",Reason:"command protocol, wire and record types; needs a guest/controller wire contract"},
{From:"agents/rds/agent",To:"domains/rds/instance",Debt:"agents/rds/agent (dependency, narrowed)",Reason:"command protocol, wire and record types; needs a guest/controller wire contract"},
{From:"cmd/aws-model-coverage",To:"domains/acm/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared ECR/ACM inventories; named exception, must not construct a registration"},
{From:"cmd/aws-model-coverage",To:"domains/ecr/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared ECR/ACM inventories; named exception, must not construct a registration"},
{From:"cmd/aws-model-coverage",To:"domains/acm/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared service 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 service inventories; named exception, must not construct a registration"},
{From:"cmd/aws-model-coverage",To:"domains/route53/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared service inventories; named exception, must not construct a registration"},
{From:"cmd/aws-model-coverage",To:"domains/elbv2/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared service inventories; named exception, must not construct a registration"},
{From:"cmd/aws-model-coverage",To:"domains/rds/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared service inventories; named exception, must not construct a registration"},
{From:"cmd/aws-model-coverage",To:"domains/ec2/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared service inventories; named exception, must not construct a registration"},
{From:"cmd/aws-model-coverage",To:"domains/ecs/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared service inventories; named exception, must not construct a registration"},
{From:"cmd/aws-model-coverage",To:"domains/eks/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared service inventories; named exception, must not construct a registration"},
{From:"cmd/aws-model-coverage",To:"domains/iam/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared service inventories; named exception, must not construct a registration"},
{From:"cmd/aws-model-coverage",To:"domains/sqs/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared service inventories; named exception, must not construct a registration"},
{From:"cmd/aws-model-coverage",To:"domains/sts/awsapi",Debt:"cmd/aws-model-coverage",Reason:"reads declared service inventories; named exception, must not construct a registration"},
{From:"daemon",To:"domains/ec2/awsapi/instance",Debt:"Instance adapter consumers outside EC2",Reason:"daemon/eks_cp_control.go and rds_deps.go call the instance adapter; needs an EC2 capability"},
{From:"runtime/daemon",To:"domains/ec2/awsapi/instance",Debt:"Instance adapter consumers outside EC2",Reason:"runtime/daemon/eks_cp_control.go and rds_deps.go call the instance adapter; needs an EC2 capability"},
{From:"domains/acm",To:"handlers/iam",Debt:"ACM: the IAM AES-GCM helper import (ACM relocation residual)",Reason:"IAM AES-GCM helpers; needs a shared crypto boundary"},
{From:"domains/acm",To:"domains/iam",Debt:"ACM: the IAM AES-GCM helper import (ACM relocation residual)",Reason:"IAM AES-GCM helpers; needs a shared crypto boundary"},
{From:"domains/admission/quota",To:"domains/ec2/awsapi/eip",Debt:"domains/admission/quota (dependency, moved from handlers/quota unchanged)",Reason:"describe adapter used as a counter; needs a reader capability"},
{From:"domains/admission/quota",To:"domains/ec2/awsapi/instance",Debt:"domains/admission/quota (dependency, moved from handlers/quota unchanged)",Reason:"vcpu.go counts instances through the instance adapter; needs an EC2 reader capability"},
{From:"domains/admission/quota",To:"domains/ec2/awsapi/volume",Debt:"domains/admission/quota (dependency, moved from handlers/quota unchanged)",Reason:"describe adapter used as a counter; needs a reader capability"},
{From:"domains/admission/quota",To:"domains/ec2/awsapi/vpc",Debt:"domains/admission/quota (dependency, moved from handlers/quota unchanged)",Reason:"describe adapter used as a counter; needs a reader capability"},
{From:"domains/admission/quota",To:"domains/ec2/instancetypes",Debt:"domains/admission/quota → domains/ec2/instancetypes",Reason:"DefaultVCPUs sizing; needs a consumer-owned capability or EC2 projection"},
{From:"domains/admission/quota",To:"gateway/elbv2",Debt:"domains/admission/quota (dependency, moved from handlers/quota unchanged)",Reason:"live ELBv2 counter; needs a reader capability"},
{From:"domains/admission/quota",To:"handlers/rds",Debt:"domains/admission/quota (dependency, moved from handlers/quota unchanged)",Reason:"live RDS counter; needs a reader capability"},
{From:"domains/admission/quota",To:"domains/elbv2/awsapi",Debt:"domains/admission/quota (dependency, moved from handlers/quota unchanged)",Reason:"live ELBv2 counter; needs a reader capability"},
{From:"domains/admission/quota",To:"domains/rds/instance",Debt:"domains/admission/quota (dependency, moved from handlers/quota unchanged)",Reason:"live RDS counter; needs a reader capability"},
{From:"domains/admission/quota",To:"runtime/compute/vm",Debt:"domains/admission/quota (dependency, moved from handlers/quota unchanged)",Reason:"instance-record vCPU sweep; needs a reader capability"},
{From:"domains/dns",To:"domains/network/reconcile",Debt:"domains/dns",Reason:"takes the reconcile lock from network's implementation"},
{From:"domains/dns",To:"runtime/compute/vm",Debt:"domains/dns",Reason:"domain-to-runtime import of VM state"},
{From:"domains/ec2/awsapi/instance",To:"handlers/iam",Debt:"domains/ec2/awsapi/instance (dependency)",Reason:"IAM edge needs an IAM capability"},
{From:"domains/ec2/awsapi/instance",To:"domains/iam",Debt:"domains/ec2/awsapi/instance (dependency)",Reason:"IAM edge needs an IAM capability"},
{From:"domains/ec2/awsapi/instance",To:"runtime/compute/cache",Debt:"domains/ec2/awsapi/instance (dependency)",Reason:"node liveness and VM state; needs an EC2-owned projection"},
{From:"domains/ec2/awsapi/instance",To:"runtime/compute/vm",Debt:"domains/ec2/awsapi/instance (dependency)",Reason:"node liveness and VM state; needs an EC2-owned projection"},
{From:"domains/ec2/awsapi/spotinstance",To:"domains/admission/quota",Debt:"domains/ec2/awsapi/spotinstance (dependency)",Reason:"RequestSpotInstances.go uses IAMService and quota.Service"},
{From:"domains/ec2/awsapi/spotinstance",To:"handlers/iam",Debt:"domains/ec2/awsapi/spotinstance (dependency)",Reason:"RequestSpotInstances.go uses IAMService and quota.Service"},
{From:"domains/ec2/eip",To:"domains/network/topology",Debt:"EC2 → network implementation",Reason:"EC2 reads network implementation; clears with the EC2-owned projection"},
{From:"domains/ec2/guestmetadata",To:"domains/dns",Debt:"Guest metadata: the cross-domain and runtime imports above",Reason:"guest-metadata relocation residual; needs capabilities, projections or contracts"},
{From:"domains/ec2/guestmetadata",To:"handlers/iam",Debt:"Guest metadata: the cross-domain and runtime imports above",Reason:"guest-metadata relocation residual; needs capabilities, projections or contracts"},
{From:"domains/ec2/guestmetadata",To:"handlers/sts",Debt:"Guest metadata: the cross-domain and runtime imports above",Reason:"guest-metadata relocation residual; needs capabilities, projections or contracts"},
{From:"domains/ec2/guestmetadata",To:"domains/iam",Debt:"Guest metadata: the cross-domain and runtime imports above",Reason:"guest-metadata relocation residual; needs capabilities, projections or contracts"},
{From:"domains/ec2/guestmetadata",To:"domains/sts",Debt:"Guest metadata: the cross-domain and runtime imports above",Reason:"guest-metadata relocation residual; needs capabilities, projections or contracts"},
{From:"domains/ec2/guestmetadata",To:"runtime/compute/vm",Debt:"Guest metadata: the cross-domain and runtime imports above",Reason:"guest-metadata relocation residual; needs capabilities, projections or contracts"},
{From:"domains/ec2/instance",To:"domains/dns",Debt:"domains/ec2/instance",Reason:"cross-domain implementation import of DNS"},
{From:"domains/ec2/instance",To:"domains/network/topology",Debt:"EC2 → network implementation",Reason:"EC2 reads network implementation; clears with the EC2-owned projection"},
{From:"domains/ec2/instance",To:"runtime/compute/gpu",Debt:"domains/ec2/instance",Reason:"VM/GPU runtime state read directly; needs a domain-owned projection"},
{From:"domains/ec2/instance",To:"runtime/compute/vm",Debt:"domains/ec2/instance",Reason:"VM/GPU runtime state read directly; needs a domain-owned projection"},
{From:"domains/ec2/instance/state",To:"daemon",Debt:"domains/ec2/instance/state (dependency, IMDS readers)",Reason:"wraps the daemon's local-file reader and JetStreamManager bucket open unchanged for vpcd's IMDS lookup"},
{From:"domains/ec2/instance/state",To:"runtime/daemon",Debt:"domains/ec2/instance/state (dependency, IMDS readers)",Reason:"wraps the daemon's local-file reader and JetStreamManager bucket open unchanged for vpcd's IMDS lookup"},
{From:"domains/ec2/instance/state",To:"runtime/compute/vm",Debt:"domains/ec2/instance/state (dependency)",Reason:"the vpcd IMDS local reader returns vm.VM"},
{From:"domains/ec2/volume",To:"runtime/compute/vm",Debt:"Domain → runtime",Reason:"VM runtime state read directly; needs a domain-owned projection"},
@@ -76,7 +82,7 @@ var allowlist = []allowance{
{From:"domains/ec2/vpc",To:"domains/network/identifiers",Debt:"EC2 → network implementation",Reason:"EC2 reads network implementation; clears with the EC2-owned projection"},
{From:"domains/ec2/vpc",To:"domains/network/topology",Debt:"EC2 → network implementation",Reason:"EC2 reads network implementation; clears with the EC2-owned projection"},
{From:"domains/ecr/auth",To:"handlers/iam",Debt:"Cross-domain",Reason:"IAM implementation import; needs an IAM capability"},
{From:"domains/ecr/auth",To:"domains/iam",Debt:"Cross-domain",Reason:"IAM implementation import; needs an IAM capability"},
{From:"domains/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:"runtime/compute/gpu",Debt:"Domain → runtime",Reason:"GPU runtime state read directly; needs a domain-owned projection"},
{From:"domains/ochre/vector",To:"handlers/iam",Debt:"Cross-domain",Reason:"consumes another domain's implementation rather than a capability"},
{From:"domains/ochre/vector",To:"domains/iam",Debt:"Cross-domain",Reason:"consumes another domain's implementation rather than a capability"},
{From:"gateway",To:"domains/ec2/awsapi",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/account",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/capacityreservation",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/eigw",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/eip",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/idem",Debt:"gateway/ec2.go (registration; ec2_idempotency.go)",Reason:"dispatch-time idempotency wrapper; moves with the EC2 dispatch map"},
{From:"gateway",To:"domains/ec2/awsapi/igw",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/image",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/instance",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/key",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/launchtemplate",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/natgw",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/placementgroup",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/routetable",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/snapshot",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/spotinstance",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/tags",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{From:"gateway",To:"domains/ec2/awsapi/volume",Debt:"gateway/ec2.go (registration)",Reason:"EC2 dispatch map stays in gateway until the ingress registration seam"},
{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:"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/bedrock",To:"handlers/iam",Debt:"IAM implementation consumers in STS, Ochre and ELBv2",Reason:"secret encrypt/decrypt helpers; needs a shared crypto boundary"},
{From:"domains/sts/awsapi",To:"domains/iam",Debt:"IAM implementation consumers in STS, Ochre and ELBv2",Reason:"GetCallerIdentity takes IAMService; needs an IAM capability"},
{From:"gateway/rds",To:"domains/ec2/awsapi/instance",Debt:"Instance adapter consumers outside EC2",Reason:"enginecatalog.go calls the instance adapter; needs an EC2 capability"},
{From:"domains/ecs",To:"domains/ec2/ebs/metadata",Debt:"close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)",Reason:"legacy ECS calls another domain's implementation as an internal API"},
{From:"domains/ecs",To:"domains/ec2/eip",Debt:"close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)",Reason:"legacy ECS calls another domain's implementation as an internal API"},
{From:"domains/ecs",To:"domains/ec2/instancetypes",Debt:"close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)",Reason:"legacy ECS calls another domain's implementation as an internal API"},
{From:"domains/ecs",To:"domains/elbv2",Debt:"close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)",Reason:"legacy ECS calls another domain's implementation as an internal API"},
{From:"domains/ecs",To:"domains/iam",Debt:"close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)",Reason:"legacy ECS calls another domain's implementation as an internal API"},
{From:"gateway/sts",To:"handlers/iam",Debt:"IAM implementation consumers in STS, Ochre and ELBv2",Reason:"GetCallerIdentity takes IAMService; needs an IAM capability"},
{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/ecs",To:"domains/ec2/ebs/metadata",Debt:"close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)",Reason:"legacy ECS calls another domain's implementation as an internal API"},
{From:"handlers/ecs",To:"domains/ec2/eip",Debt:"close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)",Reason:"legacy ECS calls another domain's implementation as an internal API"},
{From:"handlers/ecs",To:"domains/ec2/instancetypes",Debt:"close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)",Reason:"legacy ECS calls another domain's implementation as an internal API"},
{From:"handlers/ecs",To:"handlers/elbv2",Debt:"close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)",Reason:"legacy ECS calls another domain's implementation as an internal API"},
{From:"handlers/ecs",To:"handlers/iam",Debt:"close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)",Reason:"legacy ECS calls another domain's implementation as an internal API"},
{From:"domains/elbv2",To:"domains/acm",Debt:"domains/elbv2 (cross-domain)",Reason:"ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From:"domains/elbv2",To:"domains/dns",Debt:"domains/elbv2 (cross-domain)",Reason:"ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From:"domains/elbv2",To:"domains/ec2/systeminstance",Debt:"domains/elbv2 (cross-domain)",Reason:"ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From:"domains/elbv2",To:"domains/ec2/vpc",Debt:"domains/elbv2 (cross-domain)",Reason:"ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From:"domains/elbv2",To:"domains/network/topology",Debt:"domains/elbv2 (cross-domain)",Reason:"ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From:"domains/elbv2",To:"domains/iam",Debt:"IAM implementation consumers in STS, Ochre and ELBv2",Reason:"system-instance role ensurer; needs an IAM capability"},
{From:"domains/elbv2",To:"agents/elbv2",Debt:"domains/elbv2/{agent_types,haproxy,health_checker,nginx}.go import lbagent",Reason:"health-report and cert/PID-path vocabulary; needs a guest/controller contract"},
{From:"handlers/eks",To:"domains/dns",Debt:"close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)",Reason:"legacy EKS calls another domain's or runtime implementation directly"},
{From:"handlers/eks",To:"domains/ec2/ebs/metadata",Debt:"close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)",Reason:"legacy EKS calls another domain's or runtime implementation directly"},
{From:"handlers/eks",To:"domains/ec2/instancetypes",Debt:"close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)",Reason:"legacy EKS calls another domain's or runtime implementation directly"},
{From:"handlers/eks",To:"domains/ec2/placementgroup",Debt:"close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)",Reason:"legacy EKS calls another domain's or runtime implementation directly"},
{From:"handlers/eks",To:"domains/ec2/systeminstance",Debt:"close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)",Reason:"legacy EKS calls another domain's or runtime implementation directly"},
{From:"handlers/eks",To:"domains/network/systemvpc",Debt:"close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)",Reason:"legacy EKS calls another domain's or runtime implementation directly"},
{From:"handlers/eks",To:"handlers/iam",Debt:"close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)",Reason:"legacy EKS calls another domain's or runtime implementation directly"},
{From:"handlers/eks",To:"runtime/compute/vm",Debt:"close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)",Reason:"legacy EKS calls another domain's or runtime implementation directly"},
{From:"domains/rds/instance",To:"domains/dns",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"domains/rds/instance",To:"domains/ec2/instance",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"domains/rds/instance",To:"domains/ec2/instancetypes",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"domains/rds/instance",To:"domains/ec2/systeminstance",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"domains/rds/instance",To:"domains/network/systemvpc",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"domains/rds/instance",To:"domains/iam",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"domains/rds/instance",To:"runtime/compute/vm",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"handlers/elbv2",To:"domains/acm",Debt:"handlers/elbv2 (cross-domain)",Reason:"legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From:"handlers/elbv2",To:"domains/dns",Debt:"handlers/elbv2 (cross-domain)",Reason:"legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From:"handlers/elbv2",To:"domains/ec2/systeminstance",Debt:"handlers/elbv2 (cross-domain)",Reason:"legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From:"handlers/elbv2",To:"domains/ec2/vpc",Debt:"handlers/elbv2 (cross-domain)",Reason:"legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From:"handlers/elbv2",To:"domains/network/topology",Debt:"handlers/elbv2 (cross-domain)",Reason:"legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From:"handlers/elbv2",To:"handlers/iam",Debt:"IAM implementation consumers in STS, Ochre and ELBv2",Reason:"system-instance role ensurer; needs an IAM capability"},
{From:"handlers/elbv2",To:"lbagent",Debt:"handlers/elbv2/{agent_types,haproxy,health_checker,nginx}.go import lbagent",Reason:"health-report and cert/PID-path vocabulary; needs a guest/controller contract"},
{From:"handlers/rds",To:"domains/dns",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"handlers/rds",To:"domains/ec2/instance",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"handlers/rds",To:"domains/ec2/instancetypes",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"handlers/rds",To:"domains/ec2/systeminstance",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"handlers/rds",To:"domains/network/systemvpc",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"handlers/rds",To:"handlers/iam",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"handlers/rds",To:"runtime/compute/vm",Debt:"close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)",Reason:"legacy RDS calls another domain's or runtime implementation directly"},
{From:"handlers/sts",To:"handlers/iam",Debt:"IAM implementation consumers in STS, Ochre and ELBv2",Reason:"STS reads IAM service, trust-policy and OIDC-provider state; needs an IAM capability"},
{From:"operator/cli",To:"accountteardown",Debt:"operator/cli (dependency)",Reason:"command tree imports implementations; needs public capabilities"},
{From:"operator/cli",To:"daemon",Debt:"operator/cli (dependency)",Reason:"command tree imports implementations; needs public capabilities"},
{From:"operator/cli",To:"gateway",Debt:"operator/cli (dependency)",Reason:"command tree imports implementations; needs public capabilities"},
{From:"operator/cli",To:"gateway/bedrock",Debt:"operator/cli (dependency)",Reason:"command tree imports implementations; needs public capabilities"},
{From:"operator/cli",To:"handlers/eks",Debt:"operator/cli (dependency)",Reason:"command tree imports implementations; needs public capabilities"},
{From:"operator/cli",To:"handlers/iam",Debt:"operator/cli (dependency)",Reason:"command tree imports implementations; needs public capabilities"},
{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"},
clause:"ADR-0002:S7: the request record is private to ingress, so no domain, runtime or operator package reads it; only packages under ingress/aws may import ingress/aws/reqctx",
hint:"receive what you need through dispatch.Invocation instead of the request record",
This package owns the cross-process contract for the admission domain's
quota override. It is not an in-process service interface or the owner of
quota state.
## Seal account quota override
`SealAccountQuotaOverrideSubject` (`admission.v1.SealAccountQuotaOverride`) is
a request/reply route on which a gateway, in the `awsgw-quota-seal` queue
group, irreversibly seals one account's quota override record.
`domains/admission/quota` owns it: `awsgw` serves it.
`operator/accountlifecycle` is the consumer: it calls this subject to fence every
override writer at the quota owner before the account record is removed.
The request names the account in its body (`account_id`) and may carry an
opaque `correlation_ref` that is echoed for logs.
`awsgw` ignores `X-Account-ID` for this subject: the account sealed is the one
named in the body.
A seal is refused with `IncorrectState` unless IAM already holds the account
as `TERMINATING`; a precondition read failure, or no termination checker
wired, answers `InternalError`.
Both refusals write nothing and never open the override bucket.
A reply is sent only on success: `disposition` is always `sealed` and
`revision` is the stream leader's accepted write for that seal.
A failure is the standard awserrors error payload, not this reply shape.
Sealing is idempotent: a seal on an already-sealed record re-writes the same
value and returns a fresh, higher revision.
Any current gateway may answer; the leader-accepted revision is the evidence
whichever one does, and the override bucket is opened at seal time even on a
gateway whose quotas were disabled at startup.
## Seal-quota-override capability
`AccountQuotaOverrideSealCapabilitySubject(node)` (`admission.v1.AccountQuotaOverrideSealCapability.<node>`) is a per-node, read-only request/reply route on which `operator/accountlifecycle` asks one configured gateway whether it supports the quota override seal, immediately before every seal attempt.
The quota owner (`domains/admission/quota`) owns it: `awsgw` serves it with no queue group, so a request addressed to one node reaches only subscribers configured as that node, and two gateways sharing a node ID both answer.
The consumer's quota seal reaper probes every configured node that runs `awsgw` and sends no seal request unless every one answers compatible.
`AccountQuotaOverrideSealCapabilityVersion` is a claim about the whole binary's writers, not about the responder: version 1 means every quota override writer in the binary is revision-guarded and refuses a sealed record, and the binary serves the seal subject.
A gateway binary without this responder has no subscriber, so the probe for that node reads as incomplete (`no-reply`), the same as a stopped or partitioned node.
The reply proves nothing beyond the moment it was sent: a gateway downgraded or replaced after a successful probe is outside the guarantee.
### Compatibility boundary
The subject name carries the contract major; `account_quota_seal_test.go` and `account_quota_seal_capability_test.go`
preserve the version-1 JSON shapes and the subject builder.
Producer and consumer are in the coordinated Spinifex release, so the window
is N and N-1.
A gateway that predates the subject has no subscription, so the request gets
no responder.
An incompatible change to the subject or fields needs a new major.
@@ -60,3 +60,42 @@ The subject name carries the contract major; `platform_inventory_test.go` preser
Producer and consumer are in the coordinated Spinifex release, so the window is N and N-1.
A daemon that predates the subject has no subscription, so the request gets no responder; the consumer treats no responder, a timeout, an error reply, an undecodable reply, a reply for another account, and a missing, repeated or unknown-status kind as `scan-incomplete`, never clean.
An incompatible change to the subject, fields or vocabulary needs a new major.
## Seal account settings
`SealAccountSettingsSubject` (`ec2.v1.SealAccountSettings`) is a request/reply route on which a daemon in the `spinifex-workers` queue group irreversibly seals one account's EC2 settings record.
EC2 owns it: `domains/ec2/account` serves it through the daemon.
`accountteardown`'s StageAccount seal reaper is the consumer: it calls this subject only after the seal-capability probe below reports every configured node compatible, and completes only once it holds a successful sealed reply.
The request names the account in its body (`account_id`) and may carry an opaque `correlation_ref` that is echoed for logs.
The daemon ignores `X-Account-ID` for this subject: the account sealed is the one named in the body.
A seal is refused with `IncorrectState` unless IAM already holds the account as `TERMINATING`; a precondition read failure answers `InternalError`.
Both refusals write nothing.
A reply is sent only on success: `disposition` is always `sealed` and `revision` is the stream leader's accepted write for that seal.
A failure is the standard awserrors error payload, not this reply shape.
Sealing is idempotent: a seal on an already-sealed record re-writes the same value and returns a fresh, higher revision.
### Compatibility boundary
The subject name carries the contract major; `account_settings_seal_test.go` preserves the version-1 JSON shape.
Producer and consumer are in the coordinated Spinifex release, so the window is N and N-1.
A daemon that predates the subject has no subscription, so the request gets no responder.
An incompatible change to the subject or fields needs a new major.
## Seal-account-settings capability
`AccountSettingsSealCapabilitySubject(node)` (`ec2.v1.AccountSettingsSealCapability.<node>`) is a per-node, read-only request/reply route on which `accountteardown` asks one configured daemon whether it supports the account-settings seal, immediately before every seal attempt.
EC2 owns it: the daemon serves it with no queue group, so a request addressed to one node reaches only subscribers configured as that node, and two daemons sharing a node ID both answer.
`accountteardown` is the consumer: its StageAccount seal reaper probes every configured node and sends no seal request unless every one answers compatible.
`AccountSettingsSealCapabilityVersion` is a claim about the whole binary's writers, not about the responder: version 1 means every EC2 account-settings writer in the binary is revision-guarded and refuses a sealed record, and the binary serves the seal subject.
A daemon binary without this responder — predating the capability, or carrying only the seal subject without it — has no subscriber, so the probe for that node reads as incomplete (`no-reply`), the same as a stopped or partitioned node.
The reply proves nothing beyond the moment it was sent: a daemon downgraded or replaced after a successful probe is outside the guarantee.
### Compatibility boundary
The subject name carries the contract major; `account_settings_seal_capability_test.go` preserves the version-1 JSON shape and the subject builder.
Producer and consumer are in the coordinated Spinifex release, so the window is N and N-1.
A daemon that predates the capability has no subscription, so the probe gets no responder for that node.
An incompatible change to the subject or fields needs a new major.
This package will own the Lambda process contracts declared by ADR-0009:S13.
It holds the L-GUEST declaration and, in `guest.go`, the connection vocabulary both ends share: the scope, the URI SAN roles, the ALPN identifier and the fw_cfg entry names. The transport and authentication are decided (ADR-0009:S17); the invocation message types and framing are not chosen, so no message types are defined here.
It is not an AWS tenant-facing API.
## L-GUEST: node executor to guest agent
**Owner:** the Lambda domain.
**Producers and consumers:** the node-local executor (`runtime/compute/lambda`) and the in-guest Lambda agent (`agents/lambda`), in both directions.
**Transport:** TCP over the microVM's network (ADR-0009:S17).
The agent dials one connection to a listener the executor owns for the environment, and the guest exposes no listener for host control.
At most one authenticated control connection is active per environment, and the agent may reconnect; how a prior connection and its in-flight work are handled is open (ADR-0009 M1).
The network is shared and untrusted: no source address, port or bridge membership is identity or authority.
Tenant code may be able to reach the listener, but it cannot open the agent's connection or key material, so any connection it makes fails authentication.
**Authentication:** mutual TLS 1.3, bound to the environment ID and realization generation, using material the user runtime cannot read (ADR-0009:S17).
For each generation the executor creates ephemeral self-signed host and guest certificates whose single URI SAN is `spinifex-lambda://<Region>/<account>/<function ID>/<environment ID>/<generation>/<role>`, with role `host` or `guest`.
Each end validates the peer certificate against the one pinned peer certificate and separately requires that URI SAN with the opposite role; hostname verification is not relied on.
ALPN carries the L-GUEST major version, `spinifex-lambda-guest/1`, and an unsupported version fails the handshake.
Under TLS 1.3 the guest's handshake can complete before the host refuses its certificate, so the guest treats the connection as accepted only after the host's first message.
**Bootstrap material:** the executor passes these fw_cfg entries under `opt/spinifex/lambda/`, and the agent reads them as root and unloads `qemu_fw_cfg` before user code starts.
| Entry | Content |
|---|---|
| `guest-key` | Guest private key, PEM |
| `guest-cert` | Guest certificate, PEM |
| `host-cert` | The one pinned host certificate, PEM |
The guest's own interface settings come from `opt/spinifex/netcfg`, the entry other system VMs use.
The agent unloads `qemu_fw_cfg` even when an entry is missing, and refuses to start if the fw_cfg tree is still readable afterwards.
Until the message format exists, the agent authenticates and holds the connection but accepts no host message and starts no user code.
**Authority:** one environment's attempt only.
The user runtime never holds these keys, and sharing the guest with the agent gives user code no platform authority.
**Idempotency and fencing:** an invoke carries the attempt-grant identity and the attempt ID.
The agent accepts a run command only under a current grant, and a completion under a superseded grant is discarded and reported.
How a grant is enforced and proven ended is open (ADR-0009 M1).
**Errors and versions:** the agent and executor fail closed on an unknown protocol version or an identity mismatch.
**Readiness:** environment readiness needs authenticated, current-generation agent evidence; a running hypervisor process does not count.
**Payloads:** the contract must carry a synchronous payload and response of up to 6 MB and an asynchronous payload of up to 1 MB, plus framing, over a path whose size bound is declared.
How payloads move is open (ADR-0009 M3).
### Compatibility boundary
The agent ships inside the runtime image pinned by each published version, so it is outside the coordinated release train.
The executor supports every L-GUEST major carried by a still-supported runtime image.
Retiring an image follows a published runtime-deprecation lifecycle that blocks create and update before it blocks invoke.
Evidence required before the first agent image ships: a function on an old image invoked by a new executor, and the reverse during rollback.
## L-RUNTIME
The guest agent serves the AWS Lambda Runtime API, version `2018-06-01`, to the user runtime at `AWS_LAMBDA_RUNTIME_API`.
Spinifex does not version this AWS contract, and conformance is measured against AWS.
@@ -8,3 +8,8 @@ administration. It is separate from AWS tenant-facing service contracts.
roles and Reed-Solomon configuration. It is an operational status contract,
not a generic provider abstraction; do not change its subject or JSON shape
incompatibly. Add a new version when that is required.
`admin.go` holds the `/admin/<Method>` request and response bodies the private admin surface shares with the CLI.
`AdminMethodNames` is the list of callable methods, and the gateway router builds its accept set from it.
`AccountQuotaRequest`, `AccountQuotaResponse` and `DeleteAccountResponse` stay in the gateway until the `/admin` handlers move, because they embed types the contracts layer may not import.
Do not change a field name, JSON tag or omitempty incompatibly; add a new version when that is required.
@@ -60,6 +60,10 @@ moved to another node; the local daemon subscribes to stop the guest that
was using it. Node-addressed, like mount and unmount: the guest is local, and
no other node's daemon has anything to stop.
The event is sent as a request and repeated until the daemon replies with `VolumeFencedAck`, because the fence often runs while the node's NATS server is restarting and a single publish can reach no subscriber.
The daemon replies only when the event carries a reply subject, so a plain publish is still accepted.
Repeats are harmless, since fencing a guest that is already fenced changes nothing.
## Abandon route
`VolumeAbandonSubject(node)` is `ebs.<node>.abandon`. The daemon uses
// EBSMountResponse is the response to an EBSRequest on MountSubject.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.