mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 02:43:17 +00:00
496424c41c
* test(eks): EKS control-plane e2e suite (Phase 1, API-level)
Add tests/e2e/eks/eks_test.go driving the EKS lifecycle against awsgw:
CreateCluster -> ACTIVE (endpoint + CA asserted), kubeconfig artifact
assembly, AccessEntry CRUD, get-token presign decode, DeleteCluster -> gone.
Harness: EKS SDK client on AWSClient + WaitForEKSCluster{Active,Deleted}
pollers. Wire e2e-eks into the Makefile and the single-node CI suite list.
* test(eks): Phase 2 kubectl get-nodes data-plane gate
Add the reachability shim (harness/eks_mgmt.go) that reads the control-plane
VM's br-mgmt IP from the cluster KV meta over NATS, plus a kubectl wrapper
(harness/kubectl.go) that skips when kubectl is absent. KubectlGetNodes points
kubectl at https://<mgmtIP>:6443 (insecure-skip-tls-verify; cert SANs the NLB
DNS, not the mgmt IP) with an aws eks get-token exec block and polls until the
control-plane node is Ready — the get-token webhook auth regression gate.
IRSA pod + the real NLB/Route53 passthrough are deferred to a follow-up.
* test(eks): tolerate doubled NotFound code + dial loopback NATS
DeleteCluster detection matched the SDK constant ResourceNotFoundException,
but awsgw emits ResourceNotFoundExceptionException on the wire — match the
NotFound code by substring so deletion is detected immediately instead of
timing out. The mgmt-IP shim dialed NATS.Host verbatim (0.0.0.0:4222), which
the serving cert does not SAN; rewrite the wildcard bind to loopback.
Validated live: all 6 subtests green, kubectl get nodes shows the
control-plane node Ready via the get-token webhook auth path.
* feat: build system images with libguestfs (no host block devices)
Replace the qemu-nbd + sudo mount + chroot + /dev-bind flow in
build-system-image.sh with libguestfs (virt-customize / virt-resize /
guestfish). Every operation runs in an isolated userspace appliance VM with
its own kernel, so the build touches no host block device, no host mount, and
needs no sudo — removing the failure class that wiped the host's /dev on shared
CI runners (a leaked /dev bind traversed by rm -rf).
Applies to all distros: alpine resizes via guestfish e2fsck+resize2fs, ubuntu
via virt-resize; customization (packages, binaries, files, setup script,
service enable, cache cleanup) is one ordered virt-customize invocation.
LIBGUESTFS_BACKEND=direct (no libvirt on runners).
Wire the eks-node AMI into the single-node e2e job: the runner builds the
device-free raw, ships it to the VM, and the VM imports it (predastore/NATS are
VM-only) so the eks suite's CreateCluster has an AMI to boot.
* feat(eks): bootstrap + state-report via AWS gateway broker
Converge the EKS control-plane → host channel onto the ELBv2 lb-agent model:
the K3s server VM publishes its bootstrap envelopes and periodic state reports
via SigV4-signed HTTPS POST to the AWS gateway, instead of dialing core NATS
directly. A new gateway action (PublishInternal, POST
/clusters/{name}/internal-publish) relays the payload verbatim onto the
existing eks.bus.* / eks.state.* subjects, so the daemon-side subscriber, KV
persistence/encryption, and reconciler are unchanged.
This removes the VM's need to reach NATS at all, so NATS can stay
cluster-internal behind its bind/firewall/mTLS controls — the prior direct
path broke when NATS was not VM-reachable (e2e CreateCluster timeout) and was
DDIL-fragile (fire-and-forget nats pub, no ack/retry).
- gateway/eks: PublishInternal broker + route; closed-set channel/kind validation
- cmd/eks-gateway-publish: one-shot SigV4 publisher (retry-until-ack) baked into
the eks-node AMI; replaces the nats-cli publisher in k3s-first-boot.sh and
mulga-eks-state-report.sh
- handlers/eks + daemon: hand the K3s server VM the mgmt-reachable gateway URL +
system (Predastore) SigV4 creds + gateway CA instead of NATS URL/token/CA;
new resolveSystemGatewayBaseURL (mgmt-bridge-first)
- eks-node image: build/install eks-gateway-publish, drop nats-cli download
* feat: migrate eks-token-webhook to the AWS gateway broker
The TokenReview webhook dialed core NATS directly (STS verify + AccessEntry
KV read). On a mgmt-only control-plane VM with NATS bound off-subnet that
fails, so the webhook exited at startup, the apiserver token-webhook
kubeconfig was never written, and the apiserver crash-looped — the cluster
never reached ACTIVE.
Converge it onto the same gateway-broker model as bootstrap/state-report and
ELBv2's lb-agent: the webhook SigV4-signs an HTTPS POST {accountId,token} to
/clusters/{name}/token-review, and the gateway runs the verify + lookup
host-side and returns the resolved identity. The VM now speaks no core NATS.
- internal/eksgw: shared on-VM SigV4 client (also adopted by
eks-gateway-publish).
- handlers/eks/token_review.go: Authenticate decision core (moved from the
webhook binary) + ResolveTokenReview host-side wiring.
- gateway/eks token-review route + WebhookTokenReview handler.
- webhook: drop NATS, relay via eksgw.Client; broker fault -> 503 (retryable)
not a silent deny.
- initd: export EKS_GATEWAY_* / EKS_ACCESS_* creds, not SPINIFEX_NATS_*.
* fix: build eks-node helpers with GOFIPS140; cover token-review broker
eks-gateway-publish blank-imports internal/fipsboot, whose init() panics
unless the binary is built with GOFIPS140=v1.0.0. The eks-node AMI build
omitted the flag, so the binary paniced at first boot right after k3s came
up — k3s-first-boot (set -eu) died before publishing the bootstrap token and
the cluster never reached ACTIVE. Build both eks-node helpers with
GOFIPS140=v1.0.0, matching cmd/lb-agent.
Add NATS-backed unit tests for ResolveTokenReview and the WebhookTokenReview
gateway handler (verify responder + account KV) to cover the new broker path.
* fix: export first-boot env so eks-gateway-publish child sees it
k3s-first-boot.sh and mulga-eks-state-report.sh sourced /etc/spinifex-eks/
first-boot.env but never exported the vars, so the eks-gateway-publish child
process inherited an empty environment and exited '--account-id is required'.
The control-plane VM came up healthy but never published its bootstrap
envelopes, so the cluster never reached ACTIVE. Wrap the source in set -a /
set +a so the KEY=value lines export to child processes.
* fix: make awsgw start --host override actually bind
The 'awsgw start --host' override printed 'Overwriting awsgw host' but the
assignment was commented out, so AWSGW always bound the spinifex.toml host
(a specific WAN IP, e.g. 192.168.0.10). A system instance (EKS K3s control
plane, internal LB agent) has a mgmt-only NIC on-link to the br-mgmt IP with
no route to the WAN IP, so its SigV4 POSTs to the gateway were refused and the
cluster never bootstrapped. Honour --host (e2e passes 0.0.0.0:9999) so AWSGW
listens on all interfaces incl. br-mgmt; the serving cert already SANs every
local interface IP, and WAN clients still reach it.
* test(eks): install kubectl on e2e VM so KubectlGetNodes runs
The eks data-plane subtest (TestEKS/KubectlGetNodes) drives the full
aws eks get-token -> kubectl -> apiserver -> token-webhook -> gateway broker
-> STS verify path — the only end-to-end exercise of the token-review broker.
The e2e binaries run on the VM, where kubectl was absent, so
harness.NewKubectl t.Skip'd it and the path went unvalidated. Install a
checksum-verified kubectl (pinned to the k3s server version) on the VM,
gated on e2e-eks.
140 lines
6.5 KiB
Bash
Executable File
140 lines
6.5 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
# k3s-first-boot — runs once after the K3s server reaches a healthy state.
|
|
# Reads the bootstrap node-token and admin kubeconfig that K3s writes at
|
|
# server startup, rewrites the kubeconfig server address to the cluster's
|
|
# NLB endpoint (so workers and external kubectl can use it), and publishes
|
|
# the bootstrap artifacts to the host via the AWS gateway (SigV4-signed HTTPS
|
|
# POST — the eks-gateway-publish helper) for the spinifex cluster reconciler
|
|
# to consume into KV. The gateway relays onto the eks.bus.* NATS subjects; the
|
|
# VM never speaks NATS directly.
|
|
#
|
|
# Required env (from cloud-init user-data /etc/spinifex-eks/first-boot.env):
|
|
# EKS_GATEWAY_URL https://{mgmt-host}:9999 (AWS gateway)
|
|
# EKS_GATEWAY_CA /etc/spinifex-eks/gateway-ca.pem (TLS CA bundle)
|
|
# EKS_ACCESS_KEY system SigV4 access key id
|
|
# EKS_SECRET_KEY system SigV4 secret access key
|
|
# EKS_REGION SigV4 signing region
|
|
# EKS_ACCOUNT_ID
|
|
# EKS_CLUSTER_NAME
|
|
# EKS_NLB_ENDPOINT https://{cluster}.{accountID}.eks.{region}.{suffix}
|
|
#
|
|
# Idempotent: a sentinel file at /var/lib/spinifex-eks/first-boot.pending gates
|
|
# execution. On success the sentinel is removed and the OpenRC service is
|
|
# pulled from the default runlevel so it does not retry on subsequent boots.
|
|
|
|
SENTINEL=/var/lib/spinifex-eks/first-boot.pending
|
|
ENVFILE=/etc/spinifex-eks/first-boot.env
|
|
LOGTAG="k3s-first-boot"
|
|
|
|
log() { echo "[${LOGTAG}] $*"; }
|
|
die() { log "ERROR: $*"; exit 1; }
|
|
|
|
if [ ! -f "${SENTINEL}" ]; then
|
|
log "sentinel missing — first boot already complete"
|
|
exit 0
|
|
fi
|
|
|
|
if [ ! -f "${ENVFILE}" ]; then
|
|
die "${ENVFILE} not found — cloud-init did not seed first-boot env"
|
|
fi
|
|
|
|
# set -a so the sourced KEY=value lines are exported: the eks-gateway-publish
|
|
# child reads its config from the environment (EKS_ACCOUNT_ID etc.). A bare
|
|
# source leaves them unexported and the helper exits "--account-id is required".
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "${ENVFILE}"
|
|
set +a
|
|
|
|
for v in EKS_GATEWAY_URL EKS_ACCESS_KEY EKS_SECRET_KEY EKS_ACCOUNT_ID EKS_CLUSTER_NAME EKS_NLB_ENDPOINT; do
|
|
eval "val=\${$v:-}"
|
|
[ -n "${val}" ] || die "env ${v} not set"
|
|
done
|
|
|
|
# Paths K3s writes during server startup: the bootstrap node-token and the
|
|
# admin kubeconfig (used both to gate readiness and to publish downstream).
|
|
TOKEN_FILE=/var/lib/rancher/k3s/server/node-token
|
|
KUBECONFIG_FILE=/etc/rancher/k3s/k3s.yaml
|
|
|
|
# 1. Wait for the apiserver to be ready to serve, gating on /readyz. K3s runs
|
|
# the apiserver with anonymous-auth=false, so an anonymous probe returns 401,
|
|
# never "ok" — the probe must be authenticated. Use the node's admin
|
|
# kubeconfig (kubectl get --raw), which k3s writes early in startup. kubectl
|
|
# exits 0 only when /readyz returns 200; on a failing sub-check it exits
|
|
# non-zero, so the body "ok" check plus exit status gates correctly. Every
|
|
# 30s the failing /readyz checks are logged so a stuck boot is diagnosable
|
|
# from the captured serial console.
|
|
log "waiting for K3s apiserver readiness (/readyz) on 127.0.0.1:6443"
|
|
i=0
|
|
ready=0
|
|
while [ "${i}" -lt 300 ]; do
|
|
if [ -r "${KUBECONFIG_FILE}" ] && \
|
|
[ "$(kubectl --kubeconfig "${KUBECONFIG_FILE}" get --raw='/readyz' 2>/dev/null)" = "ok" ]; then
|
|
log "K3s apiserver ready after ${i}s"
|
|
ready=1
|
|
break
|
|
fi
|
|
if [ $((i % 30)) -eq 0 ]; then
|
|
log "apiserver not ready after ${i}s:"
|
|
if [ -r "${KUBECONFIG_FILE}" ]; then
|
|
kubectl --kubeconfig "${KUBECONFIG_FILE}" get --raw='/readyz?verbose' 2>&1 \
|
|
| grep -E '^\[-\]|failed$' | head -20 | while IFS= read -r l; do log " ${l}"; done
|
|
else
|
|
log " (admin kubeconfig ${KUBECONFIG_FILE} not written yet)"
|
|
fi
|
|
fi
|
|
i=$((i + 5))
|
|
sleep 5
|
|
done
|
|
if [ "${ready}" -ne 1 ]; then
|
|
log "K3s apiserver not ready within 5 minutes; last /readyz body:"
|
|
kubectl --kubeconfig "${KUBECONFIG_FILE}" get --raw='/readyz?verbose' 2>&1 \
|
|
| head -40 | while IFS= read -r l; do log " ${l}"; done
|
|
log "last 40 lines of /var/log/k3s.log follow:"
|
|
tail -n 40 /var/log/k3s.log 2>/dev/null || log "(no /var/log/k3s.log)"
|
|
die "K3s apiserver not ready within 5 minutes"
|
|
fi
|
|
|
|
# 2. Read the four bootstrap artifacts K3s wrote at server startup.
|
|
[ -r "${TOKEN_FILE}" ] || die "${TOKEN_FILE} unreadable"
|
|
[ -r "${KUBECONFIG_FILE}" ] || die "${KUBECONFIG_FILE} unreadable"
|
|
|
|
NODE_TOKEN=$(cat "${TOKEN_FILE}")
|
|
# K3s ships kubeconfig with server: https://127.0.0.1:6443 — rewrite to the
|
|
# NLB endpoint so it works from outside the control plane VM.
|
|
KUBECONFIG_REWRITTEN=$(sed "s|server: https://127\.0\.0\.1:6443|server: ${EKS_NLB_ENDPOINT}|" "${KUBECONFIG_FILE}")
|
|
# The cluster CA the daemon records (base64 PEM) is exactly the
|
|
# certificate-authority-data the admin kubeconfig already embeds.
|
|
CA_B64=$(awk '/certificate-authority-data:/ {print $2; exit}' "${KUBECONFIG_FILE}")
|
|
[ -n "${CA_B64}" ] || die "no certificate-authority-data in ${KUBECONFIG_FILE}"
|
|
# The OIDC JWKS the apiserver serves from the signing key cloud-init seeded. The
|
|
# daemon cross-checks its kid/kty against the controller-generated keypair.
|
|
JWKS=$(kubectl --kubeconfig "${KUBECONFIG_FILE}" get --raw='/openid/v1/jwks' 2>/dev/null)
|
|
[ -n "${JWKS}" ] || die "apiserver returned empty /openid/v1/jwks"
|
|
|
|
# 3. Publish the four bootstrap messages through the AWS gateway. Each is a
|
|
# BootstrapEnvelope JSON document (handlers/eks/nats_bootstrap.go); jq encodes
|
|
# the values so embedded newlines/quotes in the kubeconfig and JWKS stay valid
|
|
# JSON. eks-gateway-publish SigV4-signs the POST and retries until the gateway
|
|
# acks, then the gateway relays onto eks.bus.{account}.{cluster}.{kind}. It
|
|
# reads EKS_GATEWAY_URL/EKS_GATEWAY_CA/EKS_ACCESS_KEY/EKS_SECRET_KEY/EKS_REGION/
|
|
# EKS_ACCOUNT_ID/EKS_CLUSTER_NAME from the env file already sourced above.
|
|
|
|
# publish_envelope <kind-suffix>: reads the envelope JSON from stdin.
|
|
publish_envelope() {
|
|
log "publishing $1 via gateway"
|
|
eks-gateway-publish -channel bootstrap -kind "$1"
|
|
}
|
|
|
|
jq -n --arg t "${NODE_TOKEN}" '{token: $t}' | publish_envelope k3s-bootstrap-token
|
|
jq -n --arg k "${KUBECONFIG_REWRITTEN}" '{adminKubeconfig: $k}' | publish_envelope k3s-admin-kubeconfig
|
|
jq -n --arg j "${JWKS}" '{jwks: $j}' | publish_envelope k3s-oidc-jwks
|
|
jq -n --arg c "${CA_B64}" '{certificateAuthority: $c}' | publish_envelope k3s-ca
|
|
|
|
# 4. Self-disable. Remove sentinel, pull from runlevel.
|
|
rm -f "${SENTINEL}"
|
|
rc-update del k3s-first-boot default 2>/dev/null || true
|
|
log "first boot complete"
|