mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
5aaf8c23d29cfec29d4b489111a7bdffeb6cd32f
1142 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5aaf8c23d2 |
fix(eks): make aws eks get-token → kubectl auth work end-to-end (Sprint 6f) (#303)
* feat(eks): webhook observability — console log sink + per-request TokenReview decision * fix(eks): wire token-webhook authenticator into generated k3s apiserver config The generated /etc/rancher/k3s/config.yaml overrides the AMI-baked config.yaml.skel, but its kube-apiserver-arg list omitted the authentication-token-webhook-config-file arg. The running apiserver therefore never loaded the token webhook, so bearer tokens minted by 'aws eks get-token' 401'd without the webhook ever being invoked. * fix(eks): echo TokenReview audiences so api-audiences apiserver accepts the principal With --api-audiences set, kube-apiserver sends spec.audiences in the TokenReview and discards an authenticated=true response that does not confirm a matching audience. The webhook returned an empty status.audiences, so a valid IAM principal surfaced as a 401. Echo the request's audiences on success. * fix(eks): pin token-webhook version v1 so apiserver accepts the response kube-apiserver --authentication-token-webhook-version defaults to v1beta1, but the eks-token-webhook responds with an authentication.k8s.io/v1 TokenReview. The apiserver decodes the response against the expected GVK and rejects the mismatch, surfacing a valid principal as a 401. Pin v1 to match the webhook. |
||
|
|
d8aa31e699 |
ci(baremetal): capture combined log + junit + analyze bundle on failure
Bare-metal e2e produced no artifact, so RCA fell back to truncated log-failed output. Tee bootstrap + e2e output to test-baremetal.log, generate junit-baremetal.xml, run e2e-analyze to emit analysis.md, and upload the bundle as e2e-baremetal-artifacts-<run_id>. |
||
|
|
f4ba115316 | ci(baremetal): revert e2e to single-node | ||
|
|
597b18b2a0 |
fix(eks): map transient JetStream errors in ListClusters to ServiceUnavailable (#302)
aws eks list-clusters returned InternalErrorException for a few seconds after a daemon restart, while describe-cluster (by name) succeeded. During the post-restart window the per-account KV stream's Raft group has no leader yet, so kv.Keys() (an ordered consumer over the warming stream) fails with a transient NATS error. ListClusters wrapped it as a fmt.Errorf string the daemon's ValidErrorCode cannot map, so it sanitized to ServerInternal — a misleading 500 that tells AWS clients the request is non-retryable. Classify transient NATS conditions (no-responders, timeout, no-stream-response, connection-closed) and return a retryable ServiceUnavailable (503) instead; genuine faults still pass through wrapped. describe-cluster survived only because kv.Get on the same bucket is cheaper than the consumer kv.Keys spins up. |
||
|
|
ca2418ffa7 |
feat(eks): OIDC provider CRUD + IRSA discovery + anonymous STS (6e) (#301)
* feat(iam,awsgw): OIDC provider CRUD + IRSA discovery endpoints (6e) Complete the AWS-SDK-facing half of IRSA: - IAM Create/Get/List/DeleteOpenIDConnectProvider, backed by the per-account oidc-providers registry that STS AssumeRoleWithWebIdentity already reads to authorise an issuer. Create writes the key STS checks; the provider ARN is the scheme-stripped issuer per AWS convention. - awsgw serves the per-cluster OIDC discovery document (/.well-known/openid-configuration) and JWKS (/keys) as unauthenticated public endpoints, mounted ahead of the SigV4 surface in their own route group. The discovery issuer is the cluster's stored OIDCIssuer, byte-exact with the apiserver iss claim; JWKS is served from the EKS account bucket. Unit + route tests cover CRUD happy/idempotent/not-found/invalid-url, the ARN<->issuer round-trip, discovery issuer/jwks_uri equality, JWKS bytes, 404s, and the no-auth bypass. make preflight green. * fix(awsgw): route anonymous AssumeRoleWithWebIdentity past SigV4 (6e) AssumeRoleWithWebIdentity is the IRSA credential-bootstrap call: the AWS SDK sends it unsigned because the caller holds no AWS credentials, only a projected ServiceAccount JWT in the body. The gateway ran the SigV4 middleware first and rejected it with MissingAuthenticationToken, so the true anonymous path never reached the handler. Add anonymousSTSInterceptor, a pre-auth middleware mounted ahead of the SigV4 surface. For an unsigned POST it body-peeks the Action and, for the anonymousSTSActions set, dispatches straight to STS_Request; STS_Request now skips SigV4-caller resolution for those actions (the handler authenticates the JWT and gates on the role's web-identity trust policy). Signed requests carry an Authorization header and pass through untouched, off the body-buffering path. Surfaced by the 6e live exit gate: a real in-pod assume-role-with-web-identity would otherwise have been rejected before reaching token validation. |
||
|
|
a22bc2f716 |
feat(eks): 6d nodegroup provisioning + NATS-gated cluster health (#300)
* feat(eks): nodegroup provisioning + K3s agent join (6d)
CreateNodegroup launches N customer-owned worker EC2 from the unified
eks-node AMI in agent role; each joins the cluster via K3S_URL + K3S_TOKEN
baked into cloud-init (first-boot.env SPINIFEX_K3S_ROLE=agent + agent.env).
Workers are customer-owned and DescribeInstances-visible (no ManagedBy,
no mgmt NIC) via the normal RunInstances path, matching real EKS.
K3S_URL targets the control-plane ENI's in-VPC private IP on :6443 — the
cluster NLB DNS endpoint is not resolvable in-VPC yet; switch to the NLB
endpoint on :443 once authoritative in-VPC DNS lands. K3S_TOKEN is the
decrypted cluster node-token persisted at server bootstrap.
Describe/List/Update(scaling)/Delete round out CRUD. Cluster<->nodegroup
SG ingress rules (apiserver 6443, flannel VXLAN 8472/udp, kubelet 10250,
intra-nodegroup) authorized idempotently. UpdateNodegroupVersion deferred.
* feat: gate EKS cluster health via NATS state reports
The K3s apiserver binds the VPC node-ip, unreachable from the host-side
spinifex daemon, so the reconciler cannot probe /healthz directly. Add a
control-plane state reporter that publishes {healthz, node_count} over the
management NATS bus the daemon already shares.
- eks-node server role runs mulga-eks-state-report as an OpenRC service,
publishing eks.state.{account}.{cluster}.server every 30s.
- ClusterReconciler subscribes to that subject, gates CREATING -> ACTIVE on
a healthy report, and records Ready node_count into ClusterMeta.
- node_count counts Ready nodes only; k3s retains terminated workers as
NotReady until pruned, so a raw count never drops on scale-down.
|
||
|
|
3bb90c9fd3 |
fix(ci): guard release Clean Workspace against leaked iso-builder devtmpfs
The iso job mounts a kernel-global devtmpfs under the workspace (.cache/rootfs/dev). A killed job skips its EXIT-trap unmount and leaks the bind; the verify job (same ci-single runner) then ran a bare 'rm -rf $WORKSPACE/*' that could traverse it and wipe the host /dev. Detach any mount under the workspace before deleting and add --one-file-system so rm never crosses a mount boundary. Same backstop added to the iso job's own cleanup. |
||
|
|
da26b1db69 |
fix(ci): stop image-build /dev bind from wiping host /dev
A killed ubuntu system-image build can leave `mount --bind /dev` under its build dir. The e2e Clean Workspace step then ran `sudo rm -rf "$GITHUB_WORKSPACE"/*` as root, traversed the leaked bind, and deleted the host's /dev (incl. /dev/null), wedging the runner. - e2e.yml: umount anything under the workspace (deepest-first) before deleting, and pass `rm --one-file-system` so rm can never cross a mount boundary into host /dev. Hard backstop, applied to all 3 Clean Workspace steps. - build-system-image.sh: recursive lazy unmount in cleanup() detaches the chroot binds even when busy, and mark the build subtree rprivate so the /dev bind can't propagate into the host namespace. |
||
|
|
f9c08130ba |
fix(eks): embedded etcd datastore + Alpine IMDS-route fix + CP-VM visibility (#299)
* fix: switch EKS datastore to embedded etcd + fix Alpine cloud-init IMDS route
Flip the K3s control-plane datastore from SQLite/kine back to embedded etcd
(cluster-init: true, etcd-expose-metrics: true). The earlier abandonment
premise — that etcd's per-commit fsync stalls past the apiserver's 5s deadline
on the viperblock-backed root volume — was wrong: etcd inits fine on an
unchanged viperblock (apiserver ready 25-30s, clusters reach ACTIVE). The real
blocker was a network-config regression, fixed here.
Alpine's cloud-init eni renderer crashes (KeyError: 'gateway') on the
gateway-less IMDS on-link route, which failed the whole network-config render
so the static mgmt NIC never came up (no NATS bootstrap, no /healthz). Fixes:
- generateNetworkConfig: add emitIMDSRoute bool; selectNetworkConfigForFamily
passes false for Alpine so the eni renderer never sees the route.
- Deliver the IMDS route out-of-band via a persistent /etc/local.d script +
OpenRC local service. Resolve the VPC NIC via its default route at runtime
(ip route show default), not a hardcoded name: the persistent-net rename to
vpc0 doesn't apply on the Alpine AMI, so the NIC is eth0 at first boot.
- EKS path bakes the same route into buildK3sUserData's own write_files/runcmd;
the generic template Alpine block fires only when no #cloud-config user-data,
avoiding a duplicate write_files key (yaml.safe_load keeps last, silently
dropping a block).
- eks-node image: config.yaml.skel adds cluster-init; comments updated.
* fix: surface CreateCluster step failures at ERROR + map subnet faults; add auth-mode/delete test cases
CreateCluster wrapped its step failures in bare fmt.Errorf that the request
surface collapses to an opaque ServerInternal ("contact AWS re:Post"), so the
real cause never reached the operator. Add logCreateErr to log each step's
precise cause at ERROR before returning, and map a subnet-resolve failure
(caller-supplied parameter) to InvalidParameterValue instead of a retryable
ServerInternal the SDK pointlessly retries. (mulga-siv-165.4)
Tests (mulga-siv-165.15): reject authenticationMode=API_AND_CONFIG_MAP (only
the CONFIG_MAP value was covered); DeleteCluster on a nonexistent cluster via
the full-deps path returns ResourceNotFound (the shim path short-circuits to
ServiceUnavailable before the meta lookup).
* fix: hide EKS control-plane VMs from customer DescribeInstances
EKS K3s control-plane VMs are owned by the customer account (their ENI
lives in the customer VPC for the NLB target), so unlike LB microVMs
(owned by the system account) they leaked into customer
ec2:DescribeInstances. The spinifex:managed-by tag only filtered the UI
Nodes page, not the EC2 API.
Add a caller-aware exclusion: skip any ManagedBy-tagged VM unless the
caller is root. Preserves operator visibility, matches LB behaviour, and
leaves future customer-owned worker nodes (no ManagedBy) visible.
* docs: correct stale SQLite datastore comments in eks-node image (now embedded etcd)
|
||
|
|
6be3ccbb23 | fix: unify .iso and .iso.sha filenames | ||
|
|
158b3a25c5 | fix: updated release.yml for clean iso uploads | ||
|
|
4f84586263 | feat(ui): IAM Roles + Instance Profiles | ||
|
|
958a9cb6c1 | chore: fix docs | ||
|
|
00d1142a02 | chore: cleanup docs | ||
|
|
412eb618b0 | Adding GPU passthrough e2e tests - intended to be run on Wattle | ||
|
|
77b3bbd7b7 | fix: add route table to VPC docs | ||
|
|
2d21232d02 | fix(ui): auto-recover from stale credentials with login redirect (#297) | ||
|
|
81c8d62859 | fix: add SG guide to docs | ||
|
|
0c76161a89 |
fix(elbv2): stop lb-agent hanging on nginx start (NLB targets stuck 0/N)
reloadNginx started nginx via cmd.CombinedOutput(). nginx daemonizes, and the rendered NLB config uses `error_log stderr`, so the forked master and workers inherit and hold this command's stderr open. CombinedOutput's Wait blocks on stderr EOF, which the long-lived daemon never sends — so the call never returns, wedging the agent's synchronous tick() before it applies the nginx engine or reports health. NLB targets stayed at 0/N until the e2e health wait timed out, while the data plane (nginx itself) ran fine — so manual smoke tests passed. ALB is unaffected: haproxy -D redirects its stdio to /dev/null on daemonize, so CombinedOutput sees EOF and returns. Capture nginx start/reload stdio to a temp file and use cmd.Run(), so Wait blocks only on the short-lived foreground process that exits once daemonized. Surfaced via the LB microVM guest console now collected in CI. |
||
|
|
dc1b87f191 | feat(ui): VPC-create AWS-flow alignment — SG launch step, public-IP, NAT Gateway + Elastic IP (#294) | ||
|
|
70e9746e06 | chore: tags cleanup | ||
|
|
af690b3299 |
test(elbv2): collect LB microVM guest console in CI + surface NLB probe failures
NLB target health stalls at 0/N in CI while the data plane works locally. The lb-agent's nginx engine activation, any nginx -t/reload error, and its active-probe results are logged to the LB microVM serial console — which the host journal never captures and the GetConsoleOutput API path reports as NotFound, so the failure is undiagnosable from CI artifacts. - e2e.yml: pull /run/spinifex/console-*.log off each node in the single- and multi-node collect steps (files persist past VM teardown) into lb-console/. - lbagent healthcheck: log probe failures at Warn (was Debug) so unreachable targets surface at the agent's default level on the captured console. |
||
|
|
dae0b521da | fix: remove falltrhough code owners | ||
|
|
c666c8146d | fix: e2e.yml | ||
|
|
8e38faaede |
fix(elbv2): make NLB nginx health-check pathway robust in CI
E2E NLB suites timed out at "0/2 healthy" while ALB suites on the same target instances went healthy in ~5s. The host daemon journal shows the NLB agent reporting an empty server list (no probe results at all), which happens when the lb-agent never enters nginx mode — i.e. `nginx -t`/start fails closed inside the LB microVM, so the agent never probes and reports nothing. The data plane itself is verified working; this is purely the active-health-check pathway failing to converge in CI. - lbagent: pre-create nginx runtime prefix dirs (pid dir + /var/lib/nginx, /var/lib/nginx/tmp, /var/log/nginx) before `nginx -t`, mirroring reloadHAProxy's socket-dir pre-create. A stripped Alpine initramfs may lack these, dead-ending the whole NLB health path. - build-microvm-image.sh: assert the nginx binary and ngx_stream_module.so are present in the image and fail the build loudly, instead of surfacing as an opaque e2e health timeout. - e2e: capture the LB VM serial console to the artifact bundle on NLB health-wait failure. The agent's engine activation and any `reload nginx:` error land on the guest console, which the host journal never sees. |
||
|
|
554c5d96c7 |
feat(elbv2): NLB L4 data plane on nginx stream + UI parity (#295)
* feat(elbv2): NLB L4 data plane on nginx stream (mulga-siv-952) Network load balancers render an nginx `stream` config (TCP/UDP/TLS/ TCP_UDP) instead of HAProxy, which cannot load-balance UDP in any release. The gateway tags GetLBConfig with Engine=nginx; the LB agent dispatches to the nginx engine (config/pid/cert paths + reload) and skips HAProxy stats polling. ALB stays on HAProxy. nginx is added to the microvm image. * docs(elbv2): note NLB runs nginx stream, ALB runs HAProxy (mulga-siv-952) * fix(elbv2): load nginx stream module for NLB data plane Alpine's `nginx` apk is built without the stream module — it ships separately as `nginx-mod-stream`. Without it, the generated NLB config fails `nginx -t` with [emerg] unknown directive "stream", so the agent never applies it. Add the package to the microvm image and emit a `load_module .../ngx_stream_module.so;` directive at the top of the generated config. * fix(elbv2-ui): allow single-subnet LB + full NLB protocols in listener edit Create-LB form required min 2 subnets while the CLI/backend accept 1; relax to min(1) to match. Listener add/edit only offered HTTP/HTTPS — add the NLB protocol set (TCP, UDP, TLS, TCP_UDP), driven by LB type, with cert/ssl-policy fields gated on secure protocols (HTTPS/TLS). * feat(elbv2): active target health checks for nginx NLBs nginx stream has no active upstream probing, so NLB targets never advanced past initial — DescribeTargetHealth stayed initial forever and WaitForTargetsHealthy timed out for every NLB. GetLBConfig now delivers per-target HealthTarget specs (daemon-computed server name, probe address, protocol/path) for network LBs; the lb-agent probes each target every tick (TCP dial or HTTP/HTTPS GET) and reports UP/DOWN via the existing heartbeat path, so the daemon's threshold hysteresis advances targets to healthy. ALBs are unchanged (HAProxy stats). Adds an Internal_NLB_UDP e2e suite gating on WaitForTargetsHealthy plus a UDP datagram round-trip through the VIP. |
||
|
|
d044a4c222 |
feat(vm): ENI hot-plug Sprint 3c — real OVS tap wiring (#296)
* test: cover Sprint 3b ENI hot-plug surface Sprint 3b's gateway + daemon + KV-helper additions landed without unit tests. dev's diff-coverage gate flipped to 28.5% as a result. This adds the three test files needed to bring the diff coverage of the ENI hot-plug surface back above the 60% threshold. - handlers/ec2/vpc/eni_hotplug_helpers_test.go: GetENIRecord, UpdateENI, FindENIByAttachment — happy paths, not-found, account scoping, round-trip of every new ENIRecord field. - gateway/ec2/vpc/network_interface_hotplug_test.go: Attach + Detach validation, no-responders, NATS roundtrip with command capture, daemon error / malformed responses, attachment-resolver branches. - daemon/daemon_handlers_eni_test.go: eniHotplugErrorCode table, publishENIHotplugEvent (nil + publish), full handleAttach / handleDetach matrix (nil data, not-running, ENI/attachment not found, wrong owner, success with stub QMP + NATS event assertion, hot-plug failure + KV rollback assertion). vm.SetHotPlugTestSeams is added as an exported test seam following the same pattern as utils.SetSudoCommandForTest so daemon-package tests can swap newDeviceController + eniPipelineSleep without reassigning vm-package unexported vars. * feat(vm): wire real OVS tap into ENI hot-plug pipeline Sprint 3c: replace the stubTapAdd/stubOVSAddPort/stubTapDel/stubOVSDelPort placeholders with the shared NetworkPlumber. SetupTap creates the kernel tap and attaches it to br-int with the OVN iface-id binding in one atomic step; CleanupTap reverses both on rollback and detach. Nil-plumber managers stay a noop, mirroring setupExtraENINICs. Adds fake-plumber coverage: tap-on-br-int spec assertion, tap-setup failure aborts before any QMP call, and QMP/detach paths both invoke CleanupTap. |
||
|
|
eef337fb80 |
test(e2e): make egress baseline reproduce the real IGW gap (fresh VPC)
The previous new-subnet egress baseline used the default VPC, whose main route table is prebaked with 0.0.0.0/0 -> IGW at bootstrap and reconciled by the vpcd drift loop, so a subnet created in it is reachable regardless of the create-time gap — the test passed and never flagged the bug. Replace it with runNewVPCEgressBaseline: a brand-new VPC where the IGW route is added to the MAIN route table BEFORE the public subnet is created. The subnet then implicitly joins a main RT that already carries the route, but CreateSubnet does not recompute per-subnet egress, so the subnet's OVN egress LRP is never installed and the instance has no datapath to the IGW. With an open SG, routing is the only barrier, so the instance is unreachable — red until CreateSubnet triggers a per-subnet egress recompute. PoolMode-gated like runVPCSubnetE2E (fresh VPC needs OVN IPAM for a public IP). runVPCSubnetE2E doesn't catch this: it associates a custom RT then adds the route, so the route mutation enumerates the subnet and installs the LRP. |
||
|
|
c9a4e904a8 |
test(e2e): fresh-install reachability baselines + drop qemu hostfwd fallback
Add baseline tests that confirm two fresh-install regressions the existing
suite masks (it authorizes tcp/22 on the default SG before every SSH probe):
- default SG default-deny gates external reach of instances in the
default subnet (SG, not routing)
- a newly-created public subnet never gets its OVN IGW egress LRP, so it
has no route to the VPC IGW despite MapPublicIpOnLaunch
Plus a positive test: instances sharing the default SG can communicate via
ICMP (same-host single-node, cross-host multinode).
Single-node baselines run after TestClusterStatsCLI (0-VM gate); multinode
after TestMultinodePreflight (fixture init). Instances are self-cleaning so
they don't perturb the singleton-VM ordering.
Remove the qemu hostfwd SSH fallback from the harness (InstancePublicSSHHost,
GuestSSHEndpoint) - it bypasses the OVN datapath and masks these regressions.
Endpoints now t.Fatal when no public IP is assigned.
|
||
|
|
ff0dc42562 | feat: IMDS subnet level (#292) | ||
|
|
50bc38c8fd |
feat(ui): NLB support in create/edit load balancer + target group flows (#293)
* feat(ui): NLB support + protocol/type-dependent create-LB fields The Create Load Balancer page was hardcoded to ALB. Add a load-balancer Type selector (application/network) and switch the listener + target-group form options on the selected type, mirroring the server's validation: - ALB: HTTP/HTTPS listeners and target groups. - NLB: TCP/UDP/TLS/TCP_UDP listeners and target groups; no security groups. - Secure protocols (HTTPS/TLS) reveal a certificate selector; HTTPS also a security-policy selector. Cert is required for both; SSL policy is HTTPS-only. - The default target-group protocol + health-check protocol stay in sync with the listener (TCP health checks omit HTTP path/matcher). - The wizard mutation now sends the chosen Type and threads cert/SSL-policy through CreateListener (previously hardcoded Type=application and dropped certificates). * feat(ui): NLB-aware target groups and LB detail Standalone Create Target Group offers the full protocol set (HTTP/HTTPS/TCP/UDP/TLS/TCP_UDP) and syncs the health check to TCP for L4 groups, dropping path/matcher from the request and CLI preview. Hide the Security groups tab on the load balancer detail page for network LBs. |
||
|
|
9fa596001f |
fix: EKS 6b create-cluster follow-ups (security + cleanup + tests) (#291)
* fix(admin): write ~/.aws credentials 0600 via atomic temp+rename UpdateAWSINIFile ended with ini.SaveTo, which uses os.Create (0644 & ~umask) and left ~/.aws/credentials world-readable. Render to a 0600 temp file and rename into place so secrets are never exposed. * fix(eks): purge OIDC signing key on zeroize; pin account bucket History=1 ZeroizeClusterOIDCKey used kv.Delete (a tombstone) which leaves prior revisions of the encrypted PEM in KV history. Use kv.Purge to drop the key's whole history. Decouple the account bucket's History from the schema version (KVBucketEKSAccountHistory=1) so a version bump can't silently widen history and resurrect ciphertext. * fix(reconcile): get-or-create leader bucket in AcquireLeader CreateKeyValue returns 'stream name already in use' once the bucket exists, so every reconcile after the first hit the 60s deadline and never elected a leader. Attach to the existing bucket first, create only when genuinely absent. * fix(eks): drop registry entry on lease-held instead of phantom holder RunClusterReconciler returned (nil, nil) when another node held the lease; Spawn recorded a holder with a nil release, so Has() stayed true and a later Spawn no-op'd — the cluster was never re-reconciled until daemon restart. Return ErrLeaseHeld so Spawn drops the entry and retries after the holder's TTL lapses. * refactor: tidy EKS cluster state, dedup, and not-found error wiring - listClusterNames: O(n) map dedup instead of O(n^2) slices.Contains - drop dead store *Store field + NewStore call from EKSServiceImpl - ClusterMeta.LastHealthProbe: drop misleading omitzero tag (stdlib encoding/json ignores it; json/v2 only) and document why it always serializes - clarify ErrorResourceNotFound is the ACM code; EKS uses ErrorEKSResourceNotFound (ResourceNotFoundException) * test: EKS 6b pagination, restart-resume, and bootstrap bind hardening - T7: ListClusters NextToken/MaxResults paging + out-of-range clamp - T4: SpawnRegisteredReconcilers resumes CREATING/ACTIVE on restart, skips terminal clusters, no-ops when deps absent - T-quality: replace sleep-based subscription binding with polling NumSubscriptions, poll KV for the one-shot drop, and assert Run tears down its subscriptions on return T1 (restjson wire shape), T5 (JWKS kty mismatch), and T6 (resolveGatewayHost branches) were already covered. * test(reconcile): cover AcquireLeader get-or-create election First acquire creates the bucket and wins; a second acquire while the lock is held attaches to the existing bucket and loses cleanly (the get-or-create fix, not a CreateKeyValue dead-end); after release a later acquire takes over. |
||
|
|
216b9c7682 |
feat(ui): HTTPS listeners — ACM certificate + SSL policy (Phase C) (#290)
* feat(ui): HTTPS listeners with ACM certificate + SSL policy Expose HTTPS in the load balancer listener UI. The ListenerForm gains a HTTPS protocol option that reveals a certificate selector (backed by a new ACM ListCertificates query), an inline certificate import dialog (ImportCertificate), and an SSL security-policy selector (DescribeSSLPolicies). CreateListener/ModifyListener now carry Certificates + SslPolicy for HTTPS; the listeners table summarises the policy and certificate per HTTPS listener. Adds an ACM client (getAcmClient) and @aws-sdk/client-acm. Server, gateway and ACM endpoints already exist (epic phases A + B); this is frontend-only. * fix: ACM JSON timestamps, react-compiler watch reactivity, scroll-chaining - gateway/acm: serialise responses with jsonutil.BuildJSON so time.Time fields emit epoch seconds (AWS JSON 1.1 wire format). encoding/json emitted RFC3339 strings the JS SDK rejected with 'Expected real number', crashing the LB detail Listeners tab once any cert exists. - frontend: replace watch() with useWatch across forms so the React Compiler tracks the subscription and re-renders on field change (HTTPS listener cert/policy fields now reveal). Add toggle regression test for listener-form. - frontend: scope overscroll-none to html,body instead of *, restoring wheel scroll-chaining over nested scroll containers (sidebar, tables, dialogs). |
||
|
|
bcb49cd771 | feat: IMDS v1 (#266) | ||
|
|
f14fb64562 |
feat(eks): unify eks-server + eks-agent into one eks-node AMI (#288)
* feat(eks): unify eks-server + eks-agent into one eks-node AMI Collapse the two K3s AMIs into a single spinifex-eks-node image whose role (server vs agent) is chosen per-instance at first boot, halving build time, storage, and CVE-rotation surface. - scripts/images/eks-node/: superset manifest + merged setup.sh; all role services baked but only crond + the new eks-node-role selector enabled. - eks-node-role first-boot selector reads SPINIFEX_K3S_ROLE, rc-update adds + starts the matching services (server: k3s + eks-token-webhook + k3s-first-boot publisher; agent: k3s-agent), records /etc/spinifex-eks/role, self-disables. Paths are env-overridable; eks-node-role_test.sh covers all five cases. - mulga-eks-etcd-snapshot.sh early-exits on agent nodes. - k3s_server_vm.go seeds SPINIFEX_K3S_ROLE=server into first-boot.env. - Makefile/dev-install: build-eks-node-image / import-eks-node-image replace the server+agent targets. - Delete scripts/images/eks-server + eks-agent. The launcher resolves the AMI by the spinifex:managed-by=eks tag, so running clusters (which hold the resolved AMI ID) are unaffected. AMI bake + cluster boot are operational integration steps, not yet run. * fix(eks): order eks-node-role after cloud-config/cloud-final The first-boot role selector reads /etc/spinifex-eks/first-boot.env, which cloud-init delivers via write_files in the cloud-config/cloud-final stages. The selector only depended on net+cgroups, so on a fast boot it could start before write_files landed and read a missing role env. Add an after-ordering on cloud-config + cloud-final so the env file is always present first. |
||
|
|
bfacca7789 |
feat(eks): 6c token webhook + AccessEntry (IAM auth → kubectl) (#284)
* feat(eks): 6c AccessEntry CRUD + access-policy association
Replace the nine notImpl() AccessEntry/AccessPolicy stubs with real
KV-backed handlers (Q9, API auth mode):
- AccessEntry CRUD persisted at clusters/{name}/access-entries/{hash};
the principal ARN is hashed into the key (ARNs carry ':', illegal in
NATS KV keys) while the record keeps the plaintext ARN.
- AssociateAccessPolicy/Disassociate/ListAssociated against the four
supported AWS-managed policies; unsupported policy ARN or bad access
scope -> InvalidParameterValue.
- ListAccessPolicies returns the static supported catalogue.
- Cluster-existence gate -> ResourceNotFoundException; duplicate create
-> ResourceInUseException; node-type entries rejected until 6d.
Updates the daemon routing test for the now-implemented handlers.
* feat(eks): plumb caller principal ARN + bootstrap-creator-admin
Thread the caller's resolved IAM principal ARN from the gateway to the
EKS control plane via a new X-Principal-ARN NATS header, so CreateCluster
can mint the bootstrapClusterCreatorAdminPermissions AccessEntry (Q9):
- utils.NATSRequest takes optional extra headers; PrincipalARNHeader +
PrincipalARNFromMsg added.
- Gateway resolves the caller ARN best-effort (buildCallerARN) and passes
it down the EKS route table; only CreateCluster consumes it.
- CreateCluster signature gains callerPrincipalARN; default-true bootstrap
flag mints a system:masters entry keyed by the caller's exact ARN (the
token webhook resolves the same ARN from STS). '' degrades gracefully.
- Dedicated daemon handler reads both headers.
* feat(eks): IAM token webhook + STS verify-over-NATS + apiserver wiring
The eks-token-webhook (baked into the eks-server AMI) authenticates kubectl
requests that present an `aws eks get-token` bearer token: decode the presigned
STS GetCallerIdentity URL, verify it, resolve the principal ARN to its cluster
AccessEntry, and answer the apiserver TokenReview with the mapped username +
groups.
STS is hosted in-process in the awsgw service and is not otherwise on the bus,
so awsgw now serves an `eks.VerifyToken` NATS subject that wraps
VerifyPresignedGetCallerIdentity (the cluster name is forwarded so STS enforces
the x-k8s-aws-id anti-replay pin). The webhook reads the AccessEntry KV directly
over its own NATS connection.
Apiserver wiring: the k3s config skeleton sets
authentication-token-webhook-config-file; the webhook writes its own self-signed
serving cert + the apiserver kubeconfig at startup (ordered `before k3s`, with a
bounded wait in the k3s start_pre so the apiserver never loads a missing file).
* fix(eks): omit empty spec from token webhook TokenReview response
* fix(sts): presigned GetCallerIdentity must hash empty body, not UNSIGNED-PAYLOAD
VerifyPresignedGetCallerIdentity built the SigV4 canonical request with
"UNSIGNED-PAYLOAD" as the payload hash. That is an S3-only convention; botocore
and the aws-sdk presign non-S3 requests (sts:GetCallerIdentity, the shape `aws
eks get-token` produces) with the SHA256 of the empty string. Every real
get-token therefore failed signature verification with InvalidIdentityToken,
breaking the entire EKS IAM token-auth flow end to end.
Use the empty-string SHA256. The existing hand-rolled presign tests masked the
bug by signing with the same wrong constant; they now sign correctly, and a new
test presigns with the real aws-sdk-go v4 signer (the same canonicalisation the
CLI uses) so this can't regress silently.
Verified live: a real `aws eks get-token --cluster-name dev1` now resolves to
the caller principal over the awsgw eks.VerifyToken surface; an otherwise-valid
token presented for a different cluster is still rejected (anti-replay intact).
* fix(eks): match access-entry routes on escaped path so slashed principal ARNs resolve
IAM principal ARNs (user/admin, role/foo) embed a '/' that the AWS CLI
percent-encodes in the request path. The dispatcher matched r.URL.Path
(decoded), so %2F became a literal '/' and split the ARN across the
([^/]+) capture, dropping every describe/update/delete-access-entry and
associate/disassociate/list-access-policy call to InvalidActionException.
Match r.URL.EscapedPath() so the encoded ARN stays one segment, then
url.PathUnescape each captured param before it reaches the handler.
* test(eks): unit-cover access-entry scope validation, AWS converters, store guards
|
||
|
|
b0227e7eeb |
chore(ansible): remove ansible tree, uplift to mulga/ansible/ (#287)
Ansible orchestrates the whole stack (spinifex + predastore + viperblock + OVN + tofu hosts) and its spec/beads live in mulga. Moving it to top-level mulga/ansible/ ends the per-change submodule-pointer-bump friction. - git rm -r scripts/ansible/ - Makefile: drop ansible-dev-* / ansible-cluster-bootstrap targets + .PHONY The tree lands at mulga/ansible/ (flat copy, Option B). History pivot: tag feat/uplift-ansible-snapshot-pre-move marks the pre-move HEAD. Plan: docs/development/improvements/ansible-uplift-to-mulga.md (in mulga). |
||
|
|
61d73d1d4e |
fix: reject unknown ACM cert ARN at listener API boundary (#289)
An unresolvable certificate ARN was accepted into a listener record and only failed later at config-render time, which aborted updateStoredConfig wholesale and silently froze the load balancer's data-plane convergence. Validate every certificate ARN against the ACM store (existence + account ownership) in CreateListener, ModifyListener and AddListenerCertificates, returning CertificateNotFound up front. Skipped when the ACM store is unavailable, matching the nil-safe resolve path. |
||
|
|
470954bbe8 |
feat(elbv2): HTTPS data plane — TLS termination via ssl crt + PEM delivery (#286)
Resolve listener certificate ARNs to PEM via the ACM store (shared JetStream KV, no cross-service hop), render HAProxy 'ssl crt' for secure frontends, and deliver the combined cert+chain+key PEM to the LB agent alongside the config. ConfigHash folds in cert content so rotation triggers a reload; the agent writes each cert 0600 (path-constrained to its cert dir) before the config + reload. Phase B of the ELBv2 HTTPS epic (mulga-siv-180). |
||
|
|
38f5dd0d41 |
feat(acm): minimal ACM-alike certificate service (#285)
Stand up acm as its own AWS JSON 1.1 gateway service wiring the BYO-cert import/store path ELBv2 HTTPS listeners need. ImportCertificate validates tls.X509KeyPair, parses the leaf for domain/serial/expiry/key-algorithm, mints arn:aws:acm:<region>:<acct>:certificate/<uuid>, and stores PEM (cert, chain, key) in JetStream KV. DescribeCertificate/ListCertificates/ DeleteCertificate round-trip; access is account-scoped. New gateway dispatch by X-Amz-Target (CertificateManager.<Action>); errors share the AWS JSON 1.1 envelope with EKS. Wired through gateway/acm ops, handlers/acm (service iface, store, NATS client), daemon handlers + acm.* subscriptions, and service-interfaces.yaml. Out of scope (future, same epic): RequestCertificate + DNS validation, managed renewal, Get/ExportCertificate, tagging, ACM-PCA. |
||
|
|
e06d0c0865 |
feat(elbv2): HTTPS listener certificates + SSL policies (Phase A) (#283)
* feat(elbv2): HTTPS listener certificates + SSL policies Add Certificates[]/SslPolicy to ListenerRecord. CreateListener and ModifyListener validate secure protocols (HTTPS/TLS) require >=1 certificate with exactly one default, non-secure protocols reject certs/policy, and default+validate the SSL policy. DescribeListeners echoes certificates and SslPolicy. New endpoints: AddListenerCertificates, RemoveListenerCertificates, DescribeListenerCertificates, DescribeSSLPolicies (gateway ops, NATS subjects, daemon handlers, service-interfaces.yaml). No TLS termination yet. * test(elbv2): cover listener-cert gateway ops + handler error paths |
||
|
|
bbff004b45 |
feat(eks): 6b create-cluster → ACTIVE (single-node SQLite + mgmt-IP NLB stop-gap) (#280)
* feat(eks): cluster_state KV helpers — ClusterMeta + status CAS + prefix sweep
Stage 1 of Sprint 6b CreateCluster lifecycle. Introduces the first
read/write helpers against the eks-account-{accountID} bucket so later
stages (oidc keypair, NLB wiring, cluster reconciler) can persist state
without re-inventing CAS/sweep logic.
- ClusterMeta struct holds the DescribeCluster source of truth (status,
endpoint, OIDC issuer, CA, VPC config, control-plane VM + ENI, NLB ARN).
- ClusterStatus enum mirrors the AWS EKS string set verbatim.
- PutClusterMeta is unconditional (CreateCluster path); GetClusterMeta
returns ErrClusterNotFound on key absence so the service boundary can
map to ResourceNotFoundException.
- SetClusterStatus is revision-checked with bounded CAS retries so the
reconciler and CreateCluster body cannot wedge each other.
- DeleteClusterPrefix sweeps every clusters/{name}/* key; called from
DeleteCluster after the OIDC private key is zeroized.
Tests cover round-trip, missing/corrupt blob, idempotent status set,
DELETING transition from every prior state, concurrent revision bump
recovery, sibling-cluster preservation, empty-bucket no-op.
Plan: docs/development/feature/eks-6b-create-cluster.md
* feat(eks): per-cluster ECDSA-P256 OIDC keypair generation + zeroize
Stage 2 of Sprint 6b. Lays the at-rest primitives the CreateCluster body
will call before launching the K3s server VM. Pure crypto helpers — no
daemon, no launcher, no NATS bus.
- GenerateClusterOIDCKeypair mints an ECDSA-P256 keypair per cluster
(matches eks-v1.md Q8), PEM-marshals the private as PKCS8,
envelope-encrypts it via handlers_iam.EncryptSecret (AES-256-GCM,
AwsGW masterKey), writes the ciphertext to OIDCSigningKeyKey, and
publishes the public JWKS document to OIDCJWKSKey. Returns the JWKS
bytes so CreateCluster can hand them straight to user-data without
a follow-up KV read.
- JWKS shape mirrors what handlers_sts.JWKS already decodes for IRSA
verification (RFC 7517 EC P-256, kid = base64url SHA-256 of SPKI DER,
alg ES256, use sig). Types are defined locally to avoid a
handlers_eks → handlers_sts → handlers_eks import cycle; JSON wire
compat is the contract.
- LoadClusterOIDCPrivateKey is the recovery-path read for future
stages (reconciler restart, restore-snapshot tooling). Validates
PKCS8 parses to an ECDSA P-256 *PrivateKey and rejects anything else.
- ZeroizeClusterOIDCKey overwrites the encrypted blob with zeros of
the same length, then deletes. DeleteCluster calls this *first* so a
crash mid-teardown leaves no recoverable key material. Idempotent
on missing key — supports retry semantics.
Tests cover JWKS shape (RFC 7517 EC P-256 with 32-byte SEC1
coordinates), per-cluster key uniqueness, round-trip private-key
decrypt matches JWKS public, wrong-master-key fails, missing-key
ErrClusterNotFound mapping, zeroize idempotency, JWKS survives
zeroize (carries no secret material), padCoord left-pad correctness.
Plan: docs/development/feature/eks-6b-create-cluster.md
* feat(eks): cluster SG + NLB ensure/delete helpers
Stage 3 of Sprint 6b CreateCluster lifecycle. Adds the customer-account
SG provisioning + per-cluster NLB plumbing the CreateCluster body will
call between OIDC keypair (Stage 2) and K3s VM launch (Stage 4). Pure
interface-call helpers — no daemon, no KV, no NATS bus.
- EnsureClusterSGs creates (or returns) eks-cluster-{name}-control-plane-sg
and eks-cluster-{name}-nodegroup-sg in the customer VPC, tagged
spinifex:managed-by=eks + spinifex:eks-cluster=<name> + spinifex:eks-role.
Idempotent on group-name + vpc-id pair. DeleteClusterSGs is best-effort
with continue-on-error sweep.
- EnsureClusterNLB provisions the eks-<name> NLB (internal scheme), the
eks-<name>-cp target group (TCP, port 6443, target-type ip per eks-v1.md
Q13), and the TCP :443 forward listener. Returns ClusterNLB
{LoadBalancerArn, TargetGroupArn, ListenerArn, DNSName} so CreateCluster
can persist every ARN in ClusterMeta. Idempotent on cluster name;
resource-name length validated against the AWS 32-char cap so a too-long
cluster name fails with a clear error instead of a generic
InvalidParameterValue downstream.
- RegisterClusterTarget + DeregisterClusterTarget bind the K3s server VM
ENI IP to the TG on :6443. Stage 4 calls Register once the VM has its
ENI IP; DeleteCluster calls Deregister before VM termination.
- DeleteClusterNLB tears down NLB + TG. DeleteLoadBalancer already
cascades listener cleanup inside the ELBv2 impl. LB sweep error takes
precedence so DeleteCluster surfaces the more visible failure first
while the TG sweep still runs.
Narrow sgProvisioner + nlbProvisioner interfaces in-package give tests
hand-rolled recorder fakes; the production VPCService + ELBv2Service
satisfy them structurally so no adapter is needed at Stage 6 wire-up.
Tests cover fresh-creates with right names/tags/ports, idempotent reuse,
mixed-existence (control-plane exists, nodegroup created), name-too-long
rejection, empty-inputs rejection, missing-resource no-op on delete, and
first-error-surfaced sweep continuation on delete.
Plan: docs/development/feature/eks-6b-create-cluster.md
* feat(ec2): RunInstances honors pre-created NetworkInterfaceId
Closes the AWS-parity gap where RunInstances always auto-created a
primary ENI even when the caller specified
NetworkInterfaces[0].NetworkInterfaceId. The EKS Sprint 6b CreateCluster
launcher (Stage 4) needs to pre-create the K3s server VM's ENI so the
ENI can be tagged/owned distinctly from the launcher's instance; the
existing path silently created a second ENI and left the caller's
unattached.
Changes:
- ENICreator interface gains GetENI(accountID, eniID) → *ENIInfo. ENIInfo
is a translated struct (SubnetID, VpcID, PrivateIp, MAC, Status,
SecurityGroupIDs) so handlers/ec2/instance stays free of an import on
handlers/ec2/vpc.ENIRecord.
- daemon/vpc_adapter.go gains daemonENICreator.GetENI translating
VPCServiceImpl.GetENIRecord into ENIInfo.
- PrepareRunInstances inspects NetworkInterfaces[0].NetworkInterfaceId
after the existing subnet/SG lift block. If non-empty, the loop
dispatches to a new attachPreCreatedENI helper:
- Looks up the ENI under accountID.
- Rejects with InvalidNetworkInterface.InUse if Status == "in-use".
- Calls AttachENI to mark the ENI in-use and bind it to the new VM.
- Populates instance.ENIId / ENIMac and ec2.Instance network fields
(PrivateIp, SubnetId, VpcId, SecurityGroups, NetworkInterfaces[0]).
- Skips the auto-create + public-IP-auto-assign paths; the caller is
expected to manage public IP out-of-band.
- Auto-create path is left untouched for every other code path: callers
that pass SubnetId without a NetworkInterfaceId still get a fresh ENI
with the existing semantics.
Tests cover the four shapes: happy path (auto-create not called, attach
called once, instance populated), in-use ENI rejected with the canonical
AWS error code, lookup error surfaces verbatim, attach error surfaces
verbatim.
* feat(eks): K3s control-plane VM launcher with cloud-init user-data
LaunchK3sServerVM resolves the eks-server AMI by name, pre-creates the
customer-account control-plane ENI in the supplied subnet with the
control-plane SG, renders cloud-init user-data (first-boot env, OIDC
private key, K3s config.yaml with tls-san + service-account args), then
calls RunInstances with NetworkInterfaces[0].NetworkInterfaceId pointing
at the pre-created ENI. RunInstances failures roll back the ENI so the
caller does not leak a customer-account resource.
TerminateK3sServerVM tears the instance and ENI down for DeleteCluster;
missing identifiers are a no-op so retries stay safe.
ENI + VM both live in the customer account (deviation from plan §7 which
specified a SystemAccount-owned VM); cross-account ENI attach is deferred
to a follow-up bead.
* feat(eks): NATS bootstrap subscriber + cluster reconciler + leader lease
NATSBootstrap subscribes to the four single-shot subjects the K3s server VM
publishes (k3s-bootstrap-token, k3s-admin-kubeconfig, k3s-oidc-jwks, k3s-ca)
on eks.bus.{accountID}.{clusterName}.{kind}. Token and admin kubeconfig are
envelope-encrypted via handlers_iam.EncryptSecret and persisted to KV. The
published JWKS is cross-checked against the controller-generated keypair by
kid and rejected on mismatch. The CA lands on meta.CertificateAuthorityB64
via the new SetClusterCertificateAuthority CAS updater.
ClusterReconciler holds a leader lease on spinifex-eks-leader/{accountID}/
{clusterName} (60s TTL, 20s refresh) and drives:
- CREATING -> ACTIVE once the four bootstrap KV artifacts are present and
NLB /healthz responds 200.
- ACTIVE -> probe /healthz on the configured interval, warn on failure.
- DELETING / FAILED -> exit (DeleteCluster body owns teardown).
Loss of the leader lease (CAS conflict or different holderID) returns
ErrReconcilerLeaseLost so the caller can re-acquire from another node.
Tested with embedded JetStream + a stub HTTPDoer; race-clean.
* feat(eks): OIDC issuer URL builder + reconciler registry
ClusterOIDCIssuer renders the {gatewayBaseURL}/oidc/eks/{region}/{accountID}/
{clusterName} URL kube-apiserver advertises as service-account-issuer and
that STS / external IRSA validators dereference. Validates URL safety on the
path segments so a hostile cluster name cannot inject scheme/path fragments.
ReconcilerRegistry tracks per-(accountID, clusterName) reconciler goroutines
with idempotent Spawn (drop duplicate spawn calls), Stop (cancel one + wait
for release), and StopAll (graceful daemon shutdown). RunClusterReconciler
is the production SpawnReconcilerFn: constructs a ClusterReconciler,
acquires the leader lease, runs Run in a goroutine. Both helpers consumed
by the upcoming service_impl CreateCluster body.
* feat(eks): CreateCluster/DescribeCluster/ListClusters/DeleteCluster bodies
Wires the EKSServiceImpl orchestration deps (VPC SG/K3s/Subnet, NLB,
Instance launcher, AMI resolver, master key, gateway base URL, region,
holder ID, reconciler registry) and implements the four lifecycle
methods:
- CreateCluster: validate → CREATING meta → SGs → NLB → OIDC issuer +
keypair → K3s server VM (cloud-init carries OIDC private PEM) →
register ENI IP with NLB target group → spawn NATS bootstrap
subscriber + per-cluster reconciler via the registry.
- DescribeCluster: KV meta read → eks.Cluster shape, with
ErrClusterNotFound translated to ResourceNotFoundException at the
service boundary.
- ListClusters: KV key-prefix scan + sorted maxResults/nextToken
pagination.
- DeleteCluster: SetClusterStatus DELETING → zeroize OIDC key → stop
reconciler via registry → deregister NLB target + delete NLB →
terminate K3s VM → delete SGs → delete cluster KV prefix.
Daemon wiring:
- buildEKSServiceDeps loads MasterKey from BaseDir/config/master.key
best-effort, resolves the gateway base URL via the same precedence the
LB-agent block applies later, and passes VPCService + ELBv2Service +
ImageService directly while adapting InstanceServiceImpl via
daemonEKSInstanceLauncher (RunInstances passes through; Terminate-
Instances loops vmMgr.Get + StopOrTerminateInstance in-process).
- SubnetVPCResolver is satisfied by daemonEKSSubnetResolver wrapping
VPCService.GetSubnet.
- Startup spawns reconcilers for every persisted CREATING/ACTIVE cluster
via SpawnRegisteredReconcilers; shutdown calls eksService.Shutdown so
every per-cluster goroutine releases its lease before exit.
Test surface:
- service_impl_test: TestEKSServiceImpl_ClusterLifecycleShimMode covers
the four lifecycle methods when constructed via the back-compat shim
(CreateCluster/DeleteCluster → ServerInternal, DescribeCluster →
ResourceNotFound on empty bucket, ListClusters → empty list).
- daemon_handlers_eks_test: drop the four lifecycle subjects from the
NotImplemented routing loop; handler-side coverage now lives in
handlers/eks.
- awserrors: add ResourceNotFound (404) for the DescribeCluster path.
* fix(eks): load master.key from config dir not service data dir
eks_deps read master.key from {BaseDir}/config/master.key, but the daemon
systemd unit sets SPINIFEX_BASE_DIR=/var/lib/spinifex/spinifex (a per-service
data dir). admin init writes master.key to the shared config dir, so the
daemon looked in a path that only resolved when a {BaseDir}/config symlink
existed — awsgw has one, the daemon's nested data dir does not. Result:
MasterKey=nil, depsReadyForOrchestration() false, CreateCluster/DeleteCluster
collapse to ServerInternal.
master.key is a config artifact written next to spinifex.toml. Read it from
filepath.Dir(d.configPath), matching how the daemon already resolves
predastore.toml and TLS certs. No symlink or provisioning change needed.
* fix(eks): register + resolve eks-server AMI via tag, add import --ami-name
CreateCluster's K3s launch failed 'eks: AMI eks-server not found' on every
clean install. Two defects:
1. spx admin images import had no way to name a locally-built appliance AMI:
--name is a catalog lookup (rejects non-catalog values) and the AMI name
was hardcoded to ami-{distro}-{version}-{arch}. build-system-image.sh ran
'import --name $AMI_NAME' for eks-server/eks-agent/GPU images, which all
failed 'Image name not found in available images'.
2. The launcher resolved DescribeImages by name=='eks-server', a name the
pipeline could never produce.
Add --ami-name to override the registered AMI name on --file imports (fixes
eks-server, eks-agent, and GPU appliance imports). Resolve the EKS AMI by the
spinifex:managed-by=eks tag the pipeline already stamps, newest-wins on
multiple matches. Tag-based resolution survives the planned server+agent →
single EKS AMI unify untouched (role chosen per-instance at launch).
build-system-image.sh now passes --ami-name; Makefile gains
import-eks-server-image (IMPORT=1) and dev-install.sh imports the AMI when
DEV_INSTALL_EKS=1.
* fix(eks): give K3s server VM NATS token + CA so bootstrap can publish
The K3s control-plane VM was launched with NATSURL=ConnectedUrl() (loopback,
unreachable from the VM) and no auth material, while spinifex NATS requires a
shared token over TLS. The VM could not connect to NATS, so it never published
its four one-shot bootstrap messages and the cluster stayed in CREATING.
Hand the VM the daemon's shared NATS token + CA PEM and a VM-reachable NATS URL
(mirrors the ELBv2 LB-agent shared-credential pattern; spinifex has no nkeys
hierarchy to scope per-principal creds against).
- K3sServerInput: drop dead NATSCredsContent; add NATSToken + NATSCACert,
required by validateK3sServerInput. user-data writes SPINIFEX_NATS_TOKEN,
SPINIFEX_NATS_CA + the CA file; first-boot.env tightened to 0600.
- EKSServiceDeps + CreateCluster: pass NATSURL/NATSToken/NATSCACert.
- buildEKSServiceDeps: read CA from config.NATS.CACert; extract
resolveGatewayHost(); add resolveNATSURL() (same reachable host, NATS port).
- k3s-first-boot.sh: build nats CLI args with --token + --tlsca.
* fix(eks): retry transient bootstrap persists, fail cluster on permanent error
The NATS bootstrap subscriber tore down on the first persist error and the
cluster sat in CREATING forever. Core-NATS delivers a publication once, so a
transient KV blip during persist was unrecoverable and a malformed payload
(bad JWKS kid, non-base64 CA, empty envelope) silently stranded the cluster.
- persistWithRetry rides out transient persist failures in-handler (3 attempts,
200ms backoff) instead of unsubscribing and waiting for a re-publish that
never comes.
- errBootstrapPermanent classifies validation failures (malformed envelope,
empty fields, JWKS kid mismatch, non-base64 CA) as non-retryable so they
return immediately.
- spawnBootstrap marks the cluster FAILED (with reason) when Run exits with a
non-context error, so DescribeCluster surfaces the cause and DeleteCluster
can reclaim resources.
- MarkClusterFailed transitions to FAILED+StatusReason only from CREATING, so a
late bootstrap error cannot clobber a concurrent delete or healthy cluster.
- casUpdateMeta extracts the revision-checked RMW loop shared by the meta
setters.
* fix(eks): resume NATS bootstrap for CREATING clusters on daemon restart
SpawnRegisteredReconcilers re-spawned only the reconciler on boot, never the
bootstrap subscriber. A daemon restart while a cluster was CREATING (K3s VM
still booting) lost the subscriptions; the VM published its four one-shot
core-NATS messages to nobody and the cluster hung in CREATING forever.
- BootstrapPendingKinds reports which artifacts (token, kubeconfig, CA) are
still missing from KV. JWKS is excluded: it persists no artifact and
re-waiting on a once-published subject would deadlock — its cross-check is
best-effort across a restart.
- RunForKinds subscribes to a subject subset (nil = all four); Run delegates to
it. subjectHandlers maps each kind to its handler and rejects unknown kinds.
- SpawnRegisteredReconcilers resumes bootstrap for CREATING clusters, waiting
only on the missing artifacts, so a mid-CREATING restart still drives the
cluster to ACTIVE.
* fix(eks): DeleteCluster must not wipe KV or report success on teardown failure
DeleteCluster ran zeroize, NLB teardown, and VM termination as warn-and-continue
then unconditionally swept the per-cluster KV prefix and returned success. A
failed NLB or VM teardown leaked billable resources with no owning record (the
meta holding their ARNs/IDs was erased) and told the operator the cluster was
gone. Zeroize — a stated security guarantee of no recoverable key material —
had its failure ignored too.
- Zeroize, NLB delete, and VM terminate are now blocking: their failures
accumulate (errors.Join) and abort the call before the KV sweep, leaving the
cluster DELETING and returning the error so a retry can still reach the ARNs.
- KV prefix sweep runs only after NLB + VM teardown confirm.
- SG cleanup stays best-effort (plan step 5) — a leaked SG strands no owning
record — and target deregister stays best-effort since DeleteClusterNLB tears
down the whole NLB + target group.
* fix(eks): persist NLB + control-plane ARNs mid-CreateCluster so failures don't leak
CreateCluster set meta.NLBArn / target-group / instance / ENI in memory and
persisted them only in the final PutClusterMeta, after LaunchK3sServerVM. Any
failure between EnsureClusterNLB and that persist left the NLB + target group +
listener + backing LB VM created but unrecorded; markFailed did no rollback and
DeleteCluster keys teardown off the persisted ARNs, so the resources leaked.
- Persist meta immediately after EnsureClusterNLB (records NLB/TG ARNs) and
again after LaunchK3sServerVM (records instance + ENI), before the next
fallible step.
- A failed create now leaves a FAILED meta carrying the resource references, so
the DeleteCluster teardown path (165.9) reclaims every NLB/TG/listener/VM.
- Tests: NLB-arn-persisted-before-later-failure, failed-create-then-delete
reclaims NLB, and a CreateCluster happy path. Shared eksServiceFixture
extracted from the delete-cluster fixture.
* fix(sts): parse path-based EKS OIDC issuer so IRSA accepts 6b cluster tokens
ParseEKSIssuerURL required an `oidc.eks.` host prefix and an exactly-2-segment
path (AWS hosted-EKS shape), but ClusterOIDCIssuer emits — and awsgw serves —
https://{gateway-host}/oidc/eks/{region}/{accountID}/{clusterName}. Every 6b
cluster's iss claim was rejected with "issuer host must start with oidc.eks",
so AssumeRoleWithWebIdentity would fail all IRSA tokens once 6e serves them.
Reconcile on the path-based shape, which is what is actually reachable on
bare-metal/on-prem (no oidc.eks.* vhost or DNS exists). Host trust comes from
the IAM OIDC-provider registration (the full issuer string is the registry key)
and the JWKS signature check, not a host pattern — so the parser drops the host
prefix rule and instead pins accountID/clusterName at fixed positions in a
strict 5-segment /oidc/eks/{region}/{accountID}/{clusterName} path. A crafted
iss with extra or missing segments still fails closed.
Adds a cross-component round-trip test asserting a ClusterOIDCIssuer-built iss
parses back to the same (accountID, clusterName).
* fix(eks): reclaim FAILED clusters on retry + cluster-scoped exists/not-found errors
A failed CreateCluster left a FAILED ClusterMeta that poisoned retries: the
preflight returned ErrorELBv2TargetGroupInUse ("The target group is currently
in use...") for a cluster-exists condition, and because that maps to a
retryable code the SDK's auto-retry surfaced ResourceInUseException instead of
the original failure. FAILED clusters were also a dead state — visible to
neither a clean recreate nor a clear lifecycle.
- Add EKS-specific ResourceInUseException / ResourceNotFoundException codes with
cluster-scoped messages; CreateCluster's exists check and DescribeCluster /
DeleteCluster not-found now use them instead of the ELBv2 / EC2 codes.
- CreateCluster reclaims a FAILED cluster: it purges the prior attempt's
recorded resources and clears KV before recreating, so a retry (including the
SDK auto-retry) is idempotent rather than blocked. Teardown failure surfaces
rather than recreating on top of orphans.
- Extract purgeClusterInfra, shared by DeleteCluster and the reclaim path
(blocking zeroize/NLB/VM, best-effort SG, KV sweep only after teardown
confirms).
- FAILED clusters remain observable via Describe (status=FAILED) and List — no
state where create blocks but describe/list show nothing.
* fix(eks): fail CREATING clusters that exceed create timeout
A per-cluster reconciler that never observes a ready control plane (bootstrap
artifacts missing or healthz unreachable) previously stayed CREATING forever.
Add a configurable create timeout (default 15m): once meta.CreatedAt is older
than the deadline and the cluster is still not ready, the reconciler marks it
FAILED with a reason, exiting the loop. Within the deadline it stays CREATING.
* fix(eks): surface ACTIVE cluster health in describe-cluster
The reconciler only slog.Warn'd a failing /healthz probe for an ACTIVE
cluster, so describe-cluster kept reporting a dead control plane as healthy.
Add HealthIssue + LastHealthProbe to ClusterMeta, write them from the ACTIVE
reconcile branch (recording the failure reason, clearing on recovery), and map
a non-empty HealthIssue to a ClusterHealth ClusterUnreachable issue in
describe-cluster.
* test(eks): add plan-mandated CreateCluster/DeleteCluster service cases
service_impl_test only covered shim mode. Add the four cases the plan's Testing
section requires: CONFIG_MAP auth mode rejected (and API accepted), missing
subnetIds rejected, DescribeCluster 404 with full deps wired (past the shim
short-circuit), and the DeleteCluster zeroize-before-teardown ordering proven
by forcing a teardown failure and asserting the OIDC key is already wiped.
* refactor(eks): return OIDC private-key PEM from generator, drop re-read
CreateCluster discarded GenerateClusterOIDCKeypair's return then did a second
KV get + AES-GCM decrypt via loadOIDCPrivateKeyPEM to re-derive the same PEM the
generator already had in hand. Return the plaintext PEM (alongside the JWKS)
from the generator and use it directly; remove loadOIDCPrivateKeyPEM and the now
-unused crypto/x509 + encoding/pem imports in service_impl.
* fix(eks): make CreateCluster/DeleteCluster failures diagnosable
Unmet orchestration deps and a missing eks-server AMI both collapsed to a bare
ServerInternal with no request-time log, leaving the client a 'contact AWS
re:Post' message and zero cause. Enumerate the specific missing deps and log
them at ERROR, returning ServiceUnavailable (an operator/config fault, not an
unrecoverable internal one). Add an ErrEKSServerAMINotFound sentinel so a
missing control-plane AMI is logged and surfaced as ServiceUnavailable too.
* fix(eks): serialize REST-JSON responses via jsonutil so the AWS SDK can parse them
WriteJSONResponse marshaled aws-sdk-go *Output structs with encoding/json,
which ignores their locationName tags and emits Go PascalCase keys. EKS
clients parse restjson on the lowercase locationName, so every field of
list/describe/create/delete responses was silently dropped (empty endpoint,
null identity.oidc.issuer). Marshal via jsonutil.BuildJSON — the same
marshaler the SDK uses for restjson bodies — which honors locationName.
Adds wire-shape tests asserting lowercase keys present and PascalCase absent.
* feat(eks): launch K3s server VM via boot-agnostic system-instance path
The EKS K3s control-plane VM launched through the plain tenant RunInstances
path, so it had only its VPC ENI and no management-bridge NIC -- no route to
the daemon's NATS endpoint. It never published its bootstrap subjects and the
cluster stayed CREATING.
Generalize LaunchSystemInstance to a boot-agnostic contract supporting both
system-instance boot styles:
- BootDirect: direct-boot microVM (fw_cfg blobs) -- ELBv2 LB agent
- BootAMI: AMI boot + cloud-init seed (q35) -- EKS K3s server VM
The launch contract moves to a neutral handlers/sysinstance package so EKS can
launch system VMs without an eks->elbv2 dependency; handlers/elbv2 re-exports
the types as aliases. The BootAMI branch mirrors the daemon's
Prepare -> Insert -> Launch split and allocates the mgmt NIC on the VM record
between Insert and Launch, so the cloud-init network-config renders a static
mgmt0 interface. The EKS NATSURL is now sourced from the mgmt-bridge IP (the
on-link address the VM's mgmt NIC can reach), falling back to the gateway-host
resolution when no mgmt bridge exists.
Verified: K3s VM now boots from the eks-server AMI under q35 with a mgmt0 NIC
(10.15.8.x), reachable from the host over br-mgmt, with NATS on-link at the
bridge IP. No AMI rebuild required.
* fix(eks): make DeleteCluster K3s teardown idempotent so it can't wedge
TerminateK3sServerVM treated every DeleteNetworkInterface error -- and every
TerminateSystemInstance error -- as blocking. Because instance terminate is
async, the immediate ENI delete races it: the first attempt fails
InvalidNetworkInterface.InUse, and once the instance-terminate cascade releases
the ENI every retry fails InvalidNetworkInterfaceID.NotFound. Both joined into
purgeClusterInfra's teardownErrs gate, which short-circuits before the SG sweep
and KV prefix delete -- so once the ENI was gone the cluster was stuck DELETING
permanently, leaking its control-plane + nodegroup SGs and KV meta.
Classify the teardown errors instead of blanket-propagating:
- ENI InvalidNetworkInterfaceID.NotFound / InvalidNetworkInterface.NotFound
-> idempotent success (the cascade or a prior retry already deleted it);
must not block the downstream SG + KV sweep.
- ENI InvalidNetworkInterface.InUse (VM still terminating) and any other
error -> retryable; surfaced so the cluster stays DELETING and the
reconciler retries once the instance releases the ENI.
- Instance already gone -> idempotent. TerminateSystemInstance now returns a
sysinstance.ErrSystemInstanceNotFound sentinel that the EKS path tolerates
via errors.Is, so a retried delete after the VM drained completes.
Add awserrors.IsErrorCode(err, code) as the shared delete-error classifier
(substring match through %w wrapping) and route the lone ad-hoc strings.Contains
guard in daemon/vm_adapters.go through it.
Verified: delete-cluster on three clusters left wedged DELETING / FAILED (ENI +
VM already gone) all complete -- clusters erased, SGs + ENIs reclaimed, no leak.
* fix(eks): gate ACTIVE on verified JWKS cross-check, not pre-seeded key
The controller pre-seeds OIDCJWKSKey at create time, so its presence
proved nothing about the running cluster's actual signing key. A cluster
could reach ACTIVE — and start signing IRSA tokens — before the K3s VM's
published JWKS was ever cross-checked against the generated keypair.
Add an OIDCJWKSVerifiedKey marker written by persistJWKS only after
assertJWKSMatch passes. The reconciler's ACTIVE gate and
BootstrapPendingKinds now key off the marker, not the pre-seeded JWKS.
assertJWKSMatch is map-based and also rejects kty mismatch.
* fix(eks): collapse duplicated gateway-host switch into resolveGatewayHost
startCluster reimplemented the 5-case gateway-host precedence inline,
duplicating resolveGatewayHost (used by the OIDC issuer and EKS NATS URL).
If one drifted, the OIDC issuer host embedded in cluster tokens would
diverge from the host LB VMs use to reach the daemon -> IRSA iss mismatch
and cluster-wide token rejection.
startCluster now calls resolveGatewayHost and derives the multi-node
mgmt host route (mgmtRouteVia) from the resolved host. resolveGatewayHost
carries the canonical case enumeration; a table test pins all five
branches plus the case-1 skip guards and the no-host fallthrough.
* fix(eks): make eks-server VM boot diagnosable + fix first-boot publisher crash
Three issues found while diagnosing why K3s clusters never leave CREATING:
1. setup.sh: the stock Alpine cloud image lists `console=tty0` LAST in
default_kernel_opts, so Linux binds /dev/console to the framebuffer and the
host-side serial capture sees only kernel dmesg — OpenRC, cloud-init, and
k3s-first-boot output were invisible. Reorder so console=ttyS0 is last.
2. k3s-first-boot.initd: command_background was "no" while output_log/error_log
were set; start-stop-daemon rejects --stdout/--stderr without --background
("Failed to start k3s-first-boot"), so the bootstrap publisher never ran and
the cluster wedged in CREATING. Background it (the publisher waits up to 5min
on K3s /healthz and must not block the runlevel anyway) and route its output
to the serial console, which the orchestrator captures (the VM has no login).
3. k3s-first-boot.sh: on the /healthz timeout, dump the tail of /var/log/k3s.log
to the console so the apiserver's own failure is visible host-side.
* fix(eks): seed K3s apiserver a public OIDC key, not the private key
buildK3sUserData pointed both kube-apiserver service-account args at the same
private-key file. --service-account-key-file requires a PUBLIC key — k8s
ParsePublicKeysPEM rejects a private-key PEM with "data does not contain any
valid RSA or ECDSA public keys" and the apiserver crash-loops, so K3s never
reaches /healthz, first-boot never publishes, and the cluster wedges in
CREATING.
Add PublicKeyPEMFromPrivate (PKIX public PEM from the PKCS#8 private key) and
seed it to a separate /etc/rancher/k3s/oidc-signing-key.pub.pem. Point
service-account-key-file at the public file; keep service-account-signing-key-file
on the private file. Runtime-verified: the apiserver crash is gone and K3s now
starts and schedules system pods.
* feat(eks): egress-only SNAT + DNS for K3s control-plane VM
The K3s server VM (a hidden system instance) could not pull system-pod
images: it had no internet egress and no working resolver, so
coredns/traefik/metrics-server never started and /healthz timed out.
Egress: give the single control-plane VM egress-only internet, confined
to its /32, with no inbound exposure and no customer-visible resource.
- policy/route.go: SystemInstanceEgressPriority (1200, above the drop
gate) + AddSystemInstanceEgress/Delete — a reroute scoped to
ip4.src == <vmIP>/32 so peers in a shared subnet are unaffected.
- policy/nat.go: AddSystemInstanceSNAT/Delete — a plain snat (not
dnat_and_snat, so no inbound path), idempotent on its pool external IP.
- external/igw.go: L5 EnsureSystemInstanceEgress/Remove resolves the
gateway nexthop and attaches the reroute + snat (mirrors the NAT-GW
egress path; ADR-0006 S1/S4/S5 preserved).
- subscribers: vpc.add/delete-system-egress topics + handlers.
- handlers/eks: allocate a hidden (ManagedBy=eks) pool address on
CreateCluster and publish add; release + publish delete on teardown.
EIP dep wired through EKSServiceDeps + the daemon.
DNS: the eks-server AMI is Alpine/dhcpcd and /etc/resolv.conf is a
dangling symlink, so DHCP-supplied resolvers are never persisted. Write
a real /etc/resolv.conf from the K3s VM cloud-config via bootcmd (not
write_files — a write_files entry follows the dead symlink, fails, and
aborts the whole block, dropping first-boot.env + the k3s config).
* feat: EKS 6b single-node SQLite pivot + bootstrap-envelope fixes
Switch the K3s server VM from embedded etcd to embedded SQLite (kine)
for the single-node v1, so CreateCluster reaches a healthy control plane.
etcd's per-commit fsync stalls past the apiserver deadline on viperblock's
deferred-durability block path; SQLite tolerates it.
- k3s server: omit cluster-init (selects SQLite/kine), add anonymous-auth=true
so the reconciler's anonymous /healthz NLB probe returns 200.
- first-boot readiness gate: anonymous /healthz (401 under CIS anonymous-auth
=false) -> authenticated 'kubectl get --raw /readyz'.
- first-boot publish: emit a JSON BootstrapEnvelope for all four subjects
(token, admin-kubeconfig, oidc-jwks, ca); jwks and ca were absent before.
- nats pub: --stdin -> --force-stdin (nats CLI 0.4.0).
- manifest: add jq (builds the bootstrap-message JSON envelopes).
* feat: EKS reconciler probes control-plane mgmt IP until DNS lands
The host-side cluster reconciler cannot resolve or route to the VPC-internal
NLB DNS endpoint (no authoritative DNS / Eclipso yet), so CreateCluster timed
out in CREATING and was marked FAILED even though the control plane was healthy.
Probe the control-plane VM's br-mgmt NIC address directly — the one apiserver
endpoint reachable from the daemon. Capture the mgmt IP from the launch result
(SystemInstanceOutput.MgmtIP -> K3sServerOutput.MgmtIP -> ClusterMeta.
ControlPlaneMgmtIP) and build the reconciler health URL as
https://<mgmtIP>:6443/healthz. The probe client already sets InsecureSkipVerify,
so the mgmt IP need not be in the apiserver cert SAN.
Workaround until authoritative DNS + a host-reachable NLB front-end land, at
which point spawnReconciler reverts to the NLB endpoint.
|
||
|
|
7b9eb9cc4e |
feat(elbv2): tag/SG/IP-type setters, ip targets, redirect actions, delete-cascade hardening (#282)
* fix(elbv2): make resource deletes idempotent on absent
DeleteLoadBalancer, DeleteTargetGroup, DeleteListener and DeleteRule
returned a NotFound error when the target was already gone. Real AWS
ELBv2 deletes are idempotent (success on absent), so terraform-provider-aws
fails a destroy step on retry. Return an empty success output instead,
mirroring the KV store which already tolerates absent keys.
Adds TestRLC1_ELBv2DeleteIdempotentOnAbsent enforcing the contract across
the ELBv2 delete surface; converts the three _NotFound delete tests to
assert idempotent success.
Refs mulga-siv-172 (idempotency clause; cascade-composition clause stays open).
* feat: ELBv2 AddTags/RemoveTags, SetSecurityGroups, SetIpAddressType
- O(1) GetListenerByArn via ARN-parse short-ID lookup + ARN equality check
- persist tags on listeners/rules; seed at create time
- AddTags/RemoveTags across LB, target group, listener, rule resources
- SetSecurityGroups (ALB only) reattaches SGs to each ENI via live port update
- SetIpAddressType (ipv4-only, idempotent)
- wire all four ops through gateway, NATS client, and daemon handlers
- frontend: editable Tags tab on LB/TG detail, Security groups tab on LB detail
* feat(elbv2): redirect listener/rule actions
- add redirect action type; parse/store/read-back RedirectActionConfig
- validate status code + render-safe fields (strip AWS #{...} placeholders)
- accept redirect and fixed-response on listener rules (not just defaults)
- HAProxy: http-request redirect scheme (HTTP->HTTPS fast path) or rebuilt
location with placeholder->fetch translation; synthetic per-rule backends
* refactor: dedupe ELBv2 attribute handlers and harden store lookups
Collapse the four near-identical Modify/Describe attribute handlers (target
group and load balancer mirror pairs) onto generic helpers in both the service
impl and the gateway, and table-drive the mirror-pair tests over a TG/LB kind.
- service_impl_attributes.go: generic attrResource/modifyResourceAttributes/
describeResourceAttributes plus sdkAttrsIn/sdkAttrsOut; folds in nil-input
guards, explicit empty-accountID rejection, fresh-string output, and a
no-op skip-persist for unchanged Modify calls.
- gateway/elbv2/attributes.go: generic attributeOp + shared validateAttrArn,
replacing four per-endpoint files.
- types.go: default attribute sets are now package-level maps cloned on access
instead of rebuilt per Describe; document the access_logs empty-string defaults.
- store.go: drop the stale 'linear scan' docstring and log ARN mismatches on
GetLoadBalancerByArn/GetTargetGroupByArn instead of silently returning nil.
Adds cross-account isolation, wrong-account no-mutation, and empty-string-clear
coverage.
* feat: support ip target type in ELBv2 RegisterTargets
RegisterTargets resolved every target ID through an instance ENI lookup, so
ip-type target groups silently no-op'd with an empty PrivateIP and never
received traffic.
- types.go: add TargetTypeInstance/TargetTypeIP constants.
- CreateTargetGroup: default to instance; reject target types other than
instance/ip with InvalidParameterValue.
- RegisterTargets: resolve PrivateIP per target type via resolveRegisteredTargetIP
— ip targets use the supplied IP directly (validated with net.ParseIP),
instance targets keep the ENI lookup and reject IP-shaped IDs.
* fix: cascade rule deletion in ELBv2 DeleteLoadBalancer
DeleteLoadBalancer deleted listeners via store.DeleteListener directly, which
bypasses the rule cascade in the DeleteListener handler. Orphan rules survived
LB teardown and pinned their target groups as ResourceInUse, leaving the TG
permanently undeletable and breaking tofu destroy on multinode.
Extract deleteListenerCascade (rules then listener record) and call it from both
DeleteListener and the DeleteLoadBalancer cleanup loop.
Regression: TestDeleteLoadBalancer_CascadesRuleDeletion.
* fix: harden ELBv2 delete lifecycle against orphaned cascades
- DeleteLoadBalancer: fail-fast on listener-list/cascade error and run the
rule cascade before backing-artifact teardown, so a transient failure
leaves the record intact for retry instead of orphaning rules.
- DeleteTargetGroup: in-use scan skips listeners/rules whose owning load
balancer no longer exists, so a partial-teardown orphan can't pin a target
group as ResourceInUse permanently.
- DeleteRule: propagate the listener lookup error instead of discarding it,
so a failed HAProxy reload is no longer reported as success.
- Add lifecycle invariants for no-orphan completeness, no store-bypass
cascade, and target-group deletability after LB teardown.
- Drop plan/bead references from comments.
* test: add NATS e2e round-trips for new ELBv2 ops
Cover the client→NATS→handler marshalling path that unit tests skip:
tags lifecycle (AddTags/DescribeTags/RemoveTags), target-group and
load-balancer attribute round-trips, redirect-action default listener,
ip target-type registration, and SetSecurityGroups/SetIpAddressType.
* fix(elbv2): correct misleading InvalidConfigurationRequest message
The error code is shared by five unrelated conditions (dualstack
rejection in SetIpAddressType, cross-listener rule moves, unsupported
rule action types, and the two genuine NLB security-group cases), but
its canonical message claimed security groups were unsupported on
network load balancers. Callers hitting it from SetIpAddressType or the
rule paths got a message about security groups and NLBs that did not
match their request.
Neutralize to a generic, condition-agnostic message. The AWS error code
is unchanged.
* style(ui): oxfmt security-groups-editor test
|
||
|
|
b3f7251239 |
refactor: migrate lbagent signing to predastore/auth.SignReq (#281)
agent.go signed gateway requests with aws-sdk-go v1's v4.Signer. Switch to predastore/auth.SignReq (a thin wrapper over aws-sdk-go-v2's v4.Signer) so the monorepo runs a single signer version and drops the last direct dependency on aws-sdk-go v1's signer subpackage. - Replace the *v4.Signer field with the raw access/secret keys. - signedPost computes the hex SHA-256 payload hash and calls auth.SignReq. - Add TestSignedPost_ProducesVerifiableSignature: the signed request parses and verifies server-side via auth.ParseReq/Verify with a matching body hash. |
||
|
|
de5ee4339a |
fix(elbv2): render fixed-response listener defaults + DescribeTags listener-rules (#278)
Shared-ingress ALBs use a fixed-response default action (HTTP 404) with host-header rules forwarding to per-app target groups. Two gaps broke this: - The HAProxy generator read DefaultActions[0].TargetGroupArn unconditionally, so a fixed-response default (empty ARN) rendered `default_backend bk_`, a dangling reference that made HAProxy fail to start — the listener never bound :80 and the LB served no traffic. Store and render the fixed-response config as a synthetic backend (`http-request return status N ...`), with status, content-type, and body validated before interpolation. - ListenerAction dropped the fixed-response config on create/modify, so DescribeListeners read back an empty action and tofu saw a perpetual diff. Preserve it through CreateListener/ModifyListener and the SDK conversion. - DescribeTags returned InvalidParameterValue for listener-rule ARNs; resolve the rule and return an (untagged) descriptor instead. Adds generator tests for the fixed-response default (with host-header rules) and unsafe-body rejection, plus DescribeTags listener-rule coverage. |
||
|
|
3de1d6d7c5 |
fix(microvm): build initramfs device nodes via fakeroot, drop sudo mknod
sudo mknod created real /dev/console and /dev/null char-device nodes on the host under $CHROOT_DIR. Real device nodes on the host filesystem are a footgun: a stale-image cleanup, tmp reaper, or an empty $CHROOT_DIR (which would redirect the recursive /dev removal at the host root) could clobber host /dev/null and force a reboot, breaking CI servers. Write the two static nodes the kernel needs into the cpio archive inside a fakeroot session instead. mknod/cpio see faked device entries and emit real char-device records into the archive, while the host filesystem never gets an actual node. init mounts devtmpfs over /dev at runtime for everything else. Also guard $CHROOT_DIR against empty/unexpected values before any destructive removal, and add fakeroot to the required-tools check. |
||
|
|
044d35737e | feat: remove bash e2e for e2e-go | ||
|
|
2dada2cf9d | release: update go.mod for v1.6.0 v1.6.0 | ||
|
|
9508aaacce | chore: bump go 1.26.4 | ||
|
|
40deb1e4c2 |
fix(e2e): add explicit IGW default route in run-lb-e2e
After PR #270 the VPC-LR-wide 0.0.0.0/0 -> IGW static route is no longer installed implicitly at attach-internet-gateway. Subnets whose effective route table lacks an internet-bound route get an OVN drop policy at SubnetEgressPriorityDrop (1100), which kills LB target reply traffic before un-DNAT can fire and stalls the LB internal-client probe until job timeout. Add the standard AWS pattern: describe-route-tables -> create-route 0.0.0.0/0 -> IGW. This fires vpc.add-igw-route + vpc.ungate-subnet-egress, removing the drop policy and installing a reroute policy at SubnetEgressPriorityIGW (1000). |