fix: make ops/deploy.sh rebuild, dry-run safely, and join Traefik at start
- Always build from the checkout (`up --build`, `pull_policy: build`) instead of reusing a stale local image or pulling an old registry one. - --render-only writes to rendered/ and no longer touches the live Traefik dynamic directory; the route is installed after health check. - Join the Traefik network via a generated docker-compose.override.yaml rather than `docker network connect` after startup, which raced the control-plane's startup call to wg-easy and was lost on recreate. - Add --non-interactive, check the public HTTPS endpoint, and point the closing instructions at Settings -> Gateway (the suggested `furtka gateway connect` command doesn't exist). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e240f071d5
commit
6b8cc6e006
5 changed files with 196 additions and 172 deletions
14
.env.example
14
.env.example
|
|
@ -58,12 +58,10 @@ GATEWAY_CERT_RESOLVER=
|
||||||
GATEWAY_CONTROL_PLANE_HOST=gw.example.com
|
GATEWAY_CONTROL_PLANE_HOST=gw.example.com
|
||||||
|
|
||||||
# Where the control-plane's own API is bound (docker-compose.yaml's port
|
# Where the control-plane's own API is bound (docker-compose.yaml's port
|
||||||
# publish) and where your Traefik reaches it, respectively. Same value
|
# publish) and where your Traefik reaches it, respectively. For the
|
||||||
# for the recommended same-docker-network setup (ops/deploy.sh sets both
|
# recommended same-docker-network setup, ops/deploy.sh keeps the bind
|
||||||
# to furtka-gateway-control-plane:8090 automatically, with the bind
|
# loopback-only and sets the target to the fixed container name
|
||||||
# staying loopback-only — see docker-compose.yaml's comment on why this
|
# furtka-gateway-control-plane:8090.
|
||||||
# is more robust than the compose *service* name "control-plane" once the
|
|
||||||
# network is joined after the fact via `docker network connect`).
|
|
||||||
# Different values only for a separate-host setup, where
|
# Different values only for a separate-host setup, where
|
||||||
# GATEWAY_CONTROL_PLANE_BIND must be widened beyond 127.0.0.1 — put real
|
# GATEWAY_CONTROL_PLANE_BIND must be widened beyond 127.0.0.1 — put real
|
||||||
# access control around that bind address, this file carries a WireGuard
|
# access control around that bind address, this file carries a WireGuard
|
||||||
|
|
@ -74,8 +72,8 @@ GATEWAY_CONTROL_PLANE_TARGET=furtka-gateway-control-plane:8090
|
||||||
# Same-docker-network setup only (the recommended path — see the README's
|
# Same-docker-network setup only (the recommended path — see the README's
|
||||||
# Prerequisites checklist): your Traefik's docker network name
|
# Prerequisites checklist): your Traefik's docker network name
|
||||||
# (`docker network ls` / `docker inspect <your-traefik-container>`).
|
# (`docker network ls` / `docker inspect <your-traefik-container>`).
|
||||||
# ops/deploy.sh runs `docker network connect` with this after bringing the
|
# ops/deploy.sh writes a docker-compose.override.yaml that joins this
|
||||||
# stack up. Leave blank for a separate-host setup.
|
# network. Leave blank for a separate-host setup.
|
||||||
GATEWAY_TRAEFIK_NETWORK=
|
GATEWAY_TRAEFIK_NETWORK=
|
||||||
|
|
||||||
# The one box token a single-tenant deployment accepts at
|
# The one box token a single-tenant deployment accepts at
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -10,3 +10,6 @@ traefik/dynamic/route-*.yml
|
||||||
traefik/dynamic/control-plane.yml
|
traefik/dynamic/control-plane.yml
|
||||||
traefik/dynamic/default-cert.yml
|
traefik/dynamic/default-cert.yml
|
||||||
traefik/static/traefik.active.yml
|
traefik/static/traefik.active.yml
|
||||||
|
examples/starter-stack/dynamic/
|
||||||
|
rendered/
|
||||||
|
docker-compose.override.yaml
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
services:
|
services:
|
||||||
control-plane:
|
control-plane:
|
||||||
build: ./control_plane
|
build: ./control_plane
|
||||||
# CI (.forgejo/workflows/build-image.yml) publishes this same build to
|
# Always build from this checkout — never pull a registry image that may
|
||||||
# the registry on every push to master, tagged `latest` plus the short
|
# be older than the code you just pulled. The image name below only
|
||||||
# commit sha. `docker compose up` builds fresh from `build:` above and
|
# matters for `docker stack deploy` (Swarm can't build; see the Swarm
|
||||||
# tags the result as this name, ignoring this being a registry ref;
|
# caveats in README.md), which pulls what CI publishes on every push to
|
||||||
# `docker stack deploy` (Swarm ignores `build:` entirely, see the
|
# master (`latest` plus the short commit sha).
|
||||||
# Swarm caveats in README.md) pulls it instead — no manual
|
pull_policy: build
|
||||||
# `docker build -t ... ./control_plane` step needed there anymore.
|
|
||||||
image: ${GATEWAY_CONTROL_PLANE_IMAGE:-forgejo.sourcegate.online/robert/furtka-gateway-control-plane:latest}
|
image: ${GATEWAY_CONTROL_PLANE_IMAGE:-forgejo.sourcegate.online/robert/furtka-gateway-control-plane:latest}
|
||||||
container_name: furtka-gateway-control-plane
|
container_name: furtka-gateway-control-plane
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -58,14 +57,10 @@ services:
|
||||||
# implication of doing that when you choose it.
|
# implication of doing that when you choose it.
|
||||||
- "${GATEWAY_CONTROL_PLANE_BIND:-127.0.0.1:8090}:8090"
|
- "${GATEWAY_CONTROL_PLANE_BIND:-127.0.0.1:8090}:8090"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# No custom `networks:` here on purpose — this stays on the default
|
# No `networks:` here on purpose — your Traefik's network name differs
|
||||||
# bridge network compose creates unless you also run YOUR Traefik in
|
# per setup. ops/deploy.sh writes a gitignored
|
||||||
# this SAME compose project. To reach a Traefik running elsewhere (its
|
# docker-compose.override.yaml (auto-loaded by `docker compose`) that
|
||||||
# own compose project, or your own homelab stack), ops/deploy.sh joins
|
# joins this container to it from the first second.
|
||||||
# this container to your Traefik's existing docker network after
|
|
||||||
# `up` via `docker network connect` — no external-network dependency
|
|
||||||
# baked into this file, so it works whether or not that network
|
|
||||||
# exists yet at compose-up time.
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
control_plane_data:
|
control_plane_data:
|
||||||
|
|
|
||||||
255
ops/deploy.sh
255
ops/deploy.sh
|
|
@ -1,24 +1,23 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Interactive setup for furtka-gateway's control-plane, plugged into a
|
# Sets up furtka-gateway's control-plane, plugged into a Traefik and
|
||||||
# Traefik and wg-easy YOU already run (as sibling containers, or otherwise
|
# wg-easy YOU already run. Read the README's "Existing Traefik + wg-easy"
|
||||||
# reachable) — see the root README's Prerequisites checklist before
|
# section first — in particular the routing requirement, which this script
|
||||||
# running this. No zero-infra option here: for that, use
|
# can't set up or verify for you. On an empty server, use
|
||||||
# examples/starter-stack/deploy.sh instead, which bundles its own
|
# examples/starter-stack/ instead.
|
||||||
# Traefik+wg-easy.
|
|
||||||
#
|
#
|
||||||
# Safe to re-run — existing .env values become the defaults for each
|
# Safe to re-run — existing .env values become the defaults for each
|
||||||
# prompt (press Enter to keep them), and rendering is always fully
|
# prompt (press Enter to keep them).
|
||||||
# regenerated from the current .env, never patched in place.
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ops/deploy.sh full setup: prompts, render, compose up,
|
# ops/deploy.sh prompts, writes .env, starts the
|
||||||
# network join, health check
|
# control-plane, installs its Traefik
|
||||||
# ops/deploy.sh --render-only only (re)write .env and the rendered
|
# route, health-checks it
|
||||||
# traefik/dynamic/control-plane.yml —
|
# ops/deploy.sh --non-interactive same, but no prompts: every value
|
||||||
# skips compose up / network join /
|
# comes from .env (fails if one is missing)
|
||||||
# health check. Useful after hand-editing
|
# ops/deploy.sh --render-only only (re)write .env and
|
||||||
# .env, or to inspect what would be
|
# rendered/control-plane.yml — touches
|
||||||
# generated before touching anything live.
|
# nothing live (not even your Traefik's
|
||||||
|
# dynamic directory)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
@ -26,9 +25,14 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
RENDER_ONLY=0
|
RENDER_ONLY=0
|
||||||
if [[ "${1:-}" == "--render-only" ]]; then
|
INTERACTIVE=1
|
||||||
RENDER_ONLY=1
|
for arg in "$@"; do
|
||||||
fi
|
case "$arg" in
|
||||||
|
--render-only) RENDER_ONLY=1 ;;
|
||||||
|
--non-interactive) INTERACTIVE=0 ;;
|
||||||
|
*) echo "unknown option: $arg" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
ENV_FILE=.env
|
ENV_FILE=.env
|
||||||
|
|
||||||
|
|
@ -53,117 +57,109 @@ ask() {
|
||||||
local __var="$1" __prompt="$2" __fallback="${3:-}"
|
local __var="$1" __prompt="$2" __fallback="${3:-}"
|
||||||
local __current="${!__var:-}"
|
local __current="${!__var:-}"
|
||||||
local __default="${__current:-$__fallback}"
|
local __default="${__current:-$__fallback}"
|
||||||
local __ans
|
local __ans=""
|
||||||
|
if [[ "$INTERACTIVE" -eq 1 ]]; then
|
||||||
if [[ -n "$__default" ]]; then
|
if [[ -n "$__default" ]]; then
|
||||||
read -r -p "$__prompt [$__default]: " __ans
|
read -r -p "$__prompt [$__default]: " __ans
|
||||||
else
|
else
|
||||||
read -r -p "$__prompt: " __ans
|
read -r -p "$__prompt: " __ans
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
printf -v "$__var" '%s' "${__ans:-$__default}"
|
printf -v "$__var" '%s' "${__ans:-$__default}"
|
||||||
}
|
}
|
||||||
|
|
||||||
ask_secret() {
|
ask_secret() {
|
||||||
local __var="$1" __prompt="$2"
|
local __var="$1" __prompt="$2"
|
||||||
local __current="${!__var:-}"
|
local __current="${!__var:-}"
|
||||||
local __ans
|
local __ans=""
|
||||||
|
if [[ "$INTERACTIVE" -eq 1 ]]; then
|
||||||
if [[ -n "$__current" ]]; then
|
if [[ -n "$__current" ]]; then
|
||||||
read -r -s -p "$__prompt [unchanged, Enter to keep]: " __ans
|
read -r -s -p "$__prompt [unchanged, Enter to keep]: " __ans
|
||||||
else
|
else
|
||||||
read -r -s -p "$__prompt: " __ans
|
read -r -s -p "$__prompt: " __ans
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
|
fi
|
||||||
printf -v "$__var" '%s' "${__ans:-$__current}"
|
printf -v "$__var" '%s' "${__ans:-$__current}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require() {
|
||||||
|
# require VARNAME — fail if empty
|
||||||
|
if [[ -z "${!1:-}" ]]; then
|
||||||
|
echo "$1 is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
require_cmd() {
|
require_cmd() {
|
||||||
command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 1; }
|
command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require_cmd openssl
|
||||||
if [[ "$RENDER_ONLY" -eq 0 ]]; then
|
if [[ "$RENDER_ONLY" -eq 0 ]]; then
|
||||||
require_cmd docker
|
require_cmd docker
|
||||||
require_cmd openssl
|
|
||||||
require_cmd curl
|
require_cmd curl
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "== furtka-gateway deploy (bring your own Traefik/wg-easy) =="
|
echo "== furtka-gateway: connect to your existing Traefik + wg-easy =="
|
||||||
echo "Haven't read the README's Prerequisites checklist yet? Do that first —"
|
|
||||||
echo "this script can't verify your Traefik has a real network path to"
|
|
||||||
echo "wg-easy's WireGuard peer subnet, and a broken deploy from skipping"
|
|
||||||
echo "that is hard to debug after the fact."
|
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# ---- tenancy mode ----
|
# ---- tenancy mode ----
|
||||||
while true; do
|
|
||||||
ask GATEWAY_MODE "Mode (single / shared)" "single"
|
ask GATEWAY_MODE "Mode (single / shared)" "single"
|
||||||
case "$GATEWAY_MODE" in
|
case "$GATEWAY_MODE" in
|
||||||
single|shared) break ;;
|
single|shared) ;;
|
||||||
*) echo "must be one of: single, shared" ;;
|
*) echo "GATEWAY_MODE must be single or shared" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
|
||||||
|
|
||||||
# ---- wg-easy ----
|
# ---- wg-easy ----
|
||||||
echo
|
echo
|
||||||
ask WG_EASY_URL "Your wg-easy's admin API URL (e.g. http://wg-easy:51821)" ""
|
ask WG_EASY_URL "Your wg-easy's admin API URL, as the control-plane will reach it (e.g. http://wg-easy:51821)" ""
|
||||||
if [[ -z "$WG_EASY_URL" ]]; then
|
require WG_EASY_URL
|
||||||
echo "WG_EASY_URL is required" >&2
|
ask WG_EASY_API_VERSION "Your wg-easy image's major version (14 or 15 — check the tag)" "15"
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
while true; do
|
|
||||||
ask WG_EASY_API_VERSION "Your wg-easy image's version (14 or 15 — check the tag)" "14"
|
|
||||||
case "$WG_EASY_API_VERSION" in
|
case "$WG_EASY_API_VERSION" in
|
||||||
14|15) break ;;
|
14|15) ;;
|
||||||
*) echo "must be 14 or 15 — these are different, incompatible APIs (see control_plane/wgeasy.py vs wgeasy_v15.py)" ;;
|
*) echo "WG_EASY_API_VERSION must be 14 or 15 (different, incompatible APIs)" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
|
||||||
if [[ "$WG_EASY_API_VERSION" == "15" ]]; then
|
if [[ "$WG_EASY_API_VERSION" == "15" ]]; then
|
||||||
ask WG_EASY_ADMIN_USERNAME "wg-easy admin username" "admin"
|
ask WG_EASY_ADMIN_USERNAME "wg-easy admin username" "admin"
|
||||||
ask GATEWAY_WG_ALLOWED_IPS "wg-easy's WireGuard peer subnet (restricts new clients to split-tunnel)" "10.8.0.0/24"
|
ask GATEWAY_WG_ALLOWED_IPS "wg-easy's WireGuard peer subnet (new clients get split-tunnel to this)" "10.8.0.0/24"
|
||||||
fi
|
fi
|
||||||
ask_secret WG_EASY_ADMIN_PASSWORD "wg-easy admin password"
|
ask_secret WG_EASY_ADMIN_PASSWORD "wg-easy admin password"
|
||||||
if [[ -z "${WG_EASY_ADMIN_PASSWORD:-}" ]]; then
|
require WG_EASY_ADMIN_PASSWORD
|
||||||
echo "wg-easy admin password is required" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ---- Traefik ----
|
# ---- Traefik ----
|
||||||
echo
|
echo
|
||||||
ask GATEWAY_DYNAMIC_DIR_HOST "Host directory YOUR Traefik's file provider watches (or a new one you'll add as an extra providers.file directory)" ""
|
ask GATEWAY_DYNAMIC_DIR_HOST "Host directory your Traefik's file provider watches" ""
|
||||||
if [[ -z "$GATEWAY_DYNAMIC_DIR_HOST" ]]; then
|
require GATEWAY_DYNAMIC_DIR_HOST
|
||||||
echo "GATEWAY_DYNAMIC_DIR_HOST is required" >&2
|
ask GATEWAY_CERT_RESOLVER "Your Traefik's certResolver name (blank if a default cert applies automatically)" ""
|
||||||
exit 1
|
ask GATEWAY_CONTROL_PLANE_HOST "Gateway API hostname (what you enter on the box)" ""
|
||||||
fi
|
require GATEWAY_CONTROL_PLANE_HOST
|
||||||
ask GATEWAY_CERT_RESOLVER "Your Traefik's certResolver name (blank if it applies a default cert automatically)" ""
|
|
||||||
ask GATEWAY_CONTROL_PLANE_HOST "Control-plane API hostname (its own A/AAAA record)" "gw.example.com"
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "How does YOUR Traefik reach this control-plane container?"
|
echo "How does your Traefik reach the control-plane container?"
|
||||||
echo " 1) same docker network as my Traefik (recommended)"
|
echo " 1) it's on the same docker network as my Traefik (recommended)"
|
||||||
echo " 2) separate host — I'll expose it at a reachable host:port myself"
|
echo " 2) separate host — I'll expose it at a reachable host:port myself"
|
||||||
while true; do
|
|
||||||
ask GATEWAY_REACHABILITY_MODE "Choice (1 or 2)" "1"
|
ask GATEWAY_REACHABILITY_MODE "Choice (1 or 2)" "1"
|
||||||
case "$GATEWAY_REACHABILITY_MODE" in
|
case "$GATEWAY_REACHABILITY_MODE" in
|
||||||
1|2) break ;;
|
1)
|
||||||
*) echo "must be 1 or 2" ;;
|
ask GATEWAY_TRAEFIK_NETWORK "Your Traefik's docker network name (see: docker inspect <traefik> --format '{{json .NetworkSettings.Networks}}')" ""
|
||||||
esac
|
require GATEWAY_TRAEFIK_NETWORK
|
||||||
done
|
|
||||||
if [[ "$GATEWAY_REACHABILITY_MODE" == "1" ]]; then
|
|
||||||
ask GATEWAY_TRAEFIK_NETWORK "Your Traefik's docker network name (docker network ls / docker inspect <traefik-container>)" ""
|
|
||||||
if [[ -z "$GATEWAY_TRAEFIK_NETWORK" ]]; then
|
|
||||||
echo "a network name is required for this choice" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
GATEWAY_CONTROL_PLANE_BIND="127.0.0.1:8090"
|
GATEWAY_CONTROL_PLANE_BIND="127.0.0.1:8090"
|
||||||
GATEWAY_CONTROL_PLANE_TARGET="furtka-gateway-control-plane:8090"
|
GATEWAY_CONTROL_PLANE_TARGET="furtka-gateway-control-plane:8090"
|
||||||
else
|
;;
|
||||||
|
2)
|
||||||
echo
|
echo
|
||||||
echo "WARNING: this binds the control-plane API beyond loopback. Its"
|
echo "WARNING: this binds the control-plane API beyond loopback. Its"
|
||||||
echo "registration response carries a WireGuard private key in plaintext"
|
echo "registration response carries a WireGuard private key in plaintext"
|
||||||
echo "JSON — put real access control (firewall/security group) around"
|
echo "JSON — put real access control (firewall/security group) around"
|
||||||
echo "whatever bind address you give it, not just this compose file."
|
echo "whatever bind address you give it."
|
||||||
ask GATEWAY_CONTROL_PLANE_BIND "Bind address:port to expose it on (e.g. 10.0.0.5:8090)" ""
|
ask GATEWAY_CONTROL_PLANE_BIND "Bind address:port to expose it on (e.g. 10.0.0.5:8090)" ""
|
||||||
ask GATEWAY_CONTROL_PLANE_TARGET "The same host:port, as YOUR Traefik will reach it" "$GATEWAY_CONTROL_PLANE_BIND"
|
require GATEWAY_CONTROL_PLANE_BIND
|
||||||
|
ask GATEWAY_CONTROL_PLANE_TARGET "The same host:port, as your Traefik will reach it" "$GATEWAY_CONTROL_PLANE_BIND"
|
||||||
GATEWAY_TRAEFIK_NETWORK=""
|
GATEWAY_TRAEFIK_NETWORK=""
|
||||||
fi
|
;;
|
||||||
|
*) echo "GATEWAY_REACHABILITY_MODE must be 1 or 2" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# ---- tokens (generated once, kept on re-run) ----
|
# ---- tokens (generated once, kept on re-run) ----
|
||||||
GATEWAY_BOX_TOKEN="${GATEWAY_BOX_TOKEN:-$(openssl rand -hex 32)}"
|
GATEWAY_BOX_TOKEN="${GATEWAY_BOX_TOKEN:-$(openssl rand -hex 32)}"
|
||||||
|
|
@ -183,74 +179,111 @@ echo "Writing $ENV_FILE"
|
||||||
echo "GATEWAY_DYNAMIC_DIR_HOST=$GATEWAY_DYNAMIC_DIR_HOST"
|
echo "GATEWAY_DYNAMIC_DIR_HOST=$GATEWAY_DYNAMIC_DIR_HOST"
|
||||||
echo "GATEWAY_CERT_RESOLVER=$GATEWAY_CERT_RESOLVER"
|
echo "GATEWAY_CERT_RESOLVER=$GATEWAY_CERT_RESOLVER"
|
||||||
echo "GATEWAY_CONTROL_PLANE_HOST=$GATEWAY_CONTROL_PLANE_HOST"
|
echo "GATEWAY_CONTROL_PLANE_HOST=$GATEWAY_CONTROL_PLANE_HOST"
|
||||||
|
echo "GATEWAY_REACHABILITY_MODE=$GATEWAY_REACHABILITY_MODE"
|
||||||
echo "GATEWAY_CONTROL_PLANE_BIND=$GATEWAY_CONTROL_PLANE_BIND"
|
echo "GATEWAY_CONTROL_PLANE_BIND=$GATEWAY_CONTROL_PLANE_BIND"
|
||||||
echo "GATEWAY_CONTROL_PLANE_TARGET=$GATEWAY_CONTROL_PLANE_TARGET"
|
echo "GATEWAY_CONTROL_PLANE_TARGET=$GATEWAY_CONTROL_PLANE_TARGET"
|
||||||
[[ -n "$GATEWAY_TRAEFIK_NETWORK" ]] && echo "GATEWAY_TRAEFIK_NETWORK=$GATEWAY_TRAEFIK_NETWORK"
|
[[ -n "$GATEWAY_TRAEFIK_NETWORK" ]] && echo "GATEWAY_TRAEFIK_NETWORK=$GATEWAY_TRAEFIK_NETWORK"
|
||||||
|
[[ -n "${GATEWAY_CONTROL_PLANE_IMAGE:-}" ]] && echo "GATEWAY_CONTROL_PLANE_IMAGE=$GATEWAY_CONTROL_PLANE_IMAGE"
|
||||||
echo "GATEWAY_BOX_TOKEN=$GATEWAY_BOX_TOKEN"
|
echo "GATEWAY_BOX_TOKEN=$GATEWAY_BOX_TOKEN"
|
||||||
echo "GATEWAY_ADMIN_TOKEN=$GATEWAY_ADMIN_TOKEN"
|
echo "GATEWAY_ADMIN_TOKEN=$GATEWAY_ADMIN_TOKEN"
|
||||||
} > "$ENV_FILE"
|
} > "$ENV_FILE"
|
||||||
chmod 600 "$ENV_FILE"
|
chmod 600 "$ENV_FILE"
|
||||||
|
|
||||||
# ---- render traefik/dynamic/control-plane.yml ----
|
# ---- join your Traefik's network from the compose file itself ----
|
||||||
echo "Rendering traefik/dynamic/control-plane.yml"
|
# An override (auto-loaded by `docker compose`, gitignored) rather than a
|
||||||
mkdir -p "$GATEWAY_DYNAMIC_DIR_HOST"
|
# `docker network connect` after startup: the control-plane talks to
|
||||||
# Only the http: block onward — the .example's header comment explains the
|
# wg-easy as soon as it starts, so the network must be there from the
|
||||||
# placeholders BY NAME (for a human reading that template), which would
|
# first second, and it must survive container recreation.
|
||||||
# otherwise also get corrupted by these same substitutions.
|
if [[ -n "$GATEWAY_TRAEFIK_NETWORK" ]]; then
|
||||||
sed -n '/^http:/,$p' traefik/dynamic/control-plane.yml.example > "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml"
|
echo "Writing docker-compose.override.yaml (joins network: $GATEWAY_TRAEFIK_NETWORK)"
|
||||||
sed -i "s|GATEWAY_CONTROL_PLANE_HOST|$GATEWAY_CONTROL_PLANE_HOST|" "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml"
|
cat > docker-compose.override.yaml <<EOF
|
||||||
sed -i "s|GATEWAY_CONTROL_PLANE_TARGET|$GATEWAY_CONTROL_PLANE_TARGET|" "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml"
|
# Generated by ops/deploy.sh — joins your Traefik's docker network.
|
||||||
if [[ -z "$GATEWAY_CERT_RESOLVER" ]]; then
|
services:
|
||||||
# No resolver configured on your Traefik — drop the tls: block entirely
|
control-plane:
|
||||||
# so its own default cert applies, mirroring what GATEWAY_CERT_RESOLVER
|
networks:
|
||||||
# blank means for every app route (see traefikconf.py).
|
- default
|
||||||
sed -i "/^ tls:$/,/^ certResolver:.*$/d" "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml"
|
- traefik
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
name: $GATEWAY_TRAEFIK_NETWORK
|
||||||
|
external: true
|
||||||
|
EOF
|
||||||
else
|
else
|
||||||
sed -i "s|certResolver: le|certResolver: $GATEWAY_CERT_RESOLVER|" "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml"
|
rm -f docker-compose.override.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- render the control-plane's Traefik route (not yet installed) ----
|
||||||
|
echo "Writing rendered/control-plane.yml"
|
||||||
|
mkdir -p rendered
|
||||||
|
# Only the http: block onward — the .example's header comment explains the
|
||||||
|
# placeholders BY NAME, which these substitutions would otherwise corrupt.
|
||||||
|
sed -n '/^http:/,$p' traefik/dynamic/control-plane.yml.example > rendered/control-plane.yml
|
||||||
|
sed -i "s|GATEWAY_CONTROL_PLANE_HOST|$GATEWAY_CONTROL_PLANE_HOST|" rendered/control-plane.yml
|
||||||
|
sed -i "s|GATEWAY_CONTROL_PLANE_TARGET|$GATEWAY_CONTROL_PLANE_TARGET|" rendered/control-plane.yml
|
||||||
|
if [[ -z "$GATEWAY_CERT_RESOLVER" ]]; then
|
||||||
|
# No resolver on your Traefik — drop the tls: block so its default cert
|
||||||
|
# applies, mirroring what a blank GATEWAY_CERT_RESOLVER means for every
|
||||||
|
# app route (see traefikconf.py).
|
||||||
|
sed -i "/^ tls:$/,/^ certResolver:.*$/d" rendered/control-plane.yml
|
||||||
|
else
|
||||||
|
sed -i "s|certResolver: le|certResolver: $GATEWAY_CERT_RESOLVER|" rendered/control-plane.yml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$RENDER_ONLY" -eq 1 ]]; then
|
if [[ "$RENDER_ONLY" -eq 1 ]]; then
|
||||||
echo
|
echo
|
||||||
echo "--render-only: stopping before compose up / network join / health check."
|
echo "--render-only: nothing live was touched. Inspect .env and rendered/control-plane.yml."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---- bring the control-plane up ----
|
# ---- bring the control-plane up (always built from this checkout) ----
|
||||||
echo
|
echo
|
||||||
echo "== docker compose up -d =="
|
echo "== docker compose up -d --build =="
|
||||||
docker compose up -d
|
docker compose up -d --build
|
||||||
|
|
||||||
# ---- join your Traefik's network, if that's the reachability choice ----
|
|
||||||
if [[ -n "$GATEWAY_TRAEFIK_NETWORK" ]]; then
|
|
||||||
echo
|
|
||||||
echo "== joining docker network: $GATEWAY_TRAEFIK_NETWORK =="
|
|
||||||
docker network connect "$GATEWAY_TRAEFIK_NETWORK" furtka-gateway-control-plane 2>/dev/null \
|
|
||||||
|| echo " (already connected, or connect failed — check \`docker network ls\` and the name above)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ---- health check ----
|
|
||||||
echo
|
echo
|
||||||
echo "== health check =="
|
echo "== health check =="
|
||||||
for _ in $(seq 1 10); do
|
healthy=0
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
if curl -sf "http://${GATEWAY_CONTROL_PLANE_BIND}/healthz" >/dev/null 2>&1; then
|
if curl -sf "http://${GATEWAY_CONTROL_PLANE_BIND}/healthz" >/dev/null 2>&1; then
|
||||||
echo "control-plane is up."
|
healthy=1
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 2
|
||||||
done
|
done
|
||||||
if ! curl -sf "http://${GATEWAY_CONTROL_PLANE_BIND}/healthz" >/dev/null 2>&1; then
|
if [[ "$healthy" -ne 1 ]]; then
|
||||||
echo "control-plane did not become healthy — check: docker compose logs control-plane" >&2
|
echo "control-plane did not become healthy — check: docker compose logs control-plane" >&2
|
||||||
|
echo "(a wrong WG_EASY_URL or wg-easy password shows up there)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
echo " ok: control-plane is up"
|
||||||
|
|
||||||
|
# ---- install the route into your Traefik's dynamic directory ----
|
||||||
|
mkdir -p "$GATEWAY_DYNAMIC_DIR_HOST"
|
||||||
|
cp rendered/control-plane.yml "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml"
|
||||||
|
echo " installed $GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml"
|
||||||
|
|
||||||
|
public_ok=0
|
||||||
|
for _ in $(seq 1 20); do
|
||||||
|
if curl -sf -m 10 "https://$GATEWAY_CONTROL_PLANE_HOST/healthz" >/dev/null 2>&1; then
|
||||||
|
public_ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
if [[ "$public_ok" -eq 1 ]]; then
|
||||||
|
echo " ok: https://$GATEWAY_CONTROL_PLANE_HOST/healthz"
|
||||||
|
else
|
||||||
|
echo " WARNING: https://$GATEWAY_CONTROL_PLANE_HOST/healthz not reachable."
|
||||||
|
echo " Check that DNS points at your Traefik and that Traefik watches"
|
||||||
|
echo " $GATEWAY_DYNAMIC_DIR_HOST (providers.file.directory)."
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "== done =="
|
echo "== done =="
|
||||||
echo "Still needed before a real box can connect:"
|
echo "Before publishing apps, make sure your Traefik can reach wg-easy's peer"
|
||||||
echo " - Add $GATEWAY_DYNAMIC_DIR_HOST as a providers.file directory on"
|
echo "subnet — see the README's routing section. Quick check after a box connects:"
|
||||||
echo " your Traefik, if it isn't already watching it."
|
echo " docker exec <your-traefik> ping -c1 10.8.0.<box>"
|
||||||
echo " - Verify Traefik can actually reach wg-easy's WireGuard peer subnet"
|
echo
|
||||||
echo " (the README's Prerequisites checklist has a command for this) —"
|
echo "Connect a Furtka box: open its web UI -> Settings -> Gateway, and enter"
|
||||||
echo " this is the one thing that can't be checked from here."
|
echo " Gateway URL: https://$GATEWAY_CONTROL_PLANE_HOST"
|
||||||
echo " - DNS: A/AAAA for $GATEWAY_CONTROL_PLANE_HOST -> wherever your"
|
echo " Registration token: the GATEWAY_BOX_TOKEN value in $REPO_ROOT/.env"
|
||||||
echo " Traefik is reachable."
|
|
||||||
echo " - On the box: furtka gateway connect <this gateway's URL> <GATEWAY_BOX_TOKEN from .env> <box name>"
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
# Copy to traefik/dynamic/control-plane.yml (into whatever directory YOUR
|
# Template for the control-plane's own Traefik route. ops/deploy.sh renders
|
||||||
# Traefik's file provider watches — see the root README's Prerequisites
|
# it to rendered/control-plane.yml and installs it into the directory YOUR
|
||||||
# checklist) with two placeholders substituted:
|
# Traefik's file provider watches, with two placeholders substituted:
|
||||||
#
|
#
|
||||||
# GATEWAY_CONTROL_PLANE_HOST a real hostname you control (e.g.
|
# GATEWAY_CONTROL_PLANE_HOST a real hostname you control (e.g.
|
||||||
# gw.example.com), with its own A/AAAA record
|
# gw.example.com), with its own A/AAAA record
|
||||||
# pointing at this host.
|
# pointing at this host.
|
||||||
# GATEWAY_CONTROL_PLANE_TARGET where YOUR Traefik actually reaches the
|
# GATEWAY_CONTROL_PLANE_TARGET where YOUR Traefik actually reaches the
|
||||||
# control-plane container. If it joined your
|
# control-plane container. If it joined your
|
||||||
# Traefik's docker network (ops/deploy.sh
|
# Traefik's docker network (the recommended
|
||||||
# does this via `docker network connect` —
|
# path — ops/deploy.sh sets that up), this is
|
||||||
# the primary, recommended path), this is
|
# `furtka-gateway-control-plane:8090`, the
|
||||||
# `furtka-gateway-control-plane:8090` — the
|
# container's fixed name (unambiguous on a
|
||||||
# container's fixed name, which Docker's
|
# shared network, unlike a generic service
|
||||||
# embedded DNS resolves on any network it's
|
# name like `control-plane`).
|
||||||
# joined to (unlike the compose *service*
|
|
||||||
# name `control-plane`, which is only
|
|
||||||
# guaranteed to resolve on networks compose
|
|
||||||
# itself manages — not one joined after the
|
|
||||||
# fact via a bare `docker network connect`).
|
|
||||||
# If instead you exposed it on a separate
|
# If instead you exposed it on a separate
|
||||||
# host (GATEWAY_CONTROL_PLANE_BIND widened
|
# host (GATEWAY_CONTROL_PLANE_BIND widened
|
||||||
# beyond 127.0.0.1 in docker-compose.yaml),
|
# beyond 127.0.0.1 in docker-compose.yaml),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue