#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" MODE="${MODE:-tcp}" ACTION="up" DETACH="${DETACH:-1}" BUILD="${BUILD:-1}" CLIENTS="${CLIENTS:-single}" TARGET_PLATFORM="${TARGET_PLATFORM:-linux/arm64}" CLIENT_IMAGE="${CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}}" BUILDX_PUSH="${BUILDX_PUSH:-0}" BUILDX_BUILDER="${BUILDX_BUILDER:-}" SKIP_BASE_IMAGE_PULL="${SKIP_BASE_IMAGE_PULL:-0}" usage() { cat <<'EOF' Usage: deploy/client/build-run.sh [action] [options] Actions: up Build and run debug client (default) build Build debug client image buildx Build debug client image with docker buildx rebuild Rebuild image without cache, then run client down|stop Stop client compose stack restart Stop, then build and run client logs Follow client logs ps Show client compose status Options: --mode MODE base|tcp|tls|ssh (default: tcp) --clients MODE single|multi (default: single) --platform VALUE Target platform for buildx (default: linux/arm64) --client-image TAG Client image tag for buildx and compose --push Push buildx image instead of loading it locally --builder NAME buildx builder name --skip-base-pull Do not pull Dockerfile base images before buildx --foreground Run single client in foreground --no-build Start without --build -h|--help Show this help EOF } die() { echo "error: $*" >&2 exit 1 } has_compose() { docker compose version >/dev/null 2>&1 } has_buildx() { docker buildx version >/dev/null 2>&1 } pull_base_images() { [[ "${SKIP_BASE_IMAGE_PULL}" == "1" ]] && return local platform image local -a platforms images IFS=',' read -r -a platforms <<< "${TARGET_PLATFORM}" images=(rust:1.89-bookworm debian:bookworm-slim) for platform in "${platforms[@]}"; do for image in "${images[@]}"; do docker pull --platform "${platform}" "${image}" done done } compose_files() { if [[ "${CLIENTS}" == "multi" ]]; then [[ "${MODE}" == "tcp" || "${MODE}" == "base" ]] || die "--clients multi is only defined for tcp/base mode" printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.clients.yml" return fi case "${MODE}" in base) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.yml" ;; tcp) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.tcp.yml" ;; tls) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.tls.yml" ;; ssh) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.ssh.yml" ;; *) die "unsupported mode '${MODE}', expected base|tcp|tls|ssh" ;; esac } compose_args() { local -a args files args=() [[ -f "${SCRIPT_DIR}/.env" ]] && args+=(--env-file "${SCRIPT_DIR}/.env") mapfile -t files < <(compose_files) args+=("${files[@]}") printf '%s\n' "${args[@]}" } compose_run() { local -a args mapfile -t args < <(compose_args) docker compose "${args[@]}" "$@" } up_client() { local -a up_args up_args=(up) [[ "${CLIENTS}" == "multi" || "${DETACH}" == "1" ]] && up_args+=(-d) [[ "${BUILD}" == "1" && "${CLIENTS}" != "multi" ]] && up_args+=(--build) compose_run "${up_args[@]}" } build_client() { if [[ "${CLIENTS}" == "multi" ]]; then docker build "$@" -f "${SCRIPT_DIR}/Dockerfile" -t "${CLIENT_IMAGE}" "${REPO_ROOT}" else compose_run build "$@" fi } buildx_client() { has_buildx || die "docker buildx is not available" if [[ "${TARGET_PLATFORM}" == *,* && "${BUILDX_PUSH}" != "1" ]]; then die "multi-platform buildx output requires --push" fi local -a output_args builder_args [[ "${BUILDX_PUSH}" == "1" ]] && output_args=(--push) || output_args=(--load) [[ -n "${BUILDX_BUILDER}" ]] && builder_args=(--builder "${BUILDX_BUILDER}") || builder_args=() pull_base_images docker buildx build "${builder_args[@]}" \ --platform "${TARGET_PLATFORM}" \ -f "${SCRIPT_DIR}/Dockerfile" \ -t "${CLIENT_IMAGE}" \ "${output_args[@]}" \ "${REPO_ROOT}" } while [[ $# -gt 0 ]]; do case "$1" in up|build|buildx|rebuild|down|stop|restart|logs|ps) ACTION="$1" shift ;; --mode) [[ $# -ge 2 ]] || die "--mode requires a value" MODE="$2" shift 2 ;; --clients) [[ $# -ge 2 ]] || die "--clients requires a value" CLIENTS="$2" shift 2 ;; --platform) [[ $# -ge 2 ]] || die "--platform requires a value" TARGET_PLATFORM="$2" shift 2 ;; --client-image) [[ $# -ge 2 ]] || die "--client-image requires a value" CLIENT_IMAGE="$2" shift 2 ;; --push) BUILDX_PUSH=1 shift ;; --builder) [[ $# -ge 2 ]] || die "--builder requires a value" BUILDX_BUILDER="$2" shift 2 ;; --skip-base-pull) SKIP_BASE_IMAGE_PULL=1 shift ;; --foreground) DETACH=0 shift ;; --no-build) BUILD=0 shift ;; -h|--help) usage exit 0 ;; *) die "unknown argument '$1'" ;; esac done [[ "${CLIENTS}" == "single" || "${CLIENTS}" == "multi" ]] || die "unsupported clients '${CLIENTS}', expected single|multi" export RPKI_RTR_CLIENT_IMAGE="${CLIENT_IMAGE}" if [[ "${ACTION}" == "buildx" ]]; then has_buildx || die "docker buildx is not available" else has_compose || die "docker compose is not available" fi cd "${REPO_ROOT}" case "${ACTION}" in up) up_client ;; build) build_client ;; buildx) buildx_client ;; rebuild) build_client --no-cache up_client ;; down|stop) compose_run down ;; restart) compose_run down up_client ;; logs) compose_run logs -f ;; ps) compose_run ps ;; esac