mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +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.
370 lines
13 KiB
Bash
Executable File
370 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# build-system-image.sh — Build a minimal system AMI from a manifest
|
|
#
|
|
# Supports Alpine Linux and Ubuntu 24.04+ as base distros. Creates a pre-baked
|
|
# image with custom packages, binaries, and setup scripts installed, ready for
|
|
# import as a Spinifex AMI.
|
|
#
|
|
# Customization runs entirely inside the libguestfs appliance (its own kernel +
|
|
# userspace), so the build touches NO host block device, NO host mount, and
|
|
# needs NO sudo. This deliberately replaces the previous qemu-nbd + `sudo mount`
|
|
# + chroot + /dev bind-mount flow, whose leaked binds could be traversed by a
|
|
# later `rm -rf` and wipe the host's /dev (it wedged CI runners). libguestfs
|
|
# isolates all of that in a throwaway VM.
|
|
#
|
|
# Requirements: virt-customize, guestfish, virt-resize (libguestfs-tools),
|
|
# qemu-img, curl
|
|
# Usage: ./scripts/build-system-image.sh <manifest.conf> [--import]
|
|
# --import Also import the image as an AMI via spx admin images import
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
# No libvirt on CI runners — drive qemu directly. Harmless when libvirt exists.
|
|
export LIBGUESTFS_BACKEND="${LIBGUESTFS_BACKEND:-direct}"
|
|
|
|
usage() {
|
|
echo "Usage: $0 <manifest.conf> [--import]"
|
|
echo ""
|
|
echo " manifest.conf Path to image manifest (see scripts/images/ for examples)"
|
|
echo " --import Import the built image as an AMI"
|
|
exit 1
|
|
}
|
|
|
|
if [[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]]; then
|
|
usage
|
|
fi
|
|
|
|
MANIFEST="$1"
|
|
shift
|
|
|
|
if [[ ! -f "$MANIFEST" ]]; then
|
|
echo "ERROR: Manifest not found: $MANIFEST"
|
|
exit 1
|
|
fi
|
|
|
|
DO_IMPORT=false
|
|
QUIET=false
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--import) DO_IMPORT=true ;;
|
|
--quiet) QUIET=true ;;
|
|
esac
|
|
done
|
|
|
|
# Source the manifest
|
|
# shellcheck source=/dev/null
|
|
source "$MANIFEST"
|
|
|
|
# Default distro is alpine for backward compatibility
|
|
DISTRO="${DISTRO:-alpine}"
|
|
|
|
# Validate required manifest fields
|
|
for field in IMAGE_NAME IMAGE_SIZE; do
|
|
if [[ -z "${!field:-}" ]]; then
|
|
echo "ERROR: Manifest missing required field: $field"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Derived paths
|
|
BUILD_DIR="/tmp/${IMAGE_NAME}-image-build"
|
|
|
|
case "$DISTRO" in
|
|
alpine)
|
|
if [[ -z "${ALPINE_VERSION:-}" ]]; then
|
|
echo "ERROR: Manifest missing required field: ALPINE_VERSION"
|
|
exit 1
|
|
fi
|
|
SOURCE_IMAGE="generic_alpine-${ALPINE_VERSION}-x86_64-bios-cloudinit-r0.qcow2"
|
|
SOURCE_URL="https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION%.*}/releases/cloud/${SOURCE_IMAGE}"
|
|
OUTPUT_IMAGE="${BUILD_DIR}/${IMAGE_NAME}-alpine.qcow2"
|
|
OUTPUT_RAW="${BUILD_DIR}/${IMAGE_NAME}-alpine.raw"
|
|
DISTRO_VERSION="${ALPINE_VERSION}"
|
|
BOOT_MODE="bios"
|
|
;;
|
|
ubuntu)
|
|
if [[ -z "${UBUNTU_VERSION:-}" ]]; then
|
|
echo "ERROR: Manifest missing required field: UBUNTU_VERSION"
|
|
exit 1
|
|
fi
|
|
SOURCE_IMAGE="ubuntu-${UBUNTU_VERSION}-minimal-cloudimg-amd64.img"
|
|
SOURCE_URL="https://cloud-images.ubuntu.com/minimal/releases/${UBUNTU_CODENAME:-noble}/release/${SOURCE_IMAGE}"
|
|
OUTPUT_IMAGE="${BUILD_DIR}/${IMAGE_NAME}-ubuntu.qcow2"
|
|
OUTPUT_RAW="${BUILD_DIR}/${IMAGE_NAME}-ubuntu.raw"
|
|
DISTRO_VERSION="${UBUNTU_VERSION}"
|
|
BOOT_MODE="uefi"
|
|
;;
|
|
*)
|
|
echo "ERROR: Unknown DISTRO: $DISTRO (supported: alpine, ubuntu)"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
# import_ami registers the built raw image as a Spinifex AMI. Shared by the
|
|
# fresh-build and cached-build paths.
|
|
import_ami() {
|
|
echo "Importing as AMI..."
|
|
rm -f "$OUTPUT_IMAGE"
|
|
local args=(--file "$OUTPUT_RAW" --distro "${DISTRO}" --version "${DISTRO_VERSION}" --arch x86_64 --boot-mode "${BOOT_MODE}")
|
|
if [[ -n "${AMI_NAME:-}" ]]; then
|
|
args+=(--ami-name "$AMI_NAME")
|
|
fi
|
|
if [[ -n "${SYSTEM_TAG:-}" ]]; then
|
|
args+=(--tag "$SYSTEM_TAG")
|
|
fi
|
|
spx admin images import "${args[@]}"
|
|
}
|
|
|
|
# In quiet mode, redirect build output to /dev/null (import output still shown)
|
|
if [[ "$QUIET" == true ]]; then
|
|
exec 3>&1 # save original stdout
|
|
exec 1>/dev/null # suppress build output
|
|
fi
|
|
|
|
echo "=== System Image Builder (libguestfs) ==="
|
|
echo "Image: ${IMAGE_NAME} — ${IMAGE_DESCRIPTION:-}"
|
|
echo "Distro: ${DISTRO} ${DISTRO_VERSION}"
|
|
echo "Size: ${IMAGE_SIZE}"
|
|
echo "Build: ${BUILD_DIR}"
|
|
echo ""
|
|
|
|
# Step 0: Check prerequisites
|
|
for tool in virt-customize qemu-img; do
|
|
if ! command -v "$tool" &>/dev/null; then
|
|
echo "ERROR: $tool not found. Install libguestfs-tools + qemu-utils."
|
|
exit 1
|
|
fi
|
|
done
|
|
if [[ "$DISTRO" == "alpine" ]] && ! command -v guestfish &>/dev/null; then
|
|
echo "ERROR: guestfish not found. Install libguestfs-tools."
|
|
exit 1
|
|
fi
|
|
if [[ "$DISTRO" == "ubuntu" ]] && ! command -v virt-resize &>/dev/null; then
|
|
echo "ERROR: virt-resize not found. Install libguestfs-tools."
|
|
exit 1
|
|
fi
|
|
|
|
# Build binaries if BUILD_COMMANDS is set
|
|
if [[ -n "${BUILD_COMMANDS:-}" ]]; then
|
|
echo "Building binaries: ${BUILD_COMMANDS}"
|
|
if ! (cd "$PROJECT_DIR" && eval "$BUILD_COMMANDS"); then
|
|
echo "ERROR: BUILD_COMMANDS failed: ${BUILD_COMMANDS}"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Verify binaries exist; Alpine also requires static linking (uses musl)
|
|
if [[ -n "${INSTALL_BINARIES:-}" ]]; then
|
|
IFS=' ' read -ra BINARY_PAIRS <<< "$INSTALL_BINARIES"
|
|
for pair in "${BINARY_PAIRS[@]}"; do
|
|
src="${pair%%:*}"
|
|
src_path="${PROJECT_DIR}/${src}"
|
|
if [[ ! -f "$src_path" ]]; then
|
|
echo "ERROR: Binary not found: $src_path"
|
|
exit 1
|
|
fi
|
|
if [[ "$DISTRO" == "alpine" ]] && ! file "$src_path" | grep -q "statically linked"; then
|
|
echo "ERROR: $src_path is not statically linked (Alpine uses musl — dynamic glibc binaries will fail)"
|
|
echo " Rebuild with: CGO_ENABLED=0 go build ..."
|
|
exit 1
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Serialize the build with flock — concurrent builds on the same host (e.g. CI
|
|
# single-node + multi-node jobs) share BUILD_DIR + the cached raw image.
|
|
BUILD_LOCK="/tmp/build-system-image.lock"
|
|
echo "Acquiring build lock..."
|
|
exec 9>"$BUILD_LOCK"
|
|
flock 9
|
|
echo "Lock acquired"
|
|
|
|
# If the raw image was built recently (< 10 min), skip the entire build.
|
|
# This avoids duplicate work when concurrent CI jobs build the same image.
|
|
if [[ -f "$OUTPUT_RAW" ]] && [[ $(( $(date +%s) - $(stat -c %Y "$OUTPUT_RAW") )) -lt 600 ]]; then
|
|
echo "=== Skipping build — $OUTPUT_RAW is fresh (< 10 min old) ==="
|
|
|
|
# Restore stdout if suppressed, then jump to import
|
|
if [[ "$QUIET" == true ]]; then
|
|
exec 1>&3 3>&-
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Build complete (cached) ==="
|
|
echo " raw: $OUTPUT_RAW ($(du -h "$OUTPUT_RAW" | cut -f1))"
|
|
|
|
if [[ "$DO_IMPORT" == true ]]; then
|
|
import_ami
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
mkdir -p "$BUILD_DIR"
|
|
|
|
# Step 1: Download base cloud image
|
|
if [[ -f "${BUILD_DIR}/${SOURCE_IMAGE}" ]]; then
|
|
echo "Base image already downloaded."
|
|
else
|
|
echo "Downloading ${DISTRO} ${DISTRO_VERSION} cloud image..."
|
|
if ! curl --fail -L -o "${BUILD_DIR}/${SOURCE_IMAGE}" "$SOURCE_URL"; then
|
|
rm -f "${BUILD_DIR}/${SOURCE_IMAGE}"
|
|
echo "ERROR: Failed to download image from $SOURCE_URL"
|
|
exit 1
|
|
fi
|
|
# Verify the download is a valid qcow2/img
|
|
if ! qemu-img info "${BUILD_DIR}/${SOURCE_IMAGE}" &>/dev/null; then
|
|
rm -f "${BUILD_DIR}/${SOURCE_IMAGE}"
|
|
echo "ERROR: Downloaded file is not a valid disk image"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Step 2: Copy image for customization
|
|
rm -f "$OUTPUT_IMAGE"
|
|
echo "Copying image for customization..."
|
|
cp "${BUILD_DIR}/${SOURCE_IMAGE}" "$OUTPUT_IMAGE"
|
|
|
|
# Step 3: Resize to provide room for packages. Both paths grow the root
|
|
# filesystem entirely inside the libguestfs appliance — no host loop/nbd device.
|
|
echo "Resizing image to ${IMAGE_SIZE}..."
|
|
if [[ "$DISTRO" == "alpine" ]]; then
|
|
# Alpine cloud images have ext4 directly on the whole disk (no partition
|
|
# table); grow the disk file then expand the fs from inside the appliance.
|
|
qemu-img resize "$OUTPUT_IMAGE" "$IMAGE_SIZE"
|
|
guestfish --rw -a "$OUTPUT_IMAGE" run \
|
|
: e2fsck /dev/sda forceall:true \
|
|
: resize2fs /dev/sda
|
|
else
|
|
# Ubuntu images use a GPT partition table with the root fs on /dev/sda1;
|
|
# virt-resize expands the partition + fs into a fresh IMAGE_SIZE disk.
|
|
RESIZED="${OUTPUT_IMAGE}.resized"
|
|
rm -f "$RESIZED"
|
|
qemu-img create -f qcow2 "$RESIZED" "$IMAGE_SIZE"
|
|
virt-resize --expand /dev/sda1 "$OUTPUT_IMAGE" "$RESIZED"
|
|
mv -f "$RESIZED" "$OUTPUT_IMAGE"
|
|
fi
|
|
|
|
# Step 4: Assemble the virt-customize operation list. virt-customize runs the
|
|
# operations in the order they appear on the command line, so this mirrors the
|
|
# old chroot sequence: packages → binaries → files → setup → services → clean.
|
|
CUST=(virt-customize -a "$OUTPUT_IMAGE" --network)
|
|
if [[ "$DISTRO" == "ubuntu" ]]; then
|
|
# DKMS driver bakes (GPU images) need real resources in the appliance.
|
|
CUST+=(--memsize 6144 --smp 4)
|
|
else
|
|
CUST+=(--memsize 2048)
|
|
fi
|
|
|
|
# Packages
|
|
if [[ "$DISTRO" == "alpine" ]] && [[ -n "${APK_PACKAGES:-}" ]]; then
|
|
echo "Will install packages: ${APK_PACKAGES}"
|
|
CUST+=(--run-command 'sed -i "s|^#\(.*community\)|\1|" /etc/apk/repositories 2>/dev/null || true')
|
|
CUST+=(--run-command 'grep -q community /etc/apk/repositories || grep main /etc/apk/repositories | head -1 | sed "s|/main|/community|" >> /etc/apk/repositories')
|
|
CUST+=(--run-command 'apk update')
|
|
CUST+=(--run-command "apk add ${APK_PACKAGES}")
|
|
elif [[ "$DISTRO" == "ubuntu" ]] && [[ -n "${APT_PACKAGES:-}" ]]; then
|
|
echo "Will install packages: ${APT_PACKAGES}"
|
|
CUST+=(--run-command 'export DEBIAN_FRONTEND=noninteractive; apt-get update')
|
|
CUST+=(--run-command "export DEBIAN_FRONTEND=noninteractive; apt-get install -y --no-install-recommends ${APT_PACKAGES}")
|
|
fi
|
|
|
|
# Binaries (mode 0755). --mkdir is idempotent (mkdir -p semantics).
|
|
if [[ -n "${INSTALL_BINARIES:-}" ]]; then
|
|
IFS=' ' read -ra BINARY_PAIRS <<< "$INSTALL_BINARIES"
|
|
for pair in "${BINARY_PAIRS[@]}"; do
|
|
src="${pair%%:*}"
|
|
dst="${pair#*:}"
|
|
src_path="${PROJECT_DIR}/${src}"
|
|
CUST+=(--mkdir "$(dirname "$dst")")
|
|
CUST+=(--upload "${src_path}:${dst}")
|
|
CUST+=(--chmod "0755:${dst}")
|
|
done
|
|
fi
|
|
|
|
# Auxiliary files (systemd units, OpenRC initd scripts, cron entries, configs).
|
|
# Mode 0644; setup scripts may chmod role-specific files to 0755 afterwards.
|
|
if [[ -n "${INSTALL_FILES:-}" ]]; then
|
|
IFS=' ' read -ra FILE_PAIRS <<< "$INSTALL_FILES"
|
|
for pair in "${FILE_PAIRS[@]}"; do
|
|
src="${pair%%:*}"
|
|
dst="${pair#*:}"
|
|
src_path="${PROJECT_DIR}/${src}"
|
|
if [[ ! -f "$src_path" ]]; then
|
|
echo "ERROR: INSTALL_FILES source not found: $src_path"
|
|
exit 1
|
|
fi
|
|
CUST+=(--mkdir "$(dirname "$dst")")
|
|
CUST+=(--upload "${src_path}:${dst}")
|
|
CUST+=(--chmod "0644:${dst}")
|
|
done
|
|
fi
|
|
|
|
# Custom setup script (uploaded + executed + removed by --run, inside the guest).
|
|
if [[ -n "${SETUP_SCRIPT:-}" ]]; then
|
|
setup_path="${PROJECT_DIR}/${SETUP_SCRIPT}"
|
|
if [[ ! -f "$setup_path" ]]; then
|
|
echo "ERROR: Setup script not found: $setup_path"
|
|
exit 1
|
|
fi
|
|
CUST+=(--run "$setup_path")
|
|
fi
|
|
|
|
# Enable services (after files + setup so referenced init scripts exist).
|
|
if [[ -n "${ENABLE_SERVICES:-}" ]]; then
|
|
IFS=' ' read -ra SERVICES <<< "$ENABLE_SERVICES"
|
|
for svc in "${SERVICES[@]}"; do
|
|
if [[ "$DISTRO" == "alpine" ]]; then
|
|
CUST+=(--run-command "rc-update add ${svc} default")
|
|
else
|
|
CUST+=(--run-command "systemctl enable ${svc}")
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Final cleanup of package caches + tmp.
|
|
if [[ "$DISTRO" == "alpine" ]]; then
|
|
CUST+=(--run-command 'apk cache clean 2>/dev/null || true; rm -rf /var/cache/apk/* /tmp/* 2>/dev/null || true')
|
|
else
|
|
CUST+=(--run-command 'export DEBIAN_FRONTEND=noninteractive; apt-get clean; rm -rf /var/lib/apt/lists/* /tmp/* 2>/dev/null || true')
|
|
fi
|
|
|
|
echo "Customizing image (libguestfs appliance)..."
|
|
"${CUST[@]}"
|
|
|
|
# Step 5: Convert to raw for import
|
|
echo "Converting to raw format..."
|
|
qemu-img convert -f qcow2 -O raw "$OUTPUT_IMAGE" "$OUTPUT_RAW"
|
|
|
|
# Restore stdout if suppressed
|
|
if [[ "$QUIET" == true ]]; then
|
|
exec 1>&3 3>&-
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Build complete ==="
|
|
echo " Image: ${IMAGE_NAME}"
|
|
echo " qcow2: $OUTPUT_IMAGE ($(du -h "$OUTPUT_IMAGE" | cut -f1))"
|
|
echo " raw: $OUTPUT_RAW ($(du -h "$OUTPUT_RAW" | cut -f1))"
|
|
echo ""
|
|
|
|
if [[ "$DO_IMPORT" == true ]]; then
|
|
import_ami
|
|
else
|
|
echo "To import as AMI, run:"
|
|
echo " spx admin images import \\"
|
|
echo " --file $OUTPUT_RAW \\"
|
|
NAME_HINT=""
|
|
if [[ -n "${AMI_NAME:-}" ]]; then
|
|
NAME_HINT=" \\\n --ami-name ${AMI_NAME}"
|
|
fi
|
|
if [[ -n "${SYSTEM_TAG:-}" ]]; then
|
|
echo -e " --distro ${DISTRO} --version ${DISTRO_VERSION} --arch x86_64 --boot-mode ${BOOT_MODE}${NAME_HINT} \\"
|
|
echo " --tag ${SYSTEM_TAG}"
|
|
else
|
|
echo -e " --distro ${DISTRO} --version ${DISTRO_VERSION} --arch x86_64 --boot-mode ${BOOT_MODE}${NAME_HINT}"
|
|
fi
|
|
fi
|