feat(railiance): implement CUST-WP-0032 Haskell build machine infra
Packer build definition, cloud-init autoinstall, GHCup toolchain script, boot-time registration agent (state-hub + autossh dual tunnel), systemd unit, key injection, remote-build Makefile, smoke test, and deployment README. All 15 tasks complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
65
infra/build-machines/haskell/scripts/inject-keys.sh
Executable file
65
infra/build-machines/haskell/scripts/inject-keys.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
# inject-keys.sh — Post-boot SSH key and env injection for new VMs (Option B)
|
||||
#
|
||||
# Usage: inject-keys.sh <vm-ip> [key-dir]
|
||||
#
|
||||
# Expects the following files in key-dir (default: current directory):
|
||||
# - id_build (private key for SSH tunnel)
|
||||
# - id_build.pub (public key)
|
||||
# - build-agent.env (filled-in env config — see build-agent.env.template)
|
||||
#
|
||||
# The VM must be running with temporary password auth enabled (as built by Packer).
|
||||
# After injection, password auth is disabled and key-only access takes effect.
|
||||
set -euo pipefail
|
||||
|
||||
VM_IP="${1:?Usage: inject-keys.sh <vm-ip> [key-dir]}"
|
||||
KEY_DIR="${2:-.}"
|
||||
BUILD_USER="build"
|
||||
|
||||
echo "==> Injecting keys to ${BUILD_USER}@${VM_IP} from ${KEY_DIR}"
|
||||
|
||||
# Verify required files exist
|
||||
for f in id_build id_build.pub build-agent.env; do
|
||||
if [ ! -f "${KEY_DIR}/${f}" ]; then
|
||||
echo "ERROR: Missing ${KEY_DIR}/${f}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Create .ssh directory on VM
|
||||
ssh -o StrictHostKeyChecking=no "${BUILD_USER}@${VM_IP}" \
|
||||
"mkdir -p ~/.ssh && chmod 700 ~/.ssh"
|
||||
|
||||
# Copy SSH keys
|
||||
scp -o StrictHostKeyChecking=no \
|
||||
"${KEY_DIR}/id_build" "${KEY_DIR}/id_build.pub" \
|
||||
"${BUILD_USER}@${VM_IP}:~/.ssh/"
|
||||
|
||||
# Set correct permissions on private key
|
||||
ssh -o StrictHostKeyChecking=no "${BUILD_USER}@${VM_IP}" \
|
||||
"chmod 600 ~/.ssh/id_build && chmod 644 ~/.ssh/id_build.pub"
|
||||
|
||||
# Add the tunnel target's host key to known_hosts (optional — agent uses
|
||||
# StrictHostKeyChecking=no, but this avoids warnings in manual SSH)
|
||||
echo "==> Adding workstation public key to authorized_keys"
|
||||
ssh -o StrictHostKeyChecking=no "${BUILD_USER}@${VM_IP}" \
|
||||
"cat ~/.ssh/id_build.pub >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
|
||||
|
||||
# Copy build-agent.env to /etc (requires sudo)
|
||||
echo "==> Installing build-agent.env"
|
||||
scp -o StrictHostKeyChecking=no \
|
||||
"${KEY_DIR}/build-agent.env" "${BUILD_USER}@${VM_IP}:/tmp/build-agent.env"
|
||||
ssh -o StrictHostKeyChecking=no "${BUILD_USER}@${VM_IP}" \
|
||||
"sudo cp /tmp/build-agent.env /etc/build-agent.env && sudo chmod 600 /etc/build-agent.env && rm /tmp/build-agent.env"
|
||||
|
||||
# Disable password auth (now that keys are in place)
|
||||
echo "==> Disabling password authentication"
|
||||
ssh -o StrictHostKeyChecking=no "${BUILD_USER}@${VM_IP}" \
|
||||
"sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config && sudo systemctl restart sshd"
|
||||
|
||||
# Restart build-agent to pick up new env
|
||||
echo "==> Restarting build-agent service"
|
||||
ssh -o StrictHostKeyChecking=no -i "${KEY_DIR}/id_build" "${BUILD_USER}@${VM_IP}" \
|
||||
"sudo systemctl restart build-agent"
|
||||
|
||||
echo "==> Done. VM is ready. Test with: ssh -i ${KEY_DIR}/id_build ${BUILD_USER}@${VM_IP}"
|
||||
22
infra/build-machines/haskell/scripts/install-agent.sh
Executable file
22
infra/build-machines/haskell/scripts/install-agent.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Copy agent artefacts (provisioned by Packer file provisioner)
|
||||
install -m 0755 /tmp/build-agent.py /usr/local/bin/build-agent
|
||||
install -m 0644 /tmp/build-agent.service /etc/systemd/system/build-agent.service
|
||||
install -m 0600 /tmp/build-agent.env.template /etc/build-agent.env.template
|
||||
|
||||
# Placeholder env file — operator fills this in before first boot
|
||||
if [ ! -f /etc/build-agent.env ]; then
|
||||
cp /etc/build-agent.env.template /etc/build-agent.env
|
||||
fi
|
||||
|
||||
# Install autossh
|
||||
apt-get install -y -qq autossh
|
||||
|
||||
# Enable agent service (starts on boot, after network-online)
|
||||
systemctl daemon-reload
|
||||
systemctl enable build-agent.service
|
||||
|
||||
# SSH host key generation (deterministic at first boot, not baked in image)
|
||||
dpkg-reconfigure openssh-server
|
||||
41
infra/build-machines/haskell/scripts/install-haskell.sh
Executable file
41
infra/build-machines/haskell/scripts/install-haskell.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# System deps (already installed via cloud-init but idempotent)
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq build-essential curl git \
|
||||
libgmp-dev libffi-dev zlib1g-dev libncurses-dev libtinfo-dev pkg-config
|
||||
|
||||
# GHCup — non-interactive bootstrap
|
||||
# Primary version (9.8.4) is the default; secondary (9.6.6) covers LTS 22/23.
|
||||
# Skip Stack (cabal covers 95% of projects) and HLS (saves ~2 GB image size).
|
||||
GHC_PRIMARY="${GHC_PRIMARY_VERSION:-9.8.4}"
|
||||
GHC_SECONDARY="${GHC_SECONDARY_VERSION:-9.6.6}"
|
||||
CABAL_VERSION="${CABAL_VERSION:-3.12.1.0}"
|
||||
|
||||
export BOOTSTRAP_HASKELL_NONINTERACTIVE=1
|
||||
export BOOTSTRAP_HASKELL_GHC_VERSION="$GHC_PRIMARY"
|
||||
export BOOTSTRAP_HASKELL_CABAL_VERSION="$CABAL_VERSION"
|
||||
export BOOTSTRAP_HASKELL_INSTALL_STACK=0 # not needed; cabal suffices
|
||||
export BOOTSTRAP_HASKELL_INSTALL_HLS=0 # ~2 GB — skip for build-only image
|
||||
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org \
|
||||
| runuser -l build -c 'sh -s -- --no-modify-path'
|
||||
|
||||
# Add ghcup env to build user profile
|
||||
echo '. "$HOME/.ghcup/env"' >> /home/build/.bashrc
|
||||
echo '. "$HOME/.ghcup/env"' >> /home/build/.profile
|
||||
|
||||
# Install secondary GHC version (~500 MB, shared GHCup base — worth it)
|
||||
runuser -l build -c "source ~/.ghcup/env && ghcup install ghc $GHC_SECONDARY"
|
||||
|
||||
# Ensure primary is the default
|
||||
runuser -l build -c "source ~/.ghcup/env && ghcup set ghc $GHC_PRIMARY"
|
||||
|
||||
# Pre-warm cabal package db (saves 2-3 min on first real build)
|
||||
runuser -l build -c 'source ~/.ghcup/env && cabal update'
|
||||
|
||||
# Verify both versions present
|
||||
runuser -l build -c "source ~/.ghcup/env && ghc --version && cabal --version"
|
||||
runuser -l build -c "source ~/.ghcup/env && ghcup run --ghc $GHC_SECONDARY -- ghc --version"
|
||||
13
infra/build-machines/haskell/scripts/setup-vm.sh
Executable file
13
infra/build-machines/haskell/scripts/setup-vm.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# setup-vm.sh — switches imported VM from NAT to bridged networking
|
||||
VM_NAME="${1:?Usage: setup-vm.sh <vm-name> [adapter]}"
|
||||
# Auto-detect first available bridge interface if not specified
|
||||
ADAPTER="${2:-$(VBoxManage list bridgedifs | awk '/^Name:/{print $2; exit}')}"
|
||||
|
||||
VBoxManage modifyvm "$VM_NAME" \
|
||||
--nic1 bridged \
|
||||
--bridgeadapter1 "$ADAPTER" \
|
||||
--memory 8192 --cpus 4
|
||||
|
||||
echo "Configured $VM_NAME: bridged on $ADAPTER"
|
||||
echo "Next: inject keys with scripts/inject-keys.sh, then start VM"
|
||||
Reference in New Issue
Block a user