|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# Build the service images from local working copies and point the integration |
| 4 | +# tests at them, instead of the published :dev / :latest images. |
| 5 | +# |
| 6 | +# Why this exists |
| 7 | +# --------------- |
| 8 | +# The integration tests normally pull ghcr.io/equinor/{flotilla-backend,sara, |
| 9 | +# isar-robot}. That means a change which spans armada *and* one of the services |
| 10 | +# cannot be validated until the service change has been merged and an image |
| 11 | +# published -- but the armada side of the change is what proves the service side |
| 12 | +# works. This script closes that gap: build everything locally, run the suite, |
| 13 | +# then merge in confidence. |
| 14 | +# |
| 15 | +# Usage |
| 16 | +# ----- |
| 17 | +# scripts/build_local_images.sh # build and verify the images |
| 18 | +# scripts/build_local_images.sh --run # ... and then run the full suite |
| 19 | +# scripts/build_local_images.sh --help |
| 20 | +# |
| 21 | +# Repository locations default to the superrepo sibling layout and can each be |
| 22 | +# overridden: ISAR_DIR ISAR_ROBOT_DIR FLOTILLA_DIR SARA_DIR |
| 23 | + |
| 24 | +set -euo pipefail |
| 25 | + |
| 26 | +TAG="${LOCAL_IMAGE_TAG:-local}" |
| 27 | +PLATFORM="linux/amd64" |
| 28 | +RUN_TESTS=false |
| 29 | + |
| 30 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 31 | +ARMADA_DIR="$(dirname "$SCRIPT_DIR")" |
| 32 | +SIBLING_ROOT="$(dirname "$ARMADA_DIR")" |
| 33 | + |
| 34 | +ISAR_DIR="${ISAR_DIR:-$SIBLING_ROOT/isar}" |
| 35 | +ISAR_ROBOT_DIR="${ISAR_ROBOT_DIR:-$SIBLING_ROOT/isar-robot}" |
| 36 | +FLOTILLA_DIR="${FLOTILLA_DIR:-$SIBLING_ROOT/flotilla}" |
| 37 | +SARA_DIR="${SARA_DIR:-$SIBLING_ROOT/sara}" |
| 38 | + |
| 39 | +FLOTILLA_IMAGE="flotilla-backend:$TAG" |
| 40 | +SARA_IMAGE="sara:$TAG" |
| 41 | +ISAR_ROBOT_IMAGE="isar-robot:$TAG" |
| 42 | +ISAR_ROBOT_BASE_IMAGE="isar-robot:$TAG-base" |
| 43 | + |
| 44 | +for arg in "$@"; do |
| 45 | + case "$arg" in |
| 46 | + --run) RUN_TESTS=true ;; |
| 47 | + --help|-h) |
| 48 | + # Print the header comment block, stopping at the first non-comment line. |
| 49 | + awk 'NR==1{next} /^#/{sub(/^# ?/,""); print; next} {exit}' "${BASH_SOURCE[0]}" |
| 50 | + exit 0 ;; |
| 51 | + *) echo "Unknown argument: $arg (try --help)" >&2; exit 2 ;; |
| 52 | + esac |
| 53 | +done |
| 54 | + |
| 55 | +log() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; } |
| 56 | +warn() { printf '\033[1;33mWARNING: %s\033[0m\n' "$*" >&2; } |
| 57 | +die() { printf '\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; } |
| 58 | + |
| 59 | +require_dir() { |
| 60 | + [ -d "$1" ] || die "$2 not found at '$1'. Set $3 to override." |
| 61 | +} |
| 62 | + |
| 63 | +require_dir "$ISAR_DIR" "isar repository" ISAR_DIR |
| 64 | +require_dir "$ISAR_ROBOT_DIR" "isar-robot repository" ISAR_ROBOT_DIR |
| 65 | +require_dir "$FLOTILLA_DIR" "flotilla repository" FLOTILLA_DIR |
| 66 | +require_dir "$SARA_DIR" "sara repository" SARA_DIR |
| 67 | + |
| 68 | +docker info >/dev/null 2>&1 || die "Docker does not appear to be running." |
| 69 | + |
| 70 | +TMP_DIR="$(mktemp -d)" |
| 71 | +cleanup() { rm -rf "$TMP_DIR"; } |
| 72 | +trap cleanup EXIT |
| 73 | + |
| 74 | +# --------------------------------------------------------------------------- |
| 75 | +# flotilla-backend and sara are built straight from the working tree, so any |
| 76 | +# uncommitted changes are included. |
| 77 | +# --------------------------------------------------------------------------- |
| 78 | + |
| 79 | +log "Building $FLOTILLA_IMAGE from $FLOTILLA_DIR" |
| 80 | +docker build --platform "$PLATFORM" \ |
| 81 | + -f "$FLOTILLA_DIR/backend/Dockerfile" \ |
| 82 | + -t "$FLOTILLA_IMAGE" \ |
| 83 | + "$FLOTILLA_DIR/backend" |
| 84 | + |
| 85 | +log "Building $SARA_IMAGE from $SARA_DIR" |
| 86 | +docker build --platform "$PLATFORM" -t "$SARA_IMAGE" "$SARA_DIR" |
| 87 | + |
| 88 | +# --------------------------------------------------------------------------- |
| 89 | +# isar-robot needs two steps. |
| 90 | +# |
| 91 | +# 1. Its Dockerfile does `RUN --mount=source=.git,target=.git,type=bind`, and |
| 92 | +# setuptools_scm needs that git directory both to derive a version *and* to |
| 93 | +# discover package data such as src/isar_robot/example_data/. In the superrepo |
| 94 | +# the checkout is a submodule, so `.git` is a FILE ("gitdir: ...") which the |
| 95 | +# `!.git/` allowlist entry in .dockerignore does not match. Building directly |
| 96 | +# from the working tree therefore fails with "unable to detect version", and |
| 97 | +# forcing SETUPTOOLS_SCM_PRETEND_VERSION instead produces a wheel that is |
| 98 | +# missing example_data -- which only shows up much later as |
| 99 | +# RobotRetrieveInspectionException during a mission. Cloning into a temporary |
| 100 | +# directory yields a real .git directory with history and tags, so the stock |
| 101 | +# Dockerfile works unmodified. |
| 102 | +# |
| 103 | +# 2. isar-robot's uv.lock pins `isar` from PyPI (the lock is generated with |
| 104 | +# --no-sources, so the `[tool.uv.sources] isar = { path = "../isar" }` entry in |
| 105 | +# pyproject.toml is ignored). To test local isar changes, the locally built |
| 106 | +# wheel is installed over the released one. |
| 107 | +# --------------------------------------------------------------------------- |
| 108 | + |
| 109 | +if [ -n "$(git -C "$ISAR_ROBOT_DIR" status --porcelain)" ]; then |
| 110 | + warn "$ISAR_ROBOT_DIR has uncommitted changes." |
| 111 | + warn "isar-robot is CLONED rather than built from the working tree, so those" |
| 112 | + warn "changes will NOT be in the image. Commit them first if they matter." |
| 113 | +fi |
| 114 | + |
| 115 | +log "Cloning isar-robot into a temporary directory (needs a real .git)" |
| 116 | +git clone --quiet "$ISAR_ROBOT_DIR" "$TMP_DIR/isar-robot" \ |
| 117 | + || die "Failed to clone $ISAR_ROBOT_DIR" |
| 118 | + |
| 119 | +log "Building $ISAR_ROBOT_BASE_IMAGE" |
| 120 | +docker build --platform "$PLATFORM" -t "$ISAR_ROBOT_BASE_IMAGE" "$TMP_DIR/isar-robot" |
| 121 | + |
| 122 | +log "Building the isar wheel from $ISAR_DIR (working tree, uncommitted changes included)" |
| 123 | +mkdir -p "$TMP_DIR/wheels" |
| 124 | +if ! ( cd "$ISAR_DIR" && uv build --wheel -o "$TMP_DIR/wheels" ) >"$TMP_DIR/uv-build.log" 2>&1; then |
| 125 | + cat "$TMP_DIR/uv-build.log" >&2 |
| 126 | + die "Failed to build the isar wheel" |
| 127 | +fi |
| 128 | +ISAR_WHEEL="$(ls "$TMP_DIR"/wheels/isar-*.whl 2>/dev/null | head -1)" |
| 129 | +[ -n "$ISAR_WHEEL" ] || die "No isar wheel was produced in $TMP_DIR/wheels" |
| 130 | +echo "Built $(basename "$ISAR_WHEEL")" |
| 131 | + |
| 132 | +log "Overlaying the local isar onto $ISAR_ROBOT_IMAGE" |
| 133 | +# The wheel keeps its original filename: uv rejects anything that is not a valid |
| 134 | +# PEP 427 wheel name ("Must have a Python tag"). |
| 135 | +mkdir -p "$TMP_DIR/overlay/wheels" |
| 136 | +cp "$ISAR_WHEEL" "$TMP_DIR/overlay/wheels/" |
| 137 | +cat > "$TMP_DIR/overlay/Dockerfile" <<OVERLAY |
| 138 | +FROM $ISAR_ROBOT_BASE_IMAGE |
| 139 | +USER root |
| 140 | +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv |
| 141 | +COPY wheels /tmp/wheels |
| 142 | +# --no-deps keeps the resolved dependency set from the base image. If isar ever |
| 143 | +# gains a new dependency this will need revisiting. |
| 144 | +RUN /bin/uv pip install --python /app/.venv/bin/python --no-deps --reinstall /tmp/wheels/*.whl \ |
| 145 | + && rm -rf /tmp/wheels |
| 146 | +USER 1000 |
| 147 | +CMD ["isar-start"] |
| 148 | +OVERLAY |
| 149 | +docker build --platform "$PLATFORM" -t "$ISAR_ROBOT_IMAGE" "$TMP_DIR/overlay" |
| 150 | + |
| 151 | +docker run --rm --platform "$PLATFORM" \ |
| 152 | + --entrypoint /app/.venv/bin/python "$ISAR_ROBOT_IMAGE" -c ' |
| 153 | +import pathlib, sys |
| 154 | +import isar_robot |
| 155 | +from isar.config.settings import settings |
| 156 | +
|
| 157 | +problems = [] |
| 158 | +
|
| 159 | +example_data = pathlib.Path(isar_robot.__file__).parent / "example_data" |
| 160 | +count = len(list(example_data.iterdir())) if example_data.is_dir() else 0 |
| 161 | +if count == 0: |
| 162 | + problems.append( |
| 163 | + "isar_robot/example_data is missing or empty; the wheel was built without a " |
| 164 | + "usable git directory, and missions will fail with " |
| 165 | + "RobotRetrieveInspectionException" |
| 166 | + ) |
| 167 | +
|
| 168 | +for problem in problems: |
| 169 | + print(" FAIL " + problem) |
| 170 | +if problems: |
| 171 | + sys.exit(1) |
| 172 | +
|
| 173 | +print(f" OK isar-robot has {count} example_data files") |
| 174 | +print(" OK isar exposes OPENID_CONFIG_URL") |
| 175 | +' || die "isar-robot image verification failed" |
| 176 | + |
| 177 | +# --------------------------------------------------------------------------- |
| 178 | + |
| 179 | +PYTEST_ENV=( |
| 180 | + "FLOTILLA_BACKEND_IMAGE=$FLOTILLA_IMAGE" |
| 181 | + "SARA_IMAGE=$SARA_IMAGE" |
| 182 | + "ISAR_ROBOT_IMAGE=$ISAR_ROBOT_IMAGE" |
| 183 | + # Take the database schema from the same checkouts the images were built |
| 184 | + # from, rather than cloning the app repositories from GitHub. Without this |
| 185 | + # you would run local application code against a remote schema, and any |
| 186 | + # migration that is unpushed or uncommitted would be missed entirely. |
| 187 | + "FLOTILLA_MIGRATIONS_SOURCE_DIR=$FLOTILLA_DIR" |
| 188 | + "SARA_MIGRATIONS_SOURCE_DIR=$SARA_DIR" |
| 189 | +) |
| 190 | + |
| 191 | +log "Images ready" |
| 192 | +printf ' %s\n' "$FLOTILLA_IMAGE" "$SARA_IMAGE" "$ISAR_ROBOT_IMAGE" |
| 193 | + |
| 194 | +if [ "$RUN_TESTS" = true ]; then |
| 195 | + log "Running the integration tests against the local images" |
| 196 | + cd "$ARMADA_DIR" |
| 197 | + env "${PYTEST_ENV[@]}" uv run --frozen pytest -n auto robotics_integration_tests |
| 198 | +else |
| 199 | + log "Run the integration tests with:" |
| 200 | + echo |
| 201 | + printf ' cd %s\n' "$ARMADA_DIR" |
| 202 | + for pair in "${PYTEST_ENV[@]}"; do printf ' %s \\\n' "$pair"; done |
| 203 | + printf ' uv run --frozen pytest -n auto robotics_integration_tests\n\n' |
| 204 | + printf 'Or re-run this script with --run.\n\n' |
| 205 | +fi |
0 commit comments