mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 11:21:49 +00:00
feat: generic system image builder + lint fixes (mulga-766)
Extract build-alb-image.sh into manifest-driven build-system-image.sh: - alb.conf manifest + alb-setup.sh for ALB-specific config - build-alb-image.sh kept as thin wrapper for backward compat - Makefile targets: build-system-image, build-alb-image - Hardened error handling: curl --fail, e2fsck/resize2fs checks, static-link enforcement, rc-update validation, nbd device guard Also fixes 12 lint issues: errcheck, errchkjson, gosec, gofmt, modernize.
This commit is contained in:
@@ -67,6 +67,15 @@ build-alb-agent:
|
||||
@echo -e "\n....Building alb-agent (static)"
|
||||
CGO_ENABLED=0 go build -ldflags "-s -w" -o ./bin/alb-agent cmd/alb-agent/main.go
|
||||
|
||||
build-system-image: ## Build a system image from manifest (use IMAGE=alb)
|
||||
ifndef IMAGE
|
||||
$(error IMAGE is required. Usage: make build-system-image IMAGE=alb)
|
||||
endif
|
||||
./scripts/build-system-image.sh scripts/images/$(IMAGE).conf
|
||||
|
||||
build-alb-image: ## Build ALB Alpine image
|
||||
$(MAKE) build-system-image IMAGE=alb
|
||||
|
||||
go_run:
|
||||
@echo -e "\n....Running $(GO_PROJECT_NAME)...."
|
||||
$(GOPATH)/bin/$(GO_PROJECT_NAME)
|
||||
@@ -198,7 +207,7 @@ distro-arm64:
|
||||
distro-clean:
|
||||
rm -rf dist/
|
||||
|
||||
.PHONY: build build-ui build-alb-agent go_build go_run preflight test test-cover test-race diff-coverage bench run clean \
|
||||
.PHONY: build build-ui build-alb-agent build-system-image build-alb-image go_build go_run preflight test test-cover test-race diff-coverage bench run clean \
|
||||
install-system install-go install-aws quickinstall \
|
||||
lint fix govulncheck \
|
||||
distro distro-amd64 distro-arm64 distro-clean
|
||||
|
||||
+3
-205
@@ -1,206 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# build-alb-image.sh — Build a minimal Alpine AMI for ALB VMs
|
||||
#
|
||||
# Creates a pre-baked Alpine Linux image with HAProxy + alb-agent installed,
|
||||
# ready for import as a Spinifex AMI. ALB VMs boot in ~3s and need no internet
|
||||
# access or package installation at runtime.
|
||||
#
|
||||
# Requirements: qemu-nbd, sudo (for mount/chroot), curl
|
||||
# Usage: ./scripts/build-alb-image.sh [--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)"
|
||||
BUILD_DIR="/tmp/alb-image-build"
|
||||
ALPINE_VERSION="3.21.6"
|
||||
ALPINE_IMAGE="generic_alpine-${ALPINE_VERSION}-x86_64-bios-cloudinit-r0.qcow2"
|
||||
ALPINE_URL="https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/cloud/${ALPINE_IMAGE}"
|
||||
OUTPUT_IMAGE="${BUILD_DIR}/alb-alpine.qcow2"
|
||||
OUTPUT_RAW="${BUILD_DIR}/alb-alpine.raw"
|
||||
NBD_DEV="/dev/nbd0"
|
||||
MOUNT_DIR="${BUILD_DIR}/mnt"
|
||||
ALB_AGENT_BIN="${PROJECT_DIR}/bin/alb-agent"
|
||||
|
||||
DO_IMPORT=false
|
||||
if [[ "${1:-}" == "--import" ]]; then
|
||||
DO_IMPORT=true
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
echo "Cleaning up..."
|
||||
sudo umount "${MOUNT_DIR}" 2>/dev/null || true
|
||||
sudo qemu-nbd --disconnect "${NBD_DEV}" 2>/dev/null || true
|
||||
echo "Done."
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "=== ALB Alpine Image Builder ==="
|
||||
echo "Alpine version: ${ALPINE_VERSION}"
|
||||
echo "Build dir: ${BUILD_DIR}"
|
||||
echo ""
|
||||
|
||||
# Step 0: Check prerequisites
|
||||
if [ ! -f "$ALB_AGENT_BIN" ]; then
|
||||
echo "Building alb-agent (static)..."
|
||||
(cd "$PROJECT_DIR" && CGO_ENABLED=0 go build -o bin/alb-agent cmd/alb-agent/main.go)
|
||||
else
|
||||
# Verify the existing binary is statically linked
|
||||
if ! file "$ALB_AGENT_BIN" | grep -q "statically linked"; then
|
||||
echo "Rebuilding alb-agent as static binary (Alpine uses musl)..."
|
||||
(cd "$PROJECT_DIR" && CGO_ENABLED=0 go build -o bin/alb-agent cmd/alb-agent/main.go)
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v qemu-nbd &>/dev/null; then
|
||||
echo "ERROR: qemu-nbd not found. Install qemu-utils."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$BUILD_DIR" "$MOUNT_DIR"
|
||||
|
||||
# Step 1: Download Alpine cloud image
|
||||
if [ -f "${BUILD_DIR}/${ALPINE_IMAGE}" ]; then
|
||||
echo "Alpine image already downloaded."
|
||||
else
|
||||
echo "Downloading Alpine ${ALPINE_VERSION} cloud image..."
|
||||
curl -L -o "${BUILD_DIR}/${ALPINE_IMAGE}" "$ALPINE_URL"
|
||||
fi
|
||||
|
||||
# Step 2: Copy image for customization
|
||||
echo "Copying image for customization..."
|
||||
cp "${BUILD_DIR}/${ALPINE_IMAGE}" "$OUTPUT_IMAGE"
|
||||
|
||||
# Resize the image to 512M (Alpine cloud images are ~200MB, need room for packages)
|
||||
qemu-img resize "$OUTPUT_IMAGE" 512M
|
||||
|
||||
# Step 3: Connect via qemu-nbd
|
||||
echo "Connecting image via qemu-nbd..."
|
||||
sudo modprobe nbd max_part=4 2>/dev/null || true
|
||||
sudo qemu-nbd --disconnect "${NBD_DEV}" 2>/dev/null || true
|
||||
sudo qemu-nbd --connect="${NBD_DEV}" "$OUTPUT_IMAGE"
|
||||
sleep 1
|
||||
|
||||
# Alpine cloud images have ext4 directly on the block device (no partition table).
|
||||
# Resize the filesystem to fill the resized image.
|
||||
echo "Resizing filesystem..."
|
||||
sudo e2fsck -f -y "${NBD_DEV}" 2>/dev/null || true
|
||||
sudo resize2fs "${NBD_DEV}" 2>/dev/null || true
|
||||
|
||||
# Step 4: Mount and customize
|
||||
echo "Mounting root filesystem..."
|
||||
sudo mount "${NBD_DEV}" "$MOUNT_DIR"
|
||||
|
||||
# Set up resolv.conf for DNS inside chroot
|
||||
sudo cp /etc/resolv.conf "${MOUNT_DIR}/etc/resolv.conf"
|
||||
|
||||
echo "Installing packages in chroot..."
|
||||
sudo chroot "$MOUNT_DIR" /bin/sh -c '
|
||||
set -e
|
||||
# Enable community repo (haproxy is in community)
|
||||
sed -i "s|^#\(.*community\)|\1|" /etc/apk/repositories 2>/dev/null || true
|
||||
|
||||
# Ensure community repo is present
|
||||
if ! grep -q "community" /etc/apk/repositories; then
|
||||
MIRROR=$(grep "main" /etc/apk/repositories | head -1 | sed "s|/main|/community|")
|
||||
echo "$MIRROR" >> /etc/apk/repositories
|
||||
fi
|
||||
|
||||
apk update
|
||||
apk add haproxy curl
|
||||
|
||||
# Enable haproxy to start at boot (but do not start it now)
|
||||
rc-update add haproxy default 2>/dev/null || true
|
||||
|
||||
# Create haproxy config directory
|
||||
mkdir -p /etc/haproxy
|
||||
cat > /etc/haproxy/haproxy.cfg <<EOF
|
||||
# Placeholder config — replaced by alb-agent on first config push
|
||||
global
|
||||
daemon
|
||||
maxconn 256
|
||||
|
||||
defaults
|
||||
mode http
|
||||
timeout connect 5s
|
||||
timeout client 30s
|
||||
timeout server 30s
|
||||
|
||||
frontend health
|
||||
bind *:8405
|
||||
http-request return status 200 content-type text/plain string "ok"
|
||||
EOF
|
||||
|
||||
# Create alb-agent service directory
|
||||
mkdir -p /etc/init.d
|
||||
'
|
||||
|
||||
# Step 5: Copy alb-agent binary into the image
|
||||
echo "Installing alb-agent binary..."
|
||||
sudo cp "$ALB_AGENT_BIN" "${MOUNT_DIR}/usr/local/bin/alb-agent"
|
||||
sudo chmod 755 "${MOUNT_DIR}/usr/local/bin/alb-agent"
|
||||
|
||||
# Create OpenRC init script for alb-agent
|
||||
sudo tee "${MOUNT_DIR}/etc/init.d/alb-agent" > /dev/null <<'INITSCRIPT'
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="ALB NATS Config Agent"
|
||||
command="/usr/local/bin/alb-agent"
|
||||
command_args="--lb-id=${ALB_LB_ID:-unknown} --nats=${ALB_NATS_URL:-nats://10.0.2.2:4222}"
|
||||
command_background=true
|
||||
pidfile="/run/alb-agent.pid"
|
||||
output_log="/var/log/alb-agent.log"
|
||||
error_log="/var/log/alb-agent.log"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
after firewall
|
||||
}
|
||||
INITSCRIPT
|
||||
sudo chmod 755 "${MOUNT_DIR}/etc/init.d/alb-agent"
|
||||
|
||||
# Create a cloud-init module-final script hook that starts alb-agent with the
|
||||
# correct LB ID and NATS URL (passed via cloud-init user-data env vars).
|
||||
# Cloud-init on Alpine runs runcmd which we'll use to start the agent.
|
||||
|
||||
# Step 6: Clean up and unmount
|
||||
echo "Cleaning up image..."
|
||||
sudo chroot "$MOUNT_DIR" /bin/sh -c '
|
||||
apk cache clean 2>/dev/null || true
|
||||
rm -rf /var/cache/apk/* /tmp/*
|
||||
'
|
||||
|
||||
# Restore original resolv.conf (cloud-init will set it on boot)
|
||||
sudo rm -f "${MOUNT_DIR}/etc/resolv.conf"
|
||||
|
||||
echo "Unmounting..."
|
||||
sudo umount "$MOUNT_DIR"
|
||||
sudo qemu-nbd --disconnect "${NBD_DEV}"
|
||||
|
||||
# Step 7: Convert to raw for import
|
||||
echo "Converting to raw format..."
|
||||
qemu-img convert -f qcow2 -O raw "$OUTPUT_IMAGE" "$OUTPUT_RAW"
|
||||
|
||||
RAW_SIZE=$(stat -c%s "$OUTPUT_RAW")
|
||||
echo ""
|
||||
echo "=== Build complete ==="
|
||||
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
|
||||
echo "Importing as AMI..."
|
||||
(cd "$PROJECT_DIR" && ./bin/spx admin images import \
|
||||
--file "$OUTPUT_RAW" \
|
||||
--distro alpine \
|
||||
--version "${ALPINE_VERSION}-alb" \
|
||||
--arch x86_64 \
|
||||
--config "$HOME/spinifex/config/spinifex.toml")
|
||||
else
|
||||
echo "To import as AMI, run:"
|
||||
echo " cd $PROJECT_DIR && ./bin/spx admin images import \\"
|
||||
echo " --file $OUTPUT_RAW \\"
|
||||
echo " --distro alpine --version ${ALPINE_VERSION}-alb --arch x86_64 \\"
|
||||
echo " --config \$HOME/spinifex/config/spinifex.toml"
|
||||
fi
|
||||
# build-alb-image.sh — Thin wrapper for backward compatibility
|
||||
# Delegates to the generic build-system-image.sh with the ALB manifest.
|
||||
exec "$(dirname "$0")/build-system-image.sh" "$(dirname "$0")/images/alb.conf" "$@"
|
||||
|
||||
Executable
+275
@@ -0,0 +1,275 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# build-system-image.sh — Build a minimal Alpine AMI from a manifest
|
||||
#
|
||||
# Creates a pre-baked Alpine Linux image with custom packages, binaries,
|
||||
# and setup scripts installed, ready for import as a Spinifex AMI.
|
||||
#
|
||||
# Requirements: qemu-nbd, qemu-img, sudo (for mount/chroot), 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)"
|
||||
|
||||
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
|
||||
if [[ "${1:-}" == "--import" ]]; then
|
||||
DO_IMPORT=true
|
||||
fi
|
||||
|
||||
# Source the manifest
|
||||
# shellcheck source=/dev/null
|
||||
source "$MANIFEST"
|
||||
|
||||
# Validate required manifest fields
|
||||
for field in IMAGE_NAME ALPINE_VERSION 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"
|
||||
ALPINE_IMAGE="generic_alpine-${ALPINE_VERSION}-x86_64-bios-cloudinit-r0.qcow2"
|
||||
ALPINE_URL="https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION%.*}/releases/cloud/${ALPINE_IMAGE}"
|
||||
OUTPUT_IMAGE="${BUILD_DIR}/${IMAGE_NAME}-alpine.qcow2"
|
||||
OUTPUT_RAW="${BUILD_DIR}/${IMAGE_NAME}-alpine.raw"
|
||||
NBD_DEV="/dev/nbd0"
|
||||
MOUNT_DIR="${BUILD_DIR}/mnt"
|
||||
|
||||
cleanup() {
|
||||
echo "Cleaning up..."
|
||||
sudo umount "${MOUNT_DIR}" 2>/dev/null || true
|
||||
sudo qemu-nbd --disconnect "${NBD_DEV}" 2>/dev/null || true
|
||||
echo "Done."
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "=== System Image Builder ==="
|
||||
echo "Image: ${IMAGE_NAME} — ${IMAGE_DESCRIPTION:-}"
|
||||
echo "Alpine: ${ALPINE_VERSION}"
|
||||
echo "Size: ${IMAGE_SIZE}"
|
||||
echo "Build: ${BUILD_DIR}"
|
||||
echo ""
|
||||
|
||||
# Step 0: Check prerequisites
|
||||
if ! command -v qemu-nbd &>/dev/null; then
|
||||
echo "ERROR: qemu-nbd not found. Install qemu-utils."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v qemu-img &>/dev/null; then
|
||||
echo "ERROR: qemu-img not found. Install qemu-utils."
|
||||
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 and are statically linked
|
||||
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 ! 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
|
||||
|
||||
mkdir -p "$BUILD_DIR" "$MOUNT_DIR"
|
||||
|
||||
# Step 1: Download Alpine cloud image
|
||||
if [[ -f "${BUILD_DIR}/${ALPINE_IMAGE}" ]]; then
|
||||
echo "Alpine image already downloaded."
|
||||
else
|
||||
echo "Downloading Alpine ${ALPINE_VERSION} cloud image..."
|
||||
if ! curl --fail -L -o "${BUILD_DIR}/${ALPINE_IMAGE}" "$ALPINE_URL"; then
|
||||
rm -f "${BUILD_DIR}/${ALPINE_IMAGE}"
|
||||
echo "ERROR: Failed to download Alpine image from $ALPINE_URL"
|
||||
exit 1
|
||||
fi
|
||||
# Verify the download is a valid qcow2 image
|
||||
if ! qemu-img info "${BUILD_DIR}/${ALPINE_IMAGE}" &>/dev/null; then
|
||||
rm -f "${BUILD_DIR}/${ALPINE_IMAGE}"
|
||||
echo "ERROR: Downloaded file is not a valid qcow2 image"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 2: Copy image for customization
|
||||
echo "Copying image for customization..."
|
||||
cp "${BUILD_DIR}/${ALPINE_IMAGE}" "$OUTPUT_IMAGE"
|
||||
|
||||
# Resize the image (Alpine cloud images are ~200MB, need room for packages)
|
||||
qemu-img resize "$OUTPUT_IMAGE" "$IMAGE_SIZE"
|
||||
|
||||
# Step 3: Connect via qemu-nbd
|
||||
echo "Connecting image via qemu-nbd..."
|
||||
sudo modprobe nbd max_part=4 2>/dev/null || true
|
||||
if [[ ! -e "${NBD_DEV}" ]]; then
|
||||
echo "ERROR: ${NBD_DEV} does not exist. Is the nbd kernel module loaded? Try: sudo modprobe nbd"
|
||||
exit 1
|
||||
fi
|
||||
sudo qemu-nbd --disconnect "${NBD_DEV}" 2>/dev/null || true
|
||||
sudo qemu-nbd --connect="${NBD_DEV}" "$OUTPUT_IMAGE"
|
||||
sleep 1
|
||||
|
||||
# Alpine cloud images have ext4 directly on the block device (no partition table).
|
||||
# Resize the filesystem to fill the resized image.
|
||||
echo "Checking filesystem..."
|
||||
sudo e2fsck -f -y "${NBD_DEV}" || {
|
||||
ec=$?
|
||||
if [[ $ec -gt 1 ]]; then
|
||||
echo "ERROR: e2fsck failed with exit code $ec on ${NBD_DEV}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Resizing filesystem..."
|
||||
if ! sudo resize2fs "${NBD_DEV}"; then
|
||||
echo "ERROR: resize2fs failed on ${NBD_DEV}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 4: Mount and customize
|
||||
echo "Mounting root filesystem..."
|
||||
sudo mount "${NBD_DEV}" "$MOUNT_DIR"
|
||||
|
||||
# Set up resolv.conf for DNS inside chroot
|
||||
sudo cp /etc/resolv.conf "${MOUNT_DIR}/etc/resolv.conf"
|
||||
|
||||
# Install packages
|
||||
if [[ -n "${APK_PACKAGES:-}" ]]; then
|
||||
echo "Installing packages: ${APK_PACKAGES}..."
|
||||
sudo chroot "$MOUNT_DIR" /bin/sh -c "
|
||||
set -e
|
||||
# Enable community repo
|
||||
sed -i 's|^#\(.*community\)|\1|' /etc/apk/repositories 2>/dev/null || true
|
||||
|
||||
# Ensure community repo is present
|
||||
if ! grep -q community /etc/apk/repositories; then
|
||||
MIRROR=\$(grep main /etc/apk/repositories | head -1 | sed 's|/main|/community|')
|
||||
echo \"\$MIRROR\" >> /etc/apk/repositories
|
||||
fi
|
||||
|
||||
apk update
|
||||
apk add ${APK_PACKAGES}
|
||||
"
|
||||
fi
|
||||
|
||||
# Enable OpenRC services
|
||||
if [[ -n "${ENABLE_SERVICES:-}" ]]; then
|
||||
echo "Enabling services: ${ENABLE_SERVICES}..."
|
||||
IFS=' ' read -ra SERVICES <<< "$ENABLE_SERVICES"
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
if ! sudo chroot "$MOUNT_DIR" /bin/sh -c "rc-update add ${svc} default"; then
|
||||
echo "ERROR: Failed to enable service '${svc}' — does it exist in the image?"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Step 5: Copy binaries into the image (before setup script, which may reference them)
|
||||
if [[ -n "${INSTALL_BINARIES:-}" ]]; then
|
||||
echo "Installing binaries..."
|
||||
IFS=' ' read -ra BINARY_PAIRS <<< "$INSTALL_BINARIES"
|
||||
for pair in "${BINARY_PAIRS[@]}"; do
|
||||
src="${pair%%:*}"
|
||||
dst="${pair#*:}"
|
||||
src_path="${PROJECT_DIR}/${src}"
|
||||
echo " ${src} -> ${dst}"
|
||||
sudo cp "$src_path" "${MOUNT_DIR}${dst}"
|
||||
sudo chmod 755 "${MOUNT_DIR}${dst}"
|
||||
done
|
||||
fi
|
||||
|
||||
# Run custom setup script inside chroot (after binaries are installed)
|
||||
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
|
||||
echo "Running setup script: ${SETUP_SCRIPT}..."
|
||||
sudo cp "$setup_path" "${MOUNT_DIR}/tmp/setup.sh"
|
||||
sudo chmod 755 "${MOUNT_DIR}/tmp/setup.sh"
|
||||
sudo chroot "$MOUNT_DIR" /tmp/setup.sh
|
||||
sudo rm -f "${MOUNT_DIR}/tmp/setup.sh"
|
||||
fi
|
||||
|
||||
# Step 6: Clean up and unmount
|
||||
echo "Cleaning up image..."
|
||||
sudo chroot "$MOUNT_DIR" /bin/sh -c '
|
||||
apk cache clean 2>/dev/null || true
|
||||
rm -rf /var/cache/apk/* /tmp/*
|
||||
'
|
||||
|
||||
# Restore original resolv.conf (cloud-init will set it on boot)
|
||||
sudo rm -f "${MOUNT_DIR}/etc/resolv.conf"
|
||||
|
||||
echo "Unmounting..."
|
||||
sudo umount "$MOUNT_DIR"
|
||||
sudo qemu-nbd --disconnect "${NBD_DEV}"
|
||||
|
||||
# Step 7: Convert to raw for import
|
||||
echo "Converting to raw format..."
|
||||
qemu-img convert -f qcow2 -O raw "$OUTPUT_IMAGE" "$OUTPUT_RAW"
|
||||
|
||||
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
|
||||
echo "Importing as AMI..."
|
||||
(cd "$PROJECT_DIR" && ./bin/spx admin images import \
|
||||
--file "$OUTPUT_RAW" \
|
||||
--distro alpine \
|
||||
--version "${ALPINE_VERSION}-${IMAGE_NAME}" \
|
||||
--arch x86_64 \
|
||||
--config "$HOME/spinifex/config/spinifex.toml")
|
||||
else
|
||||
echo "To import as AMI, run:"
|
||||
echo " cd $PROJECT_DIR && ./bin/spx admin images import \\"
|
||||
echo " --file $OUTPUT_RAW \\"
|
||||
echo " --distro alpine --version ${ALPINE_VERSION}-${IMAGE_NAME} --arch x86_64 \\"
|
||||
echo " --config \$HOME/spinifex/config/spinifex.toml"
|
||||
fi
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/bin/sh
|
||||
# alb-setup.sh — ALB-specific chroot setup
|
||||
#
|
||||
# Runs inside the Alpine image chroot after packages are installed.
|
||||
# Sets up HAProxy placeholder config and alb-agent OpenRC init script.
|
||||
|
||||
set -e
|
||||
|
||||
# Create haproxy config directory and placeholder config
|
||||
mkdir -p /etc/haproxy
|
||||
cat > /etc/haproxy/haproxy.cfg <<'EOF'
|
||||
# Placeholder config — replaced by alb-agent on first config push
|
||||
global
|
||||
daemon
|
||||
maxconn 256
|
||||
|
||||
defaults
|
||||
mode http
|
||||
timeout connect 5s
|
||||
timeout client 30s
|
||||
timeout server 30s
|
||||
|
||||
frontend health
|
||||
bind *:8405
|
||||
http-request return status 200 content-type text/plain string "ok"
|
||||
EOF
|
||||
|
||||
# Create alb-agent OpenRC init script
|
||||
mkdir -p /etc/init.d
|
||||
cat > /etc/init.d/alb-agent <<'INITSCRIPT'
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="ALB NATS Config Agent"
|
||||
command="/usr/local/bin/alb-agent"
|
||||
command_args="--lb-id=${ALB_LB_ID:-unknown} --nats=${ALB_NATS_URL:-nats://10.0.2.2:4222}"
|
||||
command_background=true
|
||||
pidfile="/run/alb-agent.pid"
|
||||
output_log="/var/log/alb-agent.log"
|
||||
error_log="/var/log/alb-agent.log"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
after firewall
|
||||
}
|
||||
INITSCRIPT
|
||||
chmod 755 /etc/init.d/alb-agent
|
||||
@@ -0,0 +1,22 @@
|
||||
# ALB system image manifest
|
||||
# Used by build-system-image.sh to create an Alpine AMI with HAProxy + alb-agent
|
||||
|
||||
IMAGE_NAME="alb"
|
||||
IMAGE_DESCRIPTION="ALB VM with HAProxy and alb-agent"
|
||||
ALPINE_VERSION="3.21.6"
|
||||
IMAGE_SIZE="512M"
|
||||
|
||||
# Packages to install via apk
|
||||
APK_PACKAGES="haproxy curl"
|
||||
|
||||
# Binaries to copy into the image (source:destination)
|
||||
INSTALL_BINARIES="bin/alb-agent:/usr/local/bin/alb-agent"
|
||||
|
||||
# Build command for binaries (run before image build)
|
||||
BUILD_COMMANDS="make build-alb-agent"
|
||||
|
||||
# OpenRC services to enable at boot
|
||||
ENABLE_SERVICES="haproxy"
|
||||
|
||||
# Custom setup script (runs inside chroot after packages are installed)
|
||||
SETUP_SCRIPT="scripts/images/alb-setup.sh"
|
||||
@@ -187,7 +187,11 @@ func (a *Agent) handlePing(msg *nats.Msg) {
|
||||
ConfigAge: configAge,
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(resp)
|
||||
data, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to marshal ping response", "err", err)
|
||||
return
|
||||
}
|
||||
if err := msg.Respond(data); err != nil {
|
||||
slog.Warn("Failed to respond to ping", "err", err)
|
||||
}
|
||||
@@ -202,7 +206,11 @@ func (a *Agent) respond(msg *nats.Msg, status, errMsg string) {
|
||||
if errMsg != "" {
|
||||
resp["error"] = errMsg
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
data, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to marshal response", "err", err)
|
||||
return
|
||||
}
|
||||
if err := msg.Respond(data); err != nil {
|
||||
slog.Warn("Failed to respond", "err", err)
|
||||
}
|
||||
@@ -212,12 +220,12 @@ func (a *Agent) respond(msg *nats.Msg, status, errMsg string) {
|
||||
// It writes to a temp file first, then renames for atomicity.
|
||||
func WriteConfig(path, content string) error {
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||
return fmt.Errorf("create config dir: %w", err)
|
||||
}
|
||||
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, []byte(content), 0o644); err != nil {
|
||||
if err := os.WriteFile(tmp, []byte(content), 0o600); err != nil {
|
||||
return fmt.Errorf("write temp config: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
@@ -233,7 +241,7 @@ func WriteConfig(path, content string) error {
|
||||
func reloadHAProxy(configPath, pidPath string) error {
|
||||
// Ensure the stats socket directory exists (the config may reference
|
||||
// /tmp/spinifex-haproxy/ which doesn't exist on fresh Alpine VMs).
|
||||
_ = os.MkdirAll("/tmp/spinifex-haproxy", 0755)
|
||||
_ = os.MkdirAll("/tmp/spinifex-haproxy", 0o750)
|
||||
|
||||
oldPID := readPID(pidPath)
|
||||
|
||||
|
||||
@@ -517,7 +517,7 @@ func TestQueryParamsToStruct_ELBv2RegisterTargets(t *testing.T) {
|
||||
// ELBv2 uses Targets.member.N.Id format (IAM-style query protocol)
|
||||
args := map[string]string{
|
||||
"Action": "RegisterTargets",
|
||||
"TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:000000000001:targetgroup/my-tg/tg-abc123",
|
||||
"TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:000000000001:targetgroup/my-tg/tg-abc123",
|
||||
"Targets.member.1.Id": "i-1234567890abcdef0",
|
||||
"Targets.member.2.Id": "i-0987654321fedcba0",
|
||||
}
|
||||
@@ -535,7 +535,7 @@ func TestQueryParamsToStruct_ELBv2RegisterTargets(t *testing.T) {
|
||||
func TestQueryParamsToStruct_ELBv2RegisterTargetsWithPort(t *testing.T) {
|
||||
args := map[string]string{
|
||||
"Action": "RegisterTargets",
|
||||
"TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:000000000001:targetgroup/my-tg/tg-abc123",
|
||||
"TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:000000000001:targetgroup/my-tg/tg-abc123",
|
||||
"Targets.member.1.Id": "i-1234567890abcdef0",
|
||||
"Targets.member.1.Port": "8080",
|
||||
}
|
||||
@@ -570,12 +570,12 @@ func TestQueryParamsToStruct_ELBv2CreateLoadBalancerMemberSubnets(t *testing.T)
|
||||
|
||||
func TestQueryParamsToStruct_ELBv2CreateListenerWithActions(t *testing.T) {
|
||||
args := map[string]string{
|
||||
"Action": "CreateListener",
|
||||
"LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:000000000001:loadbalancer/app/my-alb/lb-abc123",
|
||||
"Protocol": "HTTP",
|
||||
"Port": "80",
|
||||
"DefaultActions.member.1.Type": "forward",
|
||||
"DefaultActions.member.1.TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:000000000001:targetgroup/my-tg/tg-abc123",
|
||||
"Action": "CreateListener",
|
||||
"LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:000000000001:loadbalancer/app/my-alb/lb-abc123",
|
||||
"Protocol": "HTTP",
|
||||
"Port": "80",
|
||||
"DefaultActions.member.1.Type": "forward",
|
||||
"DefaultActions.member.1.TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:000000000001:targetgroup/my-tg/tg-abc123",
|
||||
}
|
||||
|
||||
input := &elbv2.CreateListenerInput{}
|
||||
|
||||
@@ -434,21 +434,21 @@ var (
|
||||
ErrorAccessDenied = "AccessDenied"
|
||||
|
||||
// ELBv2-specific error codes
|
||||
ErrorELBv2LoadBalancerNotFound = "LoadBalancerNotFound"
|
||||
ErrorELBv2TargetGroupNotFound = "TargetGroupNotFound"
|
||||
ErrorELBv2ListenerNotFound = "ListenerNotFound"
|
||||
ErrorELBv2DuplicateLoadBalancer = "DuplicateLoadBalancerName"
|
||||
ErrorELBv2DuplicateTargetGroup = "DuplicateTargetGroupName"
|
||||
ErrorELBv2DuplicateListener = "DuplicateListener"
|
||||
ErrorELBv2TooManyLoadBalancers = "TooManyLoadBalancers"
|
||||
ErrorELBv2TooManyTargetGroups = "TooManyTargetGroups"
|
||||
ErrorELBv2TooManyListeners = "TooManyListeners"
|
||||
ErrorELBv2TooManyTargets = "TooManyRegistrationsForTargetId"
|
||||
ErrorELBv2InvalidTarget = "InvalidTarget"
|
||||
ErrorELBv2TargetGroupInUse = "ResourceInUse"
|
||||
ErrorELBv2InvalidSecurityGroup = "InvalidSecurityGroup"
|
||||
ErrorELBv2InvalidScheme = "InvalidScheme"
|
||||
ErrorELBv2SubnetNotFound = "SubnetNotFound"
|
||||
ErrorELBv2LoadBalancerNotFound = "LoadBalancerNotFound"
|
||||
ErrorELBv2TargetGroupNotFound = "TargetGroupNotFound"
|
||||
ErrorELBv2ListenerNotFound = "ListenerNotFound"
|
||||
ErrorELBv2DuplicateLoadBalancer = "DuplicateLoadBalancerName"
|
||||
ErrorELBv2DuplicateTargetGroup = "DuplicateTargetGroupName"
|
||||
ErrorELBv2DuplicateListener = "DuplicateListener"
|
||||
ErrorELBv2TooManyLoadBalancers = "TooManyLoadBalancers"
|
||||
ErrorELBv2TooManyTargetGroups = "TooManyTargetGroups"
|
||||
ErrorELBv2TooManyListeners = "TooManyListeners"
|
||||
ErrorELBv2TooManyTargets = "TooManyRegistrationsForTargetId"
|
||||
ErrorELBv2InvalidTarget = "InvalidTarget"
|
||||
ErrorELBv2TargetGroupInUse = "ResourceInUse"
|
||||
ErrorELBv2InvalidSecurityGroup = "InvalidSecurityGroup"
|
||||
ErrorELBv2InvalidScheme = "InvalidScheme"
|
||||
ErrorELBv2SubnetNotFound = "SubnetNotFound"
|
||||
ErrorELBv2AvailabilityZoneNotSupported = "AvailabilityZoneNotSupported"
|
||||
)
|
||||
|
||||
@@ -890,20 +890,20 @@ var ErrorLookup = map[string]ErrorMessage{
|
||||
ErrorAccessDenied: {HTTPCode: 403, Message: "User is not authorized to perform this action."},
|
||||
|
||||
// ELBv2 error codes
|
||||
ErrorELBv2LoadBalancerNotFound: {HTTPCode: 400, Message: "One or more load balancers not found."},
|
||||
ErrorELBv2TargetGroupNotFound: {HTTPCode: 400, Message: "One or more target groups not found."},
|
||||
ErrorELBv2ListenerNotFound: {HTTPCode: 400, Message: "One or more listeners not found."},
|
||||
ErrorELBv2DuplicateLoadBalancer: {HTTPCode: 400, Message: "A load balancer with the same name already exists."},
|
||||
ErrorELBv2DuplicateTargetGroup: {HTTPCode: 400, Message: "A target group with the same name already exists."},
|
||||
ErrorELBv2DuplicateListener: {HTTPCode: 400, Message: "A listener with the specified port already exists on this load balancer."},
|
||||
ErrorELBv2TooManyLoadBalancers: {HTTPCode: 400, Message: "You've reached the limit on the number of load balancers for your account."},
|
||||
ErrorELBv2TooManyTargetGroups: {HTTPCode: 400, Message: "You've reached the limit on the number of target groups for your account."},
|
||||
ErrorELBv2TooManyListeners: {HTTPCode: 400, Message: "You've reached the limit on the number of listeners per load balancer."},
|
||||
ErrorELBv2TooManyTargets: {HTTPCode: 400, Message: "You've reached the limit on the number of times a target can be registered with a target group."},
|
||||
ErrorELBv2InvalidTarget: {HTTPCode: 400, Message: "The specified target does not exist, is not in the same VPC as the target group, or has an unsupported instance type."},
|
||||
ErrorELBv2TargetGroupInUse: {HTTPCode: 400, Message: "The target group is currently in use by a listener or rule."},
|
||||
ErrorELBv2InvalidSecurityGroup: {HTTPCode: 400, Message: "The specified security group does not exist."},
|
||||
ErrorELBv2InvalidScheme: {HTTPCode: 400, Message: "The specified scheme is not valid. Specify 'internet-facing' or 'internal'."},
|
||||
ErrorELBv2SubnetNotFound: {HTTPCode: 400, Message: "The specified subnet does not exist."},
|
||||
ErrorELBv2LoadBalancerNotFound: {HTTPCode: 400, Message: "One or more load balancers not found."},
|
||||
ErrorELBv2TargetGroupNotFound: {HTTPCode: 400, Message: "One or more target groups not found."},
|
||||
ErrorELBv2ListenerNotFound: {HTTPCode: 400, Message: "One or more listeners not found."},
|
||||
ErrorELBv2DuplicateLoadBalancer: {HTTPCode: 400, Message: "A load balancer with the same name already exists."},
|
||||
ErrorELBv2DuplicateTargetGroup: {HTTPCode: 400, Message: "A target group with the same name already exists."},
|
||||
ErrorELBv2DuplicateListener: {HTTPCode: 400, Message: "A listener with the specified port already exists on this load balancer."},
|
||||
ErrorELBv2TooManyLoadBalancers: {HTTPCode: 400, Message: "You've reached the limit on the number of load balancers for your account."},
|
||||
ErrorELBv2TooManyTargetGroups: {HTTPCode: 400, Message: "You've reached the limit on the number of target groups for your account."},
|
||||
ErrorELBv2TooManyListeners: {HTTPCode: 400, Message: "You've reached the limit on the number of listeners per load balancer."},
|
||||
ErrorELBv2TooManyTargets: {HTTPCode: 400, Message: "You've reached the limit on the number of times a target can be registered with a target group."},
|
||||
ErrorELBv2InvalidTarget: {HTTPCode: 400, Message: "The specified target does not exist, is not in the same VPC as the target group, or has an unsupported instance type."},
|
||||
ErrorELBv2TargetGroupInUse: {HTTPCode: 400, Message: "The target group is currently in use by a listener or rule."},
|
||||
ErrorELBv2InvalidSecurityGroup: {HTTPCode: 400, Message: "The specified security group does not exist."},
|
||||
ErrorELBv2InvalidScheme: {HTTPCode: 400, Message: "The specified scheme is not valid. Specify 'internet-facing' or 'internal'."},
|
||||
ErrorELBv2SubnetNotFound: {HTTPCode: 400, Message: "The specified subnet does not exist."},
|
||||
ErrorELBv2AvailabilityZoneNotSupported: {HTTPCode: 400, Message: "The specified Availability Zone is not supported."},
|
||||
}
|
||||
|
||||
@@ -47,10 +47,10 @@ type GatewayConfig struct {
|
||||
}
|
||||
|
||||
var supportedServices = map[string]bool{
|
||||
"ec2": true,
|
||||
"iam": true,
|
||||
"account": true,
|
||||
"elasticloadbalancing": true,
|
||||
"ec2": true,
|
||||
"iam": true,
|
||||
"account": true,
|
||||
"elasticloadbalancing": true,
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
|
||||
@@ -646,14 +646,14 @@ func (s *InstanceServiceImpl) createCloudInitISO(input *ec2.RunInstancesInput, i
|
||||
slog.Warn("Failed to decode user-data, ignoring", "err", decErr)
|
||||
} else {
|
||||
raw := string(decoded)
|
||||
if strings.HasPrefix(raw, "#cloud-config") {
|
||||
if after, ok := strings.CutPrefix(raw, "#cloud-config"); ok {
|
||||
// Strip the #cloud-config header — the template already has it
|
||||
stripped := strings.TrimPrefix(raw, "#cloud-config")
|
||||
stripped := after
|
||||
userData.UserDataCloudConfig = strings.TrimSpace(stripped)
|
||||
} else {
|
||||
// Script — indent each line by 4 spaces for YAML write_files block
|
||||
var indented strings.Builder
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
for line := range strings.SplitSeq(raw, "\n") {
|
||||
indented.WriteString(" " + line + "\n")
|
||||
}
|
||||
userData.UserDataScript = indented.String()
|
||||
|
||||
@@ -357,7 +357,7 @@ func (m *HAProxyManager) WriteConfig(lbID, configContent string) (string, error)
|
||||
}
|
||||
|
||||
path := filepath.Join(m.configDir, fmt.Sprintf("alb-%s.cfg", lbID))
|
||||
if err := os.WriteFile(path, []byte(configContent), 0o640); err != nil {
|
||||
if err := os.WriteFile(path, []byte(configContent), 0o600); err != nil {
|
||||
return "", fmt.Errorf("write haproxy config: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ func TestGenerateHAProxyConfig_SkipsDrainingTargets(t *testing.T) {
|
||||
|
||||
assert.Contains(t, config, "10.0.0.1:80")
|
||||
assert.NotContains(t, config, "10.0.0.2") // draining
|
||||
assert.NotContains(t, config, "i-no-ip") // no IP
|
||||
assert.NotContains(t, config, "i-no-ip") // no IP
|
||||
}
|
||||
|
||||
func TestGenerateHAProxyConfig_SharedTargetGroup(t *testing.T) {
|
||||
|
||||
@@ -49,11 +49,11 @@ type ELBv2ServiceImpl struct {
|
||||
store *Store
|
||||
nc *nats.Conn // NATS connection for publishing config to ALB VMs
|
||||
vpcService *handlers_ec2_vpc.VPCServiceImpl // nil-safe: ENI ops skipped when nil (e.g. in tests)
|
||||
instanceLauncher SystemInstanceLauncher // nil-safe: system VM ops skipped when nil
|
||||
instanceLauncher SystemInstanceLauncher // nil-safe: system VM ops skipped when nil
|
||||
nodeID string
|
||||
region string
|
||||
systemAMI string // AMI ID for system VMs (ALB VMs), set by daemon
|
||||
natsURL string // NATS URL for ALB VM cloud-init, set by daemon
|
||||
systemAMI string // AMI ID for system VMs (ALB VMs), set by daemon
|
||||
natsURL string // NATS URL for ALB VM cloud-init, set by daemon
|
||||
}
|
||||
|
||||
// NewELBv2ServiceImplWithNATS creates an ELBv2 service backed by JetStream KV.
|
||||
@@ -103,7 +103,6 @@ func (s *ELBv2ServiceImpl) SetNATSURL(url string) {
|
||||
s.natsURL = url
|
||||
}
|
||||
|
||||
|
||||
// resolveENIBindAddr looks up the private IP of the first ENI belonging to the ALB.
|
||||
// Returns empty string if VPC service is unavailable or no ENIs exist.
|
||||
func (s *ELBv2ServiceImpl) resolveENIBindAddr(lb *LoadBalancerRecord) string {
|
||||
@@ -364,9 +363,11 @@ func (s *ELBv2ServiceImpl) CreateLoadBalancer(input *elbv2.CreateLoadBalancerInp
|
||||
if eniErr != nil {
|
||||
// Rollback: delete any ENIs already created
|
||||
for _, rollbackENI := range eniIDs {
|
||||
s.vpcService.DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{
|
||||
if _, delErr := s.vpcService.DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{
|
||||
NetworkInterfaceId: aws.String(rollbackENI),
|
||||
}, accountID)
|
||||
}, accountID); delErr != nil {
|
||||
slog.Error("CreateLoadBalancer: rollback failed to delete ENI", "eni", rollbackENI, "err", delErr)
|
||||
}
|
||||
}
|
||||
slog.Error("CreateLoadBalancer: failed to create ENI", "subnet", subnetID, "err", eniErr)
|
||||
return nil, errors.New(awserrors.ErrorELBv2SubnetNotFound)
|
||||
@@ -1055,14 +1056,14 @@ func (s *ELBv2ServiceImpl) DescribeListeners(input *elbv2.DescribeListenersInput
|
||||
|
||||
func (s *ELBv2ServiceImpl) lbRecordToSDK(r *LoadBalancerRecord) *elbv2.LoadBalancer {
|
||||
lb := &elbv2.LoadBalancer{
|
||||
LoadBalancerArn: aws.String(r.LoadBalancerArn),
|
||||
LoadBalancerArn: aws.String(r.LoadBalancerArn),
|
||||
LoadBalancerName: aws.String(r.Name),
|
||||
DNSName: aws.String(r.DNSName),
|
||||
Scheme: aws.String(r.Scheme),
|
||||
Type: aws.String(r.Type),
|
||||
IpAddressType: aws.String(r.IPAddressType),
|
||||
CreatedTime: aws.Time(r.CreatedAt),
|
||||
VpcId: aws.String(r.VpcId),
|
||||
DNSName: aws.String(r.DNSName),
|
||||
Scheme: aws.String(r.Scheme),
|
||||
Type: aws.String(r.Type),
|
||||
IpAddressType: aws.String(r.IPAddressType),
|
||||
CreatedTime: aws.Time(r.CreatedAt),
|
||||
VpcId: aws.String(r.VpcId),
|
||||
State: &elbv2.LoadBalancerState{
|
||||
Code: aws.String(r.State),
|
||||
},
|
||||
@@ -1084,19 +1085,19 @@ func (s *ELBv2ServiceImpl) lbRecordToSDK(r *LoadBalancerRecord) *elbv2.LoadBalan
|
||||
|
||||
func (s *ELBv2ServiceImpl) tgRecordToSDK(r *TargetGroupRecord) *elbv2.TargetGroup {
|
||||
return &elbv2.TargetGroup{
|
||||
TargetGroupArn: aws.String(r.TargetGroupArn),
|
||||
TargetGroupName: aws.String(r.Name),
|
||||
Protocol: aws.String(r.Protocol),
|
||||
Port: aws.Int64(r.Port),
|
||||
VpcId: aws.String(r.VpcId),
|
||||
TargetType: aws.String(r.TargetType),
|
||||
HealthCheckProtocol: aws.String(r.HealthCheck.Protocol),
|
||||
HealthCheckPort: aws.String(r.HealthCheck.Port),
|
||||
HealthCheckPath: aws.String(r.HealthCheck.Path),
|
||||
TargetGroupArn: aws.String(r.TargetGroupArn),
|
||||
TargetGroupName: aws.String(r.Name),
|
||||
Protocol: aws.String(r.Protocol),
|
||||
Port: aws.Int64(r.Port),
|
||||
VpcId: aws.String(r.VpcId),
|
||||
TargetType: aws.String(r.TargetType),
|
||||
HealthCheckProtocol: aws.String(r.HealthCheck.Protocol),
|
||||
HealthCheckPort: aws.String(r.HealthCheck.Port),
|
||||
HealthCheckPath: aws.String(r.HealthCheck.Path),
|
||||
HealthCheckIntervalSeconds: aws.Int64(r.HealthCheck.IntervalSeconds),
|
||||
HealthCheckTimeoutSeconds: aws.Int64(r.HealthCheck.TimeoutSeconds),
|
||||
HealthyThresholdCount: aws.Int64(r.HealthCheck.HealthyThreshold),
|
||||
UnhealthyThresholdCount: aws.Int64(r.HealthCheck.UnhealthyThreshold),
|
||||
HealthyThresholdCount: aws.Int64(r.HealthCheck.HealthyThreshold),
|
||||
UnhealthyThresholdCount: aws.Int64(r.HealthCheck.UnhealthyThreshold),
|
||||
Matcher: &elbv2.Matcher{
|
||||
HttpCode: aws.String(r.HealthCheck.Matcher),
|
||||
},
|
||||
|
||||
@@ -43,8 +43,8 @@ func TestCreateLoadBalancer(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
out, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
Name: aws.String("my-alb"),
|
||||
Subnets: []*string{aws.String("subnet-aaa")},
|
||||
Name: aws.String("my-alb"),
|
||||
Subnets: []*string{aws.String("subnet-aaa")},
|
||||
SecurityGroups: []*string{aws.String("sg-111")},
|
||||
}, testAccountID)
|
||||
|
||||
@@ -640,4 +640,3 @@ func (m *mockTerminateLauncher) TerminateSystemInstance(instanceID string) error
|
||||
m.terminateCalls = append(m.terminateCalls, instanceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -50,18 +50,18 @@ type LoadBalancerRecord struct {
|
||||
LoadBalancerID string `json:"load_balancer_id"` // Short ID (hex suffix)
|
||||
DNSName string `json:"dns_name"`
|
||||
Name string `json:"name"`
|
||||
Scheme string `json:"scheme"` // "internet-facing" or "internal"
|
||||
Type string `json:"type"` // Always "application"
|
||||
State string `json:"state"` // "provisioning", "active", "failed"
|
||||
Scheme string `json:"scheme"` // "internet-facing" or "internal"
|
||||
Type string `json:"type"` // Always "application"
|
||||
State string `json:"state"` // "provisioning", "active", "failed"
|
||||
VpcId string `json:"vpc_id"`
|
||||
SecurityGroups []string `json:"security_groups"`
|
||||
Subnets []string `json:"subnets"`
|
||||
AvailZones []AvailZoneInfo `json:"availability_zones"`
|
||||
ENIs []string `json:"enis,omitempty"` // ENI IDs created for this ALB (internal)
|
||||
ENIs []string `json:"enis,omitempty"` // ENI IDs created for this ALB (internal)
|
||||
InstanceID string `json:"instance_id,omitempty"` // ALB VM instance ID (system-managed)
|
||||
HostPorts map[int]int `json:"host_ports,omitempty"` // Dev mode: guest port → host port forwarding
|
||||
NodeID string `json:"node_id"` // Daemon node running this ALB
|
||||
IPAddressType string `json:"ip_address_type"` // "ipv4"
|
||||
HostPorts map[int]int `json:"host_ports,omitempty"` // Dev mode: guest port → host port forwarding
|
||||
NodeID string `json:"node_id"` // Daemon node running this ALB
|
||||
IPAddressType string `json:"ip_address_type"` // "ipv4"
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
AccountID string `json:"account_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
@@ -78,10 +78,10 @@ type TargetGroupRecord struct {
|
||||
TargetGroupArn string `json:"target_group_arn"`
|
||||
TargetGroupID string `json:"target_group_id"` // Short ID (hex suffix)
|
||||
Name string `json:"name"`
|
||||
Protocol string `json:"protocol"` // "HTTP" or "HTTPS"
|
||||
Port int64 `json:"port"` // Default target port
|
||||
Protocol string `json:"protocol"` // "HTTP" or "HTTPS"
|
||||
Port int64 `json:"port"` // Default target port
|
||||
VpcId string `json:"vpc_id"`
|
||||
TargetType string `json:"target_type"` // "instance" for v1
|
||||
TargetType string `json:"target_type"` // "instance" for v1
|
||||
HealthCheck HealthCheckConfig `json:"health_check"`
|
||||
Targets []Target `json:"targets"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
@@ -91,14 +91,14 @@ type TargetGroupRecord struct {
|
||||
|
||||
// HealthCheckConfig defines health check parameters for a target group.
|
||||
type HealthCheckConfig struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Port string `json:"port"` // Port number or "traffic-port"
|
||||
Path string `json:"path"`
|
||||
IntervalSeconds int64 `json:"interval_seconds"`
|
||||
TimeoutSeconds int64 `json:"timeout_seconds"`
|
||||
HealthyThreshold int64 `json:"healthy_threshold"`
|
||||
UnhealthyThreshold int64 `json:"unhealthy_threshold"`
|
||||
Matcher string `json:"matcher"` // HTTP codes e.g. "200" or "200-299"
|
||||
Protocol string `json:"protocol"`
|
||||
Port string `json:"port"` // Port number or "traffic-port"
|
||||
Path string `json:"path"`
|
||||
IntervalSeconds int64 `json:"interval_seconds"`
|
||||
TimeoutSeconds int64 `json:"timeout_seconds"`
|
||||
HealthyThreshold int64 `json:"healthy_threshold"`
|
||||
UnhealthyThreshold int64 `json:"unhealthy_threshold"`
|
||||
Matcher string `json:"matcher"` // HTTP codes e.g. "200" or "200-299"
|
||||
}
|
||||
|
||||
// DefaultHealthCheck returns a HealthCheckConfig with AWS default values.
|
||||
@@ -138,6 +138,6 @@ type ListenerRecord struct {
|
||||
|
||||
// ListenerAction defines a listener's default action.
|
||||
type ListenerAction struct {
|
||||
Type string `json:"type"` // "forward"
|
||||
Type string `json:"type"` // "forward"
|
||||
TargetGroupArn string `json:"target_group_arn"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user