The previous packager (scripts/packageDectRelayAgent.js) shipped a
source-only bundle and expected the DC host to build the image with
`docker compose up --build`. That fails hard in corporate DCs with
TLS-intercepted egress: Alpine's apk fetch of dl-cdn.alpinelinux.org
can't verify the intercepted certificate ("apk: TLS: server
certificate not trusted"), and npm install would fail the same way
if apk had succeeded.
New approach: build the image ONCE on the dev machine (where TLS
works), save it as a gzipped tarball, and ship a ZIP whose install
step is `docker load` + `docker compose up -d`. Zero network calls
inside the DC container, ever.
Bundling (dev-machine):
- dect-relay-agent/bundle.sh: build → docker save → gzip → zip.
Auto-derives version from package.json, records git sha + dirty
flag + build date into image labels. Cross-arch friendly
(--platform=linux/amd64 by default; --platform linux/arm64 for
ARM DCs). Output: dect-relay-agent-bundle-<YYYYMMDD-HHMMSS>.zip
at repo root (typically 40-60MB).
- dect-relay-agent/Dockerfile: multi-stage node:20-alpine build.
No apk add. No runtime npm install. Non-root `node` user (uid
1000). Node handles SIGTERM natively via index.js handlers, so
no tini/dumb-init needed. Designed to build from the REPO ROOT
(not the agent folder) because the agent imports shared modules
from ../integrations/cisco-dect and ../utils.
- dect-relay-agent/Dockerfile.dockerignore: per-Dockerfile ignore
(BuildKit ≥ 23.0) with a whitelist that keeps the build context
to ~50KB. Older Docker daemons fall through to the repo-root
.dockerignore, which already excludes secrets — nothing sensitive
can leak either way.
- package.json: `npm run package:relay` now invokes bundle.sh.
Runtime (DC-host):
- dect-relay-agent/docker-compose.yml: pins IMAGE_TAG from .env
(install.sh writes it there — never falls back to :latest), reads
the rest of the config via env_file, restart: unless-stopped,
host networking (needed to reach 10.x/8 without userland proxy
translation, and the agent doesn't listen on anything). Hardened:
read_only: true rootfs with a 16MB /tmp tmpfs, cap_drop: ALL,
no-new-privileges, log rotation at 10MB × 5 files.
- dect-relay-agent/install.sh: preflight (docker + compose present,
daemon reachable, bundle files intact), docker load, pin loaded
tag into .env, validate .env has the three required values not
still set to placeholder strings, docker compose up -d, tail last
40 log lines. Idempotent — safe to re-run on upgrades.
Cleanup:
- scripts/packageDectRelayAgent.js: deleted (superseded).
- .gitignore: drops the scripts/* + !packageDectRelayAgent.js dance
since we no longer need to whitelist that one file; add pattern
for the datestamped bundle zips + staging dirs at repo root.
- dect-relay-agent/README.md: replaces the deploy section with the
new dev-machine-build → DC-host-load workflow, plus a
troubleshooting section keyed on the exact error messages seen
during the failed in-DC build (TLS cert not trusted, docker perm
denied, DIGEST_401).
Verified: all 113 existing tests still pass. Docker build itself
requires a Docker daemon (dev machine) so can't be exercised in
this sandbox — the bash scripts pass `bash -n` syntax checks.
204 lines
9.6 KiB
Bash
Executable file
204 lines
9.6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# ────────────────────────────────────────────────────────────────────
|
|
# DECT relay agent — dev-machine bundler.
|
|
#
|
|
# Builds the Docker image for linux/amd64, saves it as a gzipped
|
|
# tarball, and zips it up with the compose file + .env template +
|
|
# install script. Output is a self-contained ZIP the DC operator
|
|
# can transfer over any file-copy channel (email, S3, USB, git-lfs)
|
|
# and install with a single `./install.sh` invocation.
|
|
#
|
|
# Requirements on the dev machine:
|
|
# - Docker Desktop / Docker Engine
|
|
# - Internet access to pull node:20-alpine + npm registry
|
|
# - `zip` (macOS + most Linux distros already have it; if not,
|
|
# `apt install zip` / `brew install zip`)
|
|
#
|
|
# Requirements on the DC host:
|
|
# - Docker + Docker Compose v2 (v1 also works)
|
|
# - Ability to `docker load` (i.e. member of the docker group or
|
|
# root)
|
|
# - Outbound HTTPS to the bot + 10.0.0.0/8 on TCP 443
|
|
# - That's it. No npm, no python, no Alpine mirrors.
|
|
#
|
|
# Usage (from repo root OR from this folder — the script figures it out):
|
|
# ./dect-relay-agent/bundle.sh # uses version from package.json
|
|
# ./dect-relay-agent/bundle.sh --tag 0.2.0 # override version
|
|
# ./dect-relay-agent/bundle.sh --platform linux/arm64
|
|
# ────────────────────────────────────────────────────────────────────
|
|
set -euo pipefail
|
|
|
|
# ─── Argument parsing ─────────────────────────────────────────────
|
|
TAG_OVERRIDE=""
|
|
PLATFORM="linux/amd64" # standard x86_64 Linux server. Override if DC is arm.
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--tag) TAG_OVERRIDE="$2"; shift 2 ;;
|
|
--platform) PLATFORM="$2"; shift 2 ;;
|
|
-h|--help)
|
|
grep -E '^# ' "$0" | sed 's/^# \?//'
|
|
exit 0
|
|
;;
|
|
*) echo "Unknown flag: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
# ─── Locate paths (works from repo root or agent dir) ─────────────
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
AGENT_DIR="$SCRIPT_DIR"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
# Sanity: the Dockerfile expects to build from the repo root.
|
|
[[ -d "$REPO_ROOT/integrations/cisco-dect" ]] || {
|
|
echo "ERROR: $REPO_ROOT does not look like the collabSupport repo root (no integrations/cisco-dect/)" >&2
|
|
exit 1
|
|
}
|
|
|
|
# ─── Determine version tag ────────────────────────────────────────
|
|
if [[ -n "$TAG_OVERRIDE" ]]; then
|
|
VERSION="$TAG_OVERRIDE"
|
|
else
|
|
# Pull version from agent's package.json without needing jq. The
|
|
# regex is deliberately tolerant of trailing commas / whitespace.
|
|
VERSION="$(sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$AGENT_DIR/package.json" | head -1)"
|
|
[[ -n "$VERSION" ]] || { echo "ERROR: could not read version from package.json" >&2; exit 1; }
|
|
fi
|
|
|
|
# Try to record the git commit into the image labels — useful in
|
|
# production for "which build am I running?". Missing git is fine.
|
|
GIT_COMMIT="unknown"
|
|
if command -v git >/dev/null 2>&1 && git -C "$REPO_ROOT" rev-parse --short HEAD >/dev/null 2>&1; then
|
|
GIT_COMMIT="$(git -C "$REPO_ROOT" rev-parse --short HEAD)"
|
|
# Mark as dirty if the working tree has uncommitted changes —
|
|
# catches "I built from local edits" surprises in production.
|
|
if ! git -C "$REPO_ROOT" diff --quiet 2>/dev/null || \
|
|
! git -C "$REPO_ROOT" diff --cached --quiet 2>/dev/null; then
|
|
GIT_COMMIT="${GIT_COMMIT}-dirty"
|
|
fi
|
|
fi
|
|
|
|
BUILD_DATE="$(date -u +%FT%TZ)"
|
|
IMAGE_TAG="collabsupport/dect-relay-agent:${VERSION}"
|
|
BUNDLE_STAMP="$(date +%Y%m%d-%H%M%S)"
|
|
BUNDLE_DIR="dect-relay-agent-bundle-${BUNDLE_STAMP}"
|
|
BUNDLE_ZIP="${BUNDLE_DIR}.zip"
|
|
|
|
echo "════════════════════════════════════════════════════════════════"
|
|
echo " Building DECT relay agent bundle"
|
|
echo "────────────────────────────────────────────────────────────────"
|
|
echo " Version: ${VERSION}"
|
|
echo " Image tag: ${IMAGE_TAG}"
|
|
echo " Platform: ${PLATFORM}"
|
|
echo " Git commit: ${GIT_COMMIT}"
|
|
echo " Build date: ${BUILD_DATE}"
|
|
echo " Bundle dir: ${BUNDLE_DIR}/"
|
|
echo " Bundle ZIP: ${BUNDLE_ZIP}"
|
|
echo "════════════════════════════════════════════════════════════════"
|
|
|
|
# ─── Docker build ─────────────────────────────────────────────────
|
|
# --platform pins the arch so building on Apple Silicon still
|
|
# produces an x86_64 image the DC can run. Docker uses QEMU to
|
|
# emulate cross-arch — slower than native but Just Works.
|
|
echo
|
|
echo "[1/4] Building image..."
|
|
docker build \
|
|
--platform="${PLATFORM}" \
|
|
--file "${AGENT_DIR}/Dockerfile" \
|
|
--tag "${IMAGE_TAG}" \
|
|
--build-arg "AGENT_VERSION=${VERSION}" \
|
|
--build-arg "BUILD_DATE=${BUILD_DATE}" \
|
|
--build-arg "GIT_COMMIT=${GIT_COMMIT}" \
|
|
"${REPO_ROOT}"
|
|
|
|
# ─── Stage the bundle ─────────────────────────────────────────────
|
|
# Work in the parent of the agent dir so the resulting ZIP + dir
|
|
# both land somewhere obvious (the repo root by convention).
|
|
echo
|
|
echo "[2/4] Staging bundle in ${REPO_ROOT}/${BUNDLE_DIR}/"
|
|
rm -rf "${REPO_ROOT:?}/${BUNDLE_DIR}"
|
|
mkdir -p "${REPO_ROOT}/${BUNDLE_DIR}"
|
|
|
|
# docker save streams a tarball to stdout; pipe through gzip to
|
|
# shrink it substantially (typically ~40% smaller for Node images).
|
|
echo
|
|
echo "[3/4] Saving image to ${BUNDLE_DIR}/image.tar.gz (this can take a minute)"
|
|
docker save "${IMAGE_TAG}" | gzip -9 > "${REPO_ROOT}/${BUNDLE_DIR}/image.tar.gz"
|
|
|
|
# Copy the operator-facing files. We do NOT copy Dockerfile / bundle.sh
|
|
# — those are dev-machine concerns.
|
|
cp "${AGENT_DIR}/docker-compose.yml" "${REPO_ROOT}/${BUNDLE_DIR}/"
|
|
cp "${AGENT_DIR}/.env.example" "${REPO_ROOT}/${BUNDLE_DIR}/"
|
|
cp "${AGENT_DIR}/install.sh" "${REPO_ROOT}/${BUNDLE_DIR}/"
|
|
cp "${AGENT_DIR}/README.md" "${REPO_ROOT}/${BUNDLE_DIR}/AGENT-README.md"
|
|
|
|
# Write a bundle-specific README that's short and tells the operator
|
|
# what to do in this exact folder. Keeps AGENT-README.md as the deep
|
|
# reference without cluttering the top-of-bundle experience.
|
|
cat > "${REPO_ROOT}/${BUNDLE_DIR}/README.txt" <<EOF
|
|
DECT Relay Agent — deployment bundle
|
|
====================================
|
|
|
|
Version: ${VERSION}
|
|
Image tag: ${IMAGE_TAG}
|
|
Built: ${BUILD_DATE}
|
|
Git commit: ${GIT_COMMIT}
|
|
Platform: ${PLATFORM}
|
|
|
|
To install on this data-center host:
|
|
|
|
1. cp .env.example .env
|
|
2. Edit .env — set:
|
|
- DECT_RELAY_BOT_URL (wss:// URL to the bot)
|
|
- DECT_RELAY_AGENT_TOKEN (shared bearer, same as bot's env)
|
|
- DECT_ADMIN_PASSWORD (DECT serviceability password)
|
|
3. ./install.sh
|
|
4. Watch it come up: docker compose logs -f dect-relay-agent
|
|
|
|
Files in this bundle:
|
|
|
|
image.tar.gz Prebuilt Docker image (gzipped, ~40MB)
|
|
docker-compose.yml Compose file — read-only rootfs, host network,
|
|
log rotation. Loaded by install.sh.
|
|
.env.example Config template.
|
|
install.sh Runs 'docker load' then 'docker compose up -d'.
|
|
Safe to re-run for upgrades.
|
|
AGENT-README.md Full agent docs — wire protocol, safety model,
|
|
run instructions.
|
|
README.txt This file.
|
|
|
|
No internet access required after the image is loaded. The container
|
|
runs with a read-only root filesystem, drops all Linux capabilities,
|
|
and uses the 'node' non-root user.
|
|
|
|
Upgrading:
|
|
Unzip the new bundle in a new folder, or overwrite this one, and
|
|
re-run ./install.sh. install.sh pins the new image tag into .env
|
|
automatically.
|
|
EOF
|
|
|
|
# ─── Zip it up ────────────────────────────────────────────────────
|
|
echo
|
|
echo "[4/4] Zipping bundle → ${BUNDLE_ZIP}"
|
|
(
|
|
cd "${REPO_ROOT}"
|
|
# -r recursive, -X strip Mac resource forks so we don't ship
|
|
# __MACOSX/ folders that confuse Linux operators.
|
|
zip -rqX "${BUNDLE_ZIP}" "${BUNDLE_DIR}"
|
|
)
|
|
|
|
# Cleanup: keep the ZIP, remove the staging dir. Operator only wants
|
|
# the ZIP to transfer.
|
|
rm -rf "${REPO_ROOT:?}/${BUNDLE_DIR}"
|
|
|
|
SIZE="$(du -h "${REPO_ROOT}/${BUNDLE_ZIP}" | cut -f1)"
|
|
|
|
echo
|
|
echo "════════════════════════════════════════════════════════════════"
|
|
echo " Bundle ready: ${REPO_ROOT}/${BUNDLE_ZIP} (${SIZE})"
|
|
echo "────────────────────────────────────────────────────────────────"
|
|
echo " Transfer to the DC and:"
|
|
echo " unzip ${BUNDLE_ZIP}"
|
|
echo " cd ${BUNDLE_DIR}"
|
|
echo " cp .env.example .env && \$EDITOR .env"
|
|
echo " ./install.sh"
|
|
echo "════════════════════════════════════════════════════════════════"
|