diff --git a/docker/linux/Dockerfile b/docker/linux/Dockerfile index 75525462a..1d98a7c6e 100644 --- a/docker/linux/Dockerfile +++ b/docker/linux/Dockerfile @@ -1,31 +1,18 @@ -FROM debian:11-slim +FROM debian:12-slim ENV ASPNETCORE_URLS=http://+:80 DOTNET_RUNNING_IN_CONTAINER=true RUN apt-get update && \ apt-get install -y --no-install-recommends \ ca-certificates \ - libc6 \ - libgcc1 \ libgssapi-krb5-2 \ - libicu67 \ - libssl1.1 \ - libstdc++6 \ - zlib1g && \ + libicu72 \ + libssl3 \ + xxd && \ + apt-get clean && \ rm -rf /var/lib/apt/lists/* ARG BUILD_NUMBER ARG BUILD_DATE -RUN apt-get update && \ - apt-get install -y \ - curl \ - dos2unix \ - jq \ - sudo \ - xxd \ - && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - EXPOSE 10933 WORKDIR /tmp @@ -43,11 +30,11 @@ RUN /install-scripts/install-docker.sh # Install Tentacle COPY _artifacts/deb/tentacle_${BUILD_NUMBER}_amd64.deb /tmp/ RUN apt-get update && \ - apt install ./tentacle_${BUILD_NUMBER}_amd64.deb && \ + apt-get install -y --no-install-recommends ./tentacle_${BUILD_NUMBER}_amd64.deb && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ ln -s /opt/octopus/tentacle/Tentacle /usr/bin/tentacle - + WORKDIR / # We know this won't reduce the image size at all. It's just to make the filesystem a little tidier. diff --git a/scripts/smoke-test-linux-tentacle.sh b/scripts/smoke-test-linux-tentacle.sh new file mode 100755 index 000000000..f5bf087ed --- /dev/null +++ b/scripts/smoke-test-linux-tentacle.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash +# +# End-to-end smoke test for the Linux Tentacle Docker image (EFT-3311). +# +# Builds the image from the .deb in _artifacts/deb, brings up a local Octopus +# Server in the sibling OctopusDeploy repo, registers the Tentacle as a worker, +# runs a hello-world AdHocScript on it, and asserts success. +# +# Required tools: docker, curl, jq. +# Required state: a built .deb in ../_artifacts/deb/tentacle_*_amd64.deb and the +# OctopusDeploy repo checked out alongside OctopusTentacle. +# +# License source: set $OCTOPUS_LICENSE_BASE64 to a base64-encoded Octopus license +# to skip the 1Password lookup (this is the path CI runners should use). When +# the env var is unset, the script falls back to `op read` against 1Password +# for local-dev use, in which case `op` must be installed and signed in. +# +# Note on $API_KEY below: "API-APIKEY01" is the well-known dev sentinel API key +# provisioned by the sibling OctopusDeploy repo's docker-compose stack for its +# local-only Server instance. It is not a real secret and is safe to commit. + +set -euo pipefail + +TENTACLE_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SERVER_REPO="${SERVER_REPO:-$(cd "$TENTACLE_REPO/../OctopusDeploy" && pwd)}" +ENV_FILE="$SERVER_REPO/.env" +# .env backup path is assigned via mktemp in Step 2 (after we know $ENV_FILE +# exists). Using a unique per-run path avoids clobbering a stale backup from +# a previously-crashed run. +ENV_BACKUP="" +# Transient compose override: disables Docker-in-Docker on the Tentacle. The +# default tentacle entrypoint launches a dockerd daemon, which requires the +# container to run with `--privileged`; without that the daemon fails and its +# wrapper script kills the Tentacle agent. Setting DISABLE_DIND=Y skips it. +# Created via mktemp in Step 4 so we never clobber an unrelated file the user +# may already have in the sibling repo. +OVERRIDE_COMPOSE="" + +API="http://localhost:8065/api" +API_KEY="API-APIKEY01" +H="X-Octopus-ApiKey: $API_KEY" +IMAGE_TAG="smoke-debian12" +ONEPASSWORD_LICENSE_REF="op://software licencing/octopus deploy ultimate license key base64/value" + +# Per-run worker name. Tagging the worker with a unique name (rather than +# relying on the container hostname / a "highest Workers-N" heuristic) keeps +# the test idempotent across reused Server DB volumes and lets teardown find +# the exact worker this run registered. +WORKER_TARGET_NAME="smoke-tentacle-$(date +%Y%m%d-%H%M%S)-$$" +# Populated in Step 5 once the Server confirms registration; used by teardown +# to deregister the worker via DELETE so the workers list doesn't grow +# monotonically across runs that share a Server DB volume. +WORKER_ID="" + +log() { printf '\033[1;34m[smoke]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[smoke]\033[0m %s\n' "$*" >&2; } +die() { printf '\033[1;31m[smoke]\033[0m %s\n' "$*" >&2; exit 1; } + +require() { command -v "$1" >/dev/null || die "Missing required tool: $1"; } +require docker +require curl +require jq +# `op` is only required when OCTOPUS_LICENSE_BASE64 is not pre-set (local-dev path). +[[ -n "${OCTOPUS_LICENSE_BASE64:-}" ]] || require op + +teardown() { + local exit_code=$? + log "--- teardown ---" + # Deregister the worker first, while the Server is still up. Best-effort: + # if the Server is already dead or the worker never registered, we just + # move on — the goal is to keep the workers list clean across runs. + if [[ -n "$WORKER_ID" ]]; then + log "Deregistering worker $WORKER_ID" + curl -fsS -X DELETE -H "$H" "$API/workers/$WORKER_ID" >/dev/null 2>&1 || true + fi + if [[ -n "$OVERRIDE_COMPOSE" && -f "$OVERRIDE_COMPOSE" ]]; then + (cd "$SERVER_REPO" && docker compose -f docker-compose.yml -f "$OVERRIDE_COMPOSE" --profile tentacle down 2>/dev/null) || true + rm -f "$OVERRIDE_COMPOSE" + fi + (cd "$SERVER_REPO" && docker compose down 2>/dev/null) || true + if [[ -n "$ENV_BACKUP" && -f "$ENV_BACKUP" ]]; then + mv "$ENV_BACKUP" "$ENV_FILE" + log "Restored $ENV_FILE" + fi + exit "$exit_code" +} +trap teardown EXIT + +############################################################################### +# Step 1: Build the Linux Tentacle image from the local .deb +############################################################################### +log "--- Step 1: build Tentacle image ---" +cd "$TENTACLE_REPO" + +shopt -s nullglob +DEBS=(_artifacts/deb/tentacle_*_amd64.deb) +shopt -u nullglob +[[ ${#DEBS[@]} -ge 1 ]] || die "No .deb found in _artifacts/deb/. Build it first." +[[ ${#DEBS[@]} -eq 1 ]] || die "Multiple .debs in _artifacts/deb/; expected one: ${DEBS[*]}" +DEB_FILE="${DEBS[0]}" +DEB_BASENAME="$(basename "$DEB_FILE")" +BUILD_NUMBER="${DEB_BASENAME#tentacle_}" +BUILD_NUMBER="${BUILD_NUMBER%_amd64.deb}" +export BUILD_NUMBER +export BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + +log "BUILD_NUMBER=$BUILD_NUMBER" +# Use `docker build` directly rather than `docker compose -f docker-compose.build.yml` +# because that compose file also defines kubernetes/windows tentacle services which +# require extra env vars (BUILD_ARCH, BUILD_VARIANT) we don't care about here. +DST_IMAGE="octopusdeploy/tentacle:${IMAGE_TAG}" +docker build \ + --platform linux/amd64 \ + --build-arg BUILD_NUMBER="$BUILD_NUMBER" \ + --build-arg BUILD_DATE="$BUILD_DATE" \ + -f docker/linux/Dockerfile \ + -t "$DST_IMAGE" \ + . +log "Built $DST_IMAGE" + +############################################################################### +# Step 2: Resolve license & patch .env +############################################################################### +log "--- Step 2: resolve license and patch .env ---" +[[ -f "$ENV_FILE" ]] || die "Expected $ENV_FILE to exist." + +if [[ -n "${OCTOPUS_LICENSE_BASE64:-}" ]]; then + LICENSE_BASE64="$OCTOPUS_LICENSE_BASE64" + log "Using license from \$OCTOPUS_LICENSE_BASE64 (${#LICENSE_BASE64} bytes)" +else + if ! op account list >/dev/null 2>&1; then + die "1Password CLI is not signed in. Run: eval \$(op signin) — or pre-set \$OCTOPUS_LICENSE_BASE64." + fi + LICENSE_BASE64="$(op read "$ONEPASSWORD_LICENSE_REF" 2>/dev/null || true)" + [[ -n "$LICENSE_BASE64" ]] || die "Could not read license from 1Password at: $ONEPASSWORD_LICENSE_REF" + log "Fetched license from 1Password (${#LICENSE_BASE64} bytes)" +fi + +ENV_BACKUP="$(mktemp "${TMPDIR:-/tmp}/octopus-server-env-smoke-tentacle-XXXXXX")" +cp "$ENV_FILE" "$ENV_BACKUP" +log "Backed up .env to $ENV_BACKUP (will be restored on exit)" + +upsert_env_var() { + # Pure-bash: avoids sed/awk escape headaches with a base64 value (which + # contains '/' and '=' but not '\' or '&'). Matches the line by literal + # "KEY=" prefix, not regex, so unusual keys won't bite us. + local key="$1" value="$2" + local tmp line found= + tmp="$(mktemp "${TMPDIR:-/tmp}/octopus-server-env-smoke-upsert-XXXXXX")" + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" == "${key}="* ]]; then + printf '%s=%s\n' "$key" "$value" >> "$tmp" + found=1 + else + printf '%s\n' "$line" >> "$tmp" + fi + done < "$ENV_FILE" + [[ -z "$found" ]] && printf '%s=%s\n' "$key" "$value" >> "$tmp" + mv "$tmp" "$ENV_FILE" +} + +upsert_env_var TENTACLE_TAG "$IMAGE_TAG" +upsert_env_var OCTOPUS_SERVER_BASE64_LICENSE "$LICENSE_BASE64" + +############################################################################### +# Step 3: Bring up Octopus Server and wait for /api to respond +############################################################################### +log "--- Step 3: start octopus-server ---" +cd "$SERVER_REPO" +docker compose up -d octopus-server + +log "Waiting for $API/octopusservernodes/ping ..." +for i in {1..120}; do + if curl -fsS -H "$H" "$API/octopusservernodes/ping" >/dev/null 2>&1; then + log "Server is up after ${i}s" + break + fi + [[ $i -eq 120 ]] && die "Server did not become ready in 120s" + sleep 1 +done + +############################################################################### +# Step 4: Bring up the Tentacle (Worker, polling mode, DIND disabled) +############################################################################### +log "--- Step 4: start tentacle ---" +OVERRIDE_COMPOSE="$(mktemp "${TMPDIR:-/tmp}/docker-compose-smoke-tentacle-XXXXXX")" +cat > "$OVERRIDE_COMPOSE" </dev/null | grep -qF "Configuration successful."; then + log "Tentacle registered after ${i}s" + break + fi + [[ $i -eq 60 ]] && die "Tentacle did not register in 60s. Logs: +$("${COMPOSE[@]}" logs --no-color --tail=80 tentacle)" + sleep 1 +done + +# Make sure the agent is still running (the wrapper script can exit shortly +# after registration if a sidecar like dockerd dies). +if ! "${COMPOSE[@]}" ps --status running --services 2>/dev/null | grep -qx tentacle; then + die "Tentacle container exited shortly after registration. Logs: +$("${COMPOSE[@]}" logs --no-color --tail=80 tentacle)" +fi + +############################################################################### +# Step 5: Verify worker is registered & run hello-world AdHocScript +############################################################################### +log "--- Step 5: verify registration and run hello-world ---" + +# Find the worker we just registered by its per-run TargetName. This is +# robust against reused Server DB volumes (where workers list grows across +# runs) and avoids the previous "highest Workers-N" heuristic. +for i in {1..60}; do + WORKERS_JSON="$(curl -fsS -H "$H" --data-urlencode "name=$WORKER_TARGET_NAME" -G "$API/workers" 2>/dev/null || echo '{"Items":[]}')" + WORKER_ID="$(echo "$WORKERS_JSON" \ + | jq -r --arg name "$WORKER_TARGET_NAME" '.Items[] | select(.Name == $name) | .Id' \ + | head -n1)" + [[ -n "$WORKER_ID" ]] && break + sleep 1 +done +if [[ -z "$WORKER_ID" ]]; then + warn "No worker named '$WORKER_TARGET_NAME' appeared. Diagnostic dump of $API/workers:" + curl -fsS -H "$H" "$API/workers" || true + warn "Tentacle container logs (tail 80):" + docker compose --profile tentacle logs --no-color --tail=80 tentacle || true + die "Worker '$WORKER_TARGET_NAME' did not appear after 60s" +fi +log "Registered worker: $WORKER_ID (name='$WORKER_TARGET_NAME')" + +ADHOC_BODY="$(jq -nc \ + --arg id "$WORKER_ID" \ + '{ + Name: "AdHocScript", + Description: "EFT-3311 Debian 12 smoke test", + Arguments: { + ScriptBody: "echo Hello from $(hostname); cat /etc/os-release | head -2", + Syntax: "Bash", + WorkerIds: [$id] + } + }')" + +TASK_RESP="$(curl -fsS -X POST -H "$H" -H "Content-Type: application/json" \ + "$API/tasks" -d "$ADHOC_BODY")" +TASK_ID="$(echo "$TASK_RESP" | jq -r '.Id')" +[[ -n "$TASK_ID" && "$TASK_ID" != "null" ]] || die "Could not submit AdHocScript task. Response: $TASK_RESP" +log "Submitted task: $TASK_ID" + +STATE="" +for i in {1..120}; do + STATE="$(curl -fsS -H "$H" "$API/tasks/$TASK_ID" | jq -r '.State')" + echo " task=$TASK_ID state=$STATE" + case "$STATE" in + Success|Failed|Canceled|TimedOut) break ;; + esac + sleep 2 +done + +log "--- Task log ---" +curl -fsS -H "$H" "$API/tasks/$TASK_ID/raw" || true +log "--- end task log ---" + +if [[ "$STATE" != "Success" ]]; then + die "Task finished in state '$STATE' (expected Success)" +fi + +# Load-bearing assertion: the whole point of this smoke test is to prove the +# Debian 12 base image is what's actually running on the Tentacle, so a missing +# os-release line is a hard failure, not a warning. +if ! curl -fsS -H "$H" "$API/tasks/$TASK_ID/raw" | grep -qF 'Debian GNU/Linux 12'; then + die "Task succeeded but the log does NOT mention 'Debian GNU/Linux 12'. Inspect output above." +fi + +log "PASS — Tentacle (Debian 12) registered and executed hello-world."