mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-03 03:59:07 +02:00
[misc] Move enterprise setup to Traefik and harden migration (#7042)
This commit is contained in:
@@ -11,6 +11,10 @@ SED_STRIP_PADDING='s/=//g'
|
|||||||
|
|
||||||
NETBIRD_EULA_URL="https://netbird.io/self-hosted-EULA"
|
NETBIRD_EULA_URL="https://netbird.io/self-hosted-EULA"
|
||||||
|
|
||||||
|
# Static IP for Traefik inside the compose bridge network. The management
|
||||||
|
# server trusts X-Forwarded-* headers from this address only.
|
||||||
|
TRAEFIK_IP="172.30.0.10"
|
||||||
|
|
||||||
check_docker_compose() {
|
check_docker_compose() {
|
||||||
if command -v docker-compose &> /dev/null; then
|
if command -v docker-compose &> /dev/null; then
|
||||||
echo "docker-compose"
|
echo "docker-compose"
|
||||||
@@ -80,7 +84,7 @@ read_nb_domain() {
|
|||||||
if ! check_domain_resolves "$value"; then
|
if ! check_domain_resolves "$value"; then
|
||||||
echo "" > /dev/stderr
|
echo "" > /dev/stderr
|
||||||
echo "Warning: '$value' does not resolve via DNS from this host." > /dev/stderr
|
echo "Warning: '$value' does not resolve via DNS from this host." > /dev/stderr
|
||||||
echo "Caddy will not be able to issue TLS certificates until it does." > /dev/stderr
|
echo "Traefik will not be able to issue TLS certificates until it does." > /dev/stderr
|
||||||
local confirm=""
|
local confirm=""
|
||||||
echo -n "Continue anyway? [y/N]: " > /dev/stderr
|
echo -n "Continue anyway? [y/N]: " > /dev/stderr
|
||||||
read -r confirm < /dev/tty
|
read -r confirm < /dev/tty
|
||||||
@@ -92,6 +96,23 @@ read_nb_domain() {
|
|||||||
echo "$value"
|
echo "$value"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
read_letsencrypt_email() {
|
||||||
|
if [[ -n "${NETBIRD_LETSENCRYPT_EMAIL:-}" ]]; then
|
||||||
|
echo "$NETBIRD_LETSENCRYPT_EMAIL"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local value=""
|
||||||
|
echo "Enter your email for Let's Encrypt certificate notifications." > /dev/stderr
|
||||||
|
echo -n "Email address: " > /dev/stderr
|
||||||
|
read -r value < /dev/tty
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
echo "Email is required for Let's Encrypt." > /dev/stderr
|
||||||
|
read_letsencrypt_email
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "$value"
|
||||||
|
}
|
||||||
|
|
||||||
read_required() {
|
read_required() {
|
||||||
local prompt="$1"
|
local prompt="$1"
|
||||||
local value=""
|
local value=""
|
||||||
@@ -204,11 +225,11 @@ init_environment() {
|
|||||||
check_openssl
|
check_openssl
|
||||||
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
|
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
|
||||||
|
|
||||||
if [[ -f .env ]] || [[ -f docker-compose.yml ]] || [[ -f config.yaml ]] || [[ -f Caddyfile ]]; then
|
if [[ -f .env ]] || [[ -f docker-compose.yml ]] || [[ -f config.yaml ]]; then
|
||||||
echo "Generated files already exist in $(pwd)."
|
echo "Generated files already exist in $(pwd)."
|
||||||
echo "If you want to reinitialize the environment, please remove them first:"
|
echo "If you want to reinitialize the environment, please remove them first:"
|
||||||
echo " $DOCKER_COMPOSE_COMMAND down --volumes # removes all containers and volumes"
|
echo " $DOCKER_COMPOSE_COMMAND down --volumes # removes all containers and volumes"
|
||||||
echo " rm -f .env docker-compose.yml Caddyfile config.yaml"
|
echo " rm -f .env docker-compose.yml config.yaml"
|
||||||
echo "Be aware this will remove all data from the database."
|
echo "Be aware this will remove all data from the database."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -230,6 +251,9 @@ init_environment() {
|
|||||||
echo ""
|
echo ""
|
||||||
NETBIRD_DOMAIN=$(read_nb_domain)
|
NETBIRD_DOMAIN=$(read_nb_domain)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
NETBIRD_LETSENCRYPT_EMAIL=$(read_letsencrypt_email)
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
NETBIRD_LICENSE_KEY=$(read_secret "Enter license key (input hidden)")
|
NETBIRD_LICENSE_KEY=$(read_secret "Enter license key (input hidden)")
|
||||||
@@ -247,6 +271,7 @@ init_environment() {
|
|||||||
echo "Selected:"
|
echo "Selected:"
|
||||||
echo " Traffic flow: ${NETBIRD_TRAFFIC_FLOW}"
|
echo " Traffic flow: ${NETBIRD_TRAFFIC_FLOW}"
|
||||||
echo " Domain: ${NETBIRD_DOMAIN}"
|
echo " Domain: ${NETBIRD_DOMAIN}"
|
||||||
|
echo " ACME email: ${NETBIRD_LETSENCRYPT_EMAIL}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Rendering files into $(pwd) ..."
|
echo "Rendering files into $(pwd) ..."
|
||||||
install -m 600 /dev/null .env
|
install -m 600 /dev/null .env
|
||||||
@@ -256,7 +281,6 @@ init_environment() {
|
|||||||
if [[ -z "${NETBIRD_LICENSE_SERVER_BASE_URL:-}" ]]; then
|
if [[ -z "${NETBIRD_LICENSE_SERVER_BASE_URL:-}" ]]; then
|
||||||
sed -i.bak '/NETBIRD_LICENSE_SERVER_BASE_URL/d' docker-compose.yml && rm -f docker-compose.yml.bak
|
sed -i.bak '/NETBIRD_LICENSE_SERVER_BASE_URL/d' docker-compose.yml && rm -f docker-compose.yml.bak
|
||||||
fi
|
fi
|
||||||
render_caddyfile > Caddyfile
|
|
||||||
install -m 600 /dev/null config.yaml
|
install -m 600 /dev/null config.yaml
|
||||||
render_config_yaml >> config.yaml
|
render_config_yaml >> config.yaml
|
||||||
|
|
||||||
@@ -283,7 +307,7 @@ init_environment() {
|
|||||||
echo "All configuration and secrets are stored (mode 600) in $(pwd)/.env"
|
echo "All configuration and secrets are stored (mode 600) in $(pwd)/.env"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Tail logs:"
|
echo "Tail logs:"
|
||||||
echo " cd $(pwd) && $DOCKER_COMPOSE_COMMAND logs -f netbird-server caddy"
|
echo " cd $(pwd) && $DOCKER_COMPOSE_COMMAND logs -f netbird-server traefik"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -306,6 +330,11 @@ NETBIRD_TRAFFIC_FLOW_ENABLED=${NETBIRD_TRAFFIC_FLOW}
|
|||||||
# Domain
|
# Domain
|
||||||
NETBIRD_DOMAIN=${NETBIRD_DOMAIN}
|
NETBIRD_DOMAIN=${NETBIRD_DOMAIN}
|
||||||
|
|
||||||
|
# Reverse proxy (Traefik)
|
||||||
|
NETBIRD_LETSENCRYPT_EMAIL=${NETBIRD_LETSENCRYPT_EMAIL}
|
||||||
|
NETBIRD_TRAEFIK_TAG=${NETBIRD_TRAEFIK_TAG:-v3.6}
|
||||||
|
NETBIRD_TRAEFIK_IP=${TRAEFIK_IP}
|
||||||
|
|
||||||
# Image tags. Default to "latest"
|
# Image tags. Default to "latest"
|
||||||
NETBIRD_DASHBOARD_TAG=${NETBIRD_DASHBOARD_TAG:-latest}
|
NETBIRD_DASHBOARD_TAG=${NETBIRD_DASHBOARD_TAG:-latest}
|
||||||
NETBIRD_SERVER_TAG=${NETBIRD_SERVER_TAG:-latest}
|
NETBIRD_SERVER_TAG=${NETBIRD_SERVER_TAG:-latest}
|
||||||
@@ -378,26 +407,78 @@ EOF
|
|||||||
|
|
||||||
render_compose_common() {
|
render_compose_common() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
caddy:
|
# Reverse proxy with automatic TLS via Let's Encrypt. Routes are declared as
|
||||||
|
# labels on the services below and picked up through the Docker provider.
|
||||||
|
traefik:
|
||||||
<<: *default
|
<<: *default
|
||||||
image: caddy:2
|
image: traefik:${NETBIRD_TRAEFIK_TAG}
|
||||||
container_name: netbird-caddy
|
container_name: netbird-traefik
|
||||||
networks: [netbird]
|
networks:
|
||||||
environment:
|
netbird:
|
||||||
- CADDY_SECURE_DOMAIN=${NETBIRD_DOMAIN}
|
ipv4_address: ${NETBIRD_TRAEFIK_IP}
|
||||||
|
command:
|
||||||
|
# Logging
|
||||||
|
- "--log.level=INFO"
|
||||||
|
- "--accesslog=true"
|
||||||
|
# Docker provider
|
||||||
|
- "--providers.docker=true"
|
||||||
|
- "--providers.docker.exposedbydefault=false"
|
||||||
|
- "--providers.docker.network=netbird"
|
||||||
|
# Entrypoints
|
||||||
|
- "--entrypoints.web.address=:80"
|
||||||
|
- "--entrypoints.websecure.address=:443"
|
||||||
|
- "--entrypoints.websecure.allowACMEByPass=true"
|
||||||
|
# readTimeout bounds the whole request, and gRPC streams / relay WebSockets
|
||||||
|
# never end one; idleTimeout would close the keep-alive connection they
|
||||||
|
# are reused over. Entrypoint-wide is the only scope Traefik offers here.
|
||||||
|
# writeTimeout is left alone: it already defaults to 0.
|
||||||
|
- "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0"
|
||||||
|
- "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=0"
|
||||||
|
# HTTP to HTTPS redirect
|
||||||
|
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
|
||||||
|
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
||||||
|
# Let's Encrypt ACME
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.email=${NETBIRD_LETSENCRYPT_EMAIL}"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
||||||
ports:
|
ports:
|
||||||
- '443:443'
|
- '443:443'
|
||||||
- '443:443/udp'
|
|
||||||
- '80:80'
|
- '80:80'
|
||||||
volumes:
|
volumes:
|
||||||
- netbird_caddy_data:/data
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
- netbird_traefik_letsencrypt:/letsencrypt
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
# Shared security headers, referenced by every NetBird router below. A
|
||||||
|
# label-declared middleware only exists while its container runs, so this
|
||||||
|
# lives on Traefik itself: declaring it on an app container would drop
|
||||||
|
# every router referencing it whenever that container restarts.
|
||||||
|
- traefik.http.middlewares.nb-security.headers.stsSeconds=3600
|
||||||
|
- traefik.http.middlewares.nb-security.headers.stsIncludeSubdomains=true
|
||||||
|
- traefik.http.middlewares.nb-security.headers.contentTypeNosniff=true
|
||||||
|
- traefik.http.middlewares.nb-security.headers.browserXssFilter=true
|
||||||
|
- traefik.http.middlewares.nb-security.headers.referrerPolicy=strict-origin-when-cross-origin
|
||||||
|
- traefik.http.middlewares.nb-security.headers.customResponseHeaders.X-Frame-Options=SAMEORIGIN
|
||||||
|
# Empty value strips the header. Only the dashboard's nginx sets one; the
|
||||||
|
# server emits none. Do not quote it — "" would send a literal Server: "".
|
||||||
|
- traefik.http.middlewares.nb-security.headers.customResponseHeaders.Server=
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
<<: *default
|
<<: *default
|
||||||
image: ghcr.io/netbirdio/dashboard-cloud:${NETBIRD_DASHBOARD_TAG}
|
image: ghcr.io/netbirdio/dashboard-cloud:${NETBIRD_DASHBOARD_TAG}
|
||||||
container_name: netbird-dashboard
|
container_name: netbird-dashboard
|
||||||
networks: [netbird]
|
networks: [netbird]
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
# Dashboard catch-all: lowest priority so every route below wins
|
||||||
|
- traefik.http.routers.netbird-dashboard.rule=Host(`${NETBIRD_DOMAIN}`)
|
||||||
|
- traefik.http.routers.netbird-dashboard.entrypoints=websecure
|
||||||
|
- traefik.http.routers.netbird-dashboard.tls=true
|
||||||
|
- traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.netbird-dashboard.middlewares=nb-security@docker
|
||||||
|
- traefik.http.routers.netbird-dashboard.service=dashboard
|
||||||
|
- traefik.http.routers.netbird-dashboard.priority=1
|
||||||
|
- traefik.http.services.dashboard.loadbalancer.server.port=80
|
||||||
environment:
|
environment:
|
||||||
- NETBIRD_MGMT_API_ENDPOINT=https://${NETBIRD_DOMAIN}
|
- NETBIRD_MGMT_API_ENDPOINT=https://${NETBIRD_DOMAIN}
|
||||||
- NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${NETBIRD_DOMAIN}
|
- NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${NETBIRD_DOMAIN}
|
||||||
@@ -435,6 +516,28 @@ render_compose_server() {
|
|||||||
- netbird_data:/var/lib/netbird
|
- netbird_data:/var/lib/netbird
|
||||||
- ./config.yaml:/etc/netbird/config.yaml
|
- ./config.yaml:/etc/netbird/config.yaml
|
||||||
command: ["--config", "/etc/netbird/config.yaml"]
|
command: ["--config", "/etc/netbird/config.yaml"]
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
# Signal + Management gRPC (needs an h2c backend for HTTP/2 cleartext)
|
||||||
|
- traefik.http.routers.netbird-grpc.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))
|
||||||
|
- traefik.http.routers.netbird-grpc.entrypoints=websecure
|
||||||
|
- traefik.http.routers.netbird-grpc.tls=true
|
||||||
|
- traefik.http.routers.netbird-grpc.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.netbird-grpc.middlewares=nb-security@docker
|
||||||
|
- traefik.http.routers.netbird-grpc.service=netbird-server-h2c
|
||||||
|
- traefik.http.routers.netbird-grpc.priority=100
|
||||||
|
# Relay WebSocket, management API, and the embedded IdP
|
||||||
|
- traefik.http.routers.netbird-backend.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))
|
||||||
|
- traefik.http.routers.netbird-backend.entrypoints=websecure
|
||||||
|
- traefik.http.routers.netbird-backend.tls=true
|
||||||
|
- traefik.http.routers.netbird-backend.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.netbird-backend.middlewares=nb-security@docker
|
||||||
|
- traefik.http.routers.netbird-backend.service=netbird-server
|
||||||
|
- traefik.http.routers.netbird-backend.priority=100
|
||||||
|
# Services
|
||||||
|
- traefik.http.services.netbird-server.loadbalancer.server.port=80
|
||||||
|
- traefik.http.services.netbird-server-h2c.loadbalancer.server.port=80
|
||||||
|
- traefik.http.services.netbird-server-h2c.loadbalancer.server.scheme=h2c
|
||||||
environment:
|
environment:
|
||||||
- NB_LICENSE_KEY=${NETBIRD_LICENSE_KEY}
|
- NB_LICENSE_KEY=${NETBIRD_LICENSE_KEY}
|
||||||
- NETBIRD_LICENSE_SERVER_BASE_URL=${NETBIRD_LICENSE_SERVER_BASE_URL}
|
- NETBIRD_LICENSE_SERVER_BASE_URL=${NETBIRD_LICENSE_SERVER_BASE_URL}
|
||||||
@@ -497,6 +600,18 @@ render_compose_flow() {
|
|||||||
- NB_FLOW_NATS_ENDPOINTS=nats://nats:4222
|
- NB_FLOW_NATS_ENDPOINTS=nats://nats:4222
|
||||||
- NB_FLOW_NATS_STREAM=traffic-events
|
- NB_FLOW_NATS_STREAM=traffic-events
|
||||||
- NB_FLOW_AUTH_SECRET=${NETBIRD_RELAY_AUTH_SECRET}
|
- NB_FLOW_AUTH_SECRET=${NETBIRD_RELAY_AUTH_SECRET}
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
# Flow receiver gRPC (h2c backend)
|
||||||
|
- traefik.http.routers.netbird-flow.rule=Host(`${NETBIRD_DOMAIN}`) && PathPrefix(`/flow.FlowService/`)
|
||||||
|
- traefik.http.routers.netbird-flow.entrypoints=websecure
|
||||||
|
- traefik.http.routers.netbird-flow.tls=true
|
||||||
|
- traefik.http.routers.netbird-flow.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.netbird-flow.middlewares=nb-security@docker
|
||||||
|
- traefik.http.routers.netbird-flow.service=netbird-flow-h2c
|
||||||
|
- traefik.http.routers.netbird-flow.priority=100
|
||||||
|
- traefik.http.services.netbird-flow-h2c.loadbalancer.server.port=80
|
||||||
|
- traefik.http.services.netbird-flow-h2c.loadbalancer.server.scheme=h2c
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -536,61 +651,16 @@ EOF
|
|||||||
fi
|
fi
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
netbird_postgres:
|
netbird_postgres:
|
||||||
netbird_caddy_data:
|
netbird_traefik_letsencrypt:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
netbird:
|
netbird:
|
||||||
EOF
|
name: netbird
|
||||||
}
|
driver: bridge
|
||||||
|
ipam:
|
||||||
render_caddyfile() {
|
config:
|
||||||
cat <<'EOF'
|
- subnet: 172.30.0.0/24
|
||||||
{
|
gateway: 172.30.0.1
|
||||||
servers :80,:443 {
|
|
||||||
protocols h1 h2c h2 h3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(security_headers) {
|
|
||||||
header * {
|
|
||||||
Strict-Transport-Security "max-age=3600; includeSubDomains; preload"
|
|
||||||
X-Content-Type-Options "nosniff"
|
|
||||||
X-Frame-Options "SAMEORIGIN"
|
|
||||||
X-XSS-Protection "1; mode=block"
|
|
||||||
-Server
|
|
||||||
Referrer-Policy strict-origin-when-cross-origin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:80 {
|
|
||||||
redir https://{$CADDY_SECURE_DOMAIN}{uri} permanent
|
|
||||||
}
|
|
||||||
|
|
||||||
{$CADDY_SECURE_DOMAIN}:443 {
|
|
||||||
import security_headers
|
|
||||||
# Signal (gRPC over h2c)
|
|
||||||
reverse_proxy /signalexchange.SignalExchange/* h2c://netbird-server:80
|
|
||||||
# Management (gRPC over h2c + HTTP)
|
|
||||||
reverse_proxy /management.ManagementService/* h2c://netbird-server:80
|
|
||||||
reverse_proxy /api/* netbird-server:80
|
|
||||||
reverse_proxy /ws-proxy/* netbird-server:80
|
|
||||||
# Embedded IdP (OAuth2 endpoints served by netbird server)
|
|
||||||
reverse_proxy /oauth2/* netbird-server:80
|
|
||||||
# Relay (WebSocket multiplexed on the same port)
|
|
||||||
reverse_proxy /relay* netbird-server:80
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [[ "$NETBIRD_TRAFFIC_FLOW" == "yes" ]]; then
|
|
||||||
cat <<'EOF'
|
|
||||||
# Flow receiver (gRPC over h2c)
|
|
||||||
reverse_proxy /flow.FlowService/* h2c://receiver:80
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat <<'EOF'
|
|
||||||
# Dashboard
|
|
||||||
reverse_proxy /* dashboard:80
|
|
||||||
}
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,7 +679,7 @@ server:
|
|||||||
logLevel: "info"
|
logLevel: "info"
|
||||||
logFile: "console"
|
logFile: "console"
|
||||||
|
|
||||||
# TLS is terminated by Caddy in front; leave this block empty.
|
# TLS is terminated by Traefik in front; leave this block empty.
|
||||||
tls:
|
tls:
|
||||||
certFile: ""
|
certFile: ""
|
||||||
keyFile: ""
|
keyFile: ""
|
||||||
@@ -632,6 +702,16 @@ server:
|
|||||||
cliRedirectURIs:
|
cliRedirectURIs:
|
||||||
- "http://localhost:53000/"
|
- "http://localhost:53000/"
|
||||||
|
|
||||||
|
# Trust X-Forwarded-* only from the Traefik container's static address. Both
|
||||||
|
# keys must stay in step with the ipv4_address pinned in docker-compose.yml:
|
||||||
|
# trustedPeers decides whether forwarded headers are read at all, and leaving
|
||||||
|
# it unset falls back to 0.0.0.0/0.
|
||||||
|
reverseProxy:
|
||||||
|
trustedPeers:
|
||||||
|
- "${TRAEFIK_IP}/32"
|
||||||
|
trustedHTTPProxies:
|
||||||
|
- "${TRAEFIK_IP}/32"
|
||||||
|
|
||||||
store:
|
store:
|
||||||
engine: "postgres"
|
engine: "postgres"
|
||||||
dsn: "${POSTGRES_DSN}"
|
dsn: "${POSTGRES_DSN}"
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ set -o pipefail
|
|||||||
# 2. Postgres migration — add Postgres, migrate SQLite data via migrate-store.
|
# 2. Postgres migration — add Postgres, migrate SQLite data via migrate-store.
|
||||||
# 3. Traffic flow — add NATS + flow-enricher + flow-receiver.
|
# 3. Traffic flow — add NATS + flow-enricher + flow-receiver.
|
||||||
#
|
#
|
||||||
# To revert:
|
# If any step fails once the stack has been touched, the script rolls itself
|
||||||
|
# back automatically: generated files are removed, the Postgres volume this run
|
||||||
|
# created is dropped, and the original deployment is started again.
|
||||||
|
#
|
||||||
|
# To revert a successful migration:
|
||||||
# docker compose down
|
# docker compose down
|
||||||
# rm -f docker-compose.override.yml config.yaml.enterprise
|
# rm -f docker-compose.override.yml config.yaml.enterprise
|
||||||
# # If Postgres migration was done, also restore the SQLite backup printed
|
# # If Postgres migration was done, also restore the SQLite backup printed
|
||||||
@@ -25,6 +29,15 @@ set -o pipefail
|
|||||||
OVERRIDE_FILE="docker-compose.override.yml"
|
OVERRIDE_FILE="docker-compose.override.yml"
|
||||||
ENTERPRISE_CONFIG_FILE="config.yaml.enterprise"
|
ENTERPRISE_CONFIG_FILE="config.yaml.enterprise"
|
||||||
|
|
||||||
|
# Rollback bookkeeping. ROLLBACK_STATE flips to "armed" the moment the script
|
||||||
|
# starts mutating the deployment, and back to "disarmed" once the migration has
|
||||||
|
# completed successfully.
|
||||||
|
ROLLBACK_STATE="disarmed"
|
||||||
|
ENV_EXISTED="unknown"
|
||||||
|
ENV_BACKUP=""
|
||||||
|
PG_VOLUME_NAME=""
|
||||||
|
BACKUP_DIR=""
|
||||||
|
|
||||||
NETBIRD_EULA_URL="https://netbird.io/self-hosted-EULA"
|
NETBIRD_EULA_URL="https://netbird.io/self-hosted-EULA"
|
||||||
|
|
||||||
check_docker_compose() {
|
check_docker_compose() {
|
||||||
@@ -361,7 +374,77 @@ render_enterprise_config() {
|
|||||||
# Execution steps
|
# Execution steps
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
resolve_data_volume() {
|
combined_container_id() {
|
||||||
|
$DOCKER_COMPOSE_COMMAND ps -aq "$COMBINED_SERVICE" 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
container_data_mount() {
|
||||||
|
local container="$1"
|
||||||
|
[[ -n "$container" ]] || return 0
|
||||||
|
docker inspect "$container" --format \
|
||||||
|
'{{range .Mounts}}{{if eq .Destination "/var/lib/netbird"}}{{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}}{{end}}{{end}}' 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# The name comes from the container, so `-v` cannot invent an empty volume here.
|
||||||
|
# 0 = empty, 1 = holds data, 2 = could not determine. A failed listing must not
|
||||||
|
# be reported as empty: that would abort a healthy migration over a pull error
|
||||||
|
# or an unreadable bind mount.
|
||||||
|
data_dir_state() {
|
||||||
|
local src="$1" out
|
||||||
|
if [[ "$src" == /* ]]; then
|
||||||
|
[[ -d "$src" ]] || return 2
|
||||||
|
out=$(ls -A "$src" 2>/dev/null) || return 2
|
||||||
|
else
|
||||||
|
docker volume inspect "$src" &> /dev/null || return 0
|
||||||
|
out=$(docker run --rm -v "${src}:/d:ro" busybox sh -c 'ls -A /d' 2>/dev/null) || return 2
|
||||||
|
fi
|
||||||
|
[[ -z "$out" ]] && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_data_directory() {
|
||||||
|
[[ "$MIGRATE_POSTGRES" == "yes" ]] || return 0
|
||||||
|
|
||||||
|
local container
|
||||||
|
container=$(combined_container_id)
|
||||||
|
if [[ -z "$container" ]]; then
|
||||||
|
echo "" > /dev/stderr
|
||||||
|
echo "No container found for service '$COMBINED_SERVICE'." > /dev/stderr
|
||||||
|
echo "The migration backs up the store by copying it out of that container," > /dev/stderr
|
||||||
|
echo "so it has to exist. Start the deployment and re-run:" > /dev/stderr
|
||||||
|
echo " $DOCKER_COMPOSE_COMMAND up -d" > /dev/stderr
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local src
|
||||||
|
src=$(container_data_mount "$container")
|
||||||
|
if [[ -z "$src" ]]; then
|
||||||
|
echo "" > /dev/stderr
|
||||||
|
echo "The '$COMBINED_SERVICE' container has nothing mounted at /var/lib/netbird." > /dev/stderr
|
||||||
|
echo "Cannot locate the NetBird store to back it up." > /dev/stderr
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local state=0
|
||||||
|
data_dir_state "$src" || state=$?
|
||||||
|
if [[ $state -eq 0 ]]; then
|
||||||
|
echo "" > /dev/stderr
|
||||||
|
echo "The NetBird data directory is empty:" > /dev/stderr
|
||||||
|
echo " $src" > /dev/stderr
|
||||||
|
echo "There is nothing to migrate. Check that you are running this from the" > /dev/stderr
|
||||||
|
echo "deployment directory of the NetBird install you mean to migrate." > /dev/stderr
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ $state -eq 2 ]]; then
|
||||||
|
echo " ⚠ Could not read $src to confirm it holds data — continuing." > /dev/stderr
|
||||||
|
echo " The backup step still fails loudly if it turns out to be empty." > /dev/stderr
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Data directory: $src"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only for the Postgres volume, which has no container to read it off yet.
|
||||||
|
resolve_compose_volume() {
|
||||||
local short="$1"
|
local short="$1"
|
||||||
local actual
|
local actual
|
||||||
# Resolve project-prefixed volume name from Docker Compose config first.
|
# Resolve project-prefixed volume name from Docker Compose config first.
|
||||||
@@ -391,18 +474,21 @@ resolve_data_volume() {
|
|||||||
backup_sqlite() {
|
backup_sqlite() {
|
||||||
BACKUP_DIR="$(pwd)/backups/sqlite-pre-enterprise-$(date +%Y%m%d-%H%M%S)"
|
BACKUP_DIR="$(pwd)/backups/sqlite-pre-enterprise-$(date +%Y%m%d-%H%M%S)"
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
local data_volume_actual
|
|
||||||
data_volume_actual=$(resolve_data_volume "$DATA_VOLUME")
|
local container
|
||||||
echo "Backing up SQLite store from volume '$data_volume_actual' to $BACKUP_DIR ..."
|
container=$(combined_container_id)
|
||||||
docker run --rm \
|
if [[ -z "$container" ]]; then
|
||||||
-v "${data_volume_actual}:/var/lib/netbird:ro" \
|
echo " ⚠ No container found for '$COMBINED_SERVICE' — cannot back up the store." > /dev/stderr
|
||||||
-v "${BACKUP_DIR}:/backup" \
|
exit 1
|
||||||
busybox \
|
fi
|
||||||
sh -c 'cp -a /var/lib/netbird/. /backup/ 2>/dev/null || true'
|
|
||||||
|
echo "Backing up the NetBird store to $BACKUP_DIR ..."
|
||||||
|
docker cp "${container}:/var/lib/netbird/." "$BACKUP_DIR/"
|
||||||
|
|
||||||
local copied
|
local copied
|
||||||
copied=$(find "$BACKUP_DIR" -mindepth 1 | head -1)
|
copied=$(find "$BACKUP_DIR" -mindepth 1 | head -1)
|
||||||
if [[ -z "$copied" ]]; then
|
if [[ -z "$copied" ]]; then
|
||||||
echo " ⚠ Backup directory is empty — the volume '$data_volume_actual' didn't contain data. Aborting." > /dev/stderr
|
echo " ⚠ Backup directory is empty — /var/lib/netbird held no data. Aborting." > /dev/stderr
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo " done"
|
echo " done"
|
||||||
@@ -414,6 +500,135 @@ run_migrate_store() {
|
|||||||
echo " done"
|
echo " done"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Rollback — a failed run must not leave the operator with a stopped stack and
|
||||||
|
# half-written artifacts.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Resolve the name Compose would give the Postgres volume before the override
|
||||||
|
# exists, so a leftover volume can be spotted up front.
|
||||||
|
compose_project_name() {
|
||||||
|
local container project
|
||||||
|
container=$($DOCKER_COMPOSE_COMMAND ps -aq 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$container" ]]; then
|
||||||
|
project=$(docker inspect "$container" \
|
||||||
|
--format '{{index .Config.Labels "com.docker.compose.project"}}' 2>/dev/null)
|
||||||
|
if [[ -n "$project" ]]; then
|
||||||
|
echo "$project"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
project=$($DOCKER_COMPOSE_COMMAND config 2>/dev/null | yq eval '.name // ""' - 2>/dev/null)
|
||||||
|
if [[ -n "$project" ]] && [[ "$project" != "null" ]]; then
|
||||||
|
echo "$project"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
postgres_volume_name() {
|
||||||
|
local project
|
||||||
|
project=$(compose_project_name)
|
||||||
|
if [[ -n "$project" ]]; then
|
||||||
|
echo "${project}_netbird_postgres"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Postgres skips initdb when its data directory is non-empty, so a volume left
|
||||||
|
# behind by an interrupted run would keep the old password and old contents,
|
||||||
|
# and migrate-store would fail against it.
|
||||||
|
check_stale_postgres_volume() {
|
||||||
|
[[ "$MIGRATE_POSTGRES" == "yes" ]] || return 0
|
||||||
|
|
||||||
|
PG_VOLUME_NAME=$(postgres_volume_name)
|
||||||
|
if [[ -z "$PG_VOLUME_NAME" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo " ⚠ Could not determine the Compose project name, so a Postgres volume"
|
||||||
|
echo " left over from an earlier attempt cannot be checked for. If a"
|
||||||
|
echo " previous run failed, remove it before continuing:"
|
||||||
|
echo " docker volume ls | grep netbird_postgres"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
docker volume inspect "$PG_VOLUME_NAME" &> /dev/null || return 0
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ⚠ A Postgres volume from an earlier attempt already exists:"
|
||||||
|
echo " $PG_VOLUME_NAME"
|
||||||
|
echo " Postgres does not re-initialise a non-empty data directory, so the"
|
||||||
|
echo " migration would run against stale credentials and stale data."
|
||||||
|
local remove
|
||||||
|
remove=$(read_yes_no " Remove it and continue?" "y")
|
||||||
|
if [[ "$remove" != "yes" ]]; then
|
||||||
|
echo "" > /dev/stderr
|
||||||
|
echo "Aborted. Remove it manually with: docker volume rm $PG_VOLUME_NAME" > /dev/stderr
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker volume rm "$PG_VOLUME_NAME" > /dev/null
|
||||||
|
echo " Removed."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Undo whatever this run changed and start the previous deployment again.
|
||||||
|
rollback() {
|
||||||
|
ROLLBACK_STATE="done"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "──────────────────────────────────────────────────────────────────────"
|
||||||
|
echo " Migration failed — restoring the previous deployment"
|
||||||
|
echo "──────────────────────────────────────────────────────────────────────"
|
||||||
|
|
||||||
|
# Resolve while the override is still present; without it Compose no longer
|
||||||
|
# knows about the Postgres volume.
|
||||||
|
local pg_volume="$PG_VOLUME_NAME"
|
||||||
|
if [[ -z "$pg_volume" ]] && [[ "$MIGRATE_POSTGRES" == "yes" ]]; then
|
||||||
|
pg_volume=$(postgres_volume_name)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Stopping services ..."
|
||||||
|
$DOCKER_COMPOSE_COMMAND down || true
|
||||||
|
|
||||||
|
echo "Removing generated files ..."
|
||||||
|
rm -f "$OVERRIDE_FILE" "$ENTERPRISE_CONFIG_FILE"
|
||||||
|
|
||||||
|
# Restore .env to exactly what it was, or remove it if this run created it.
|
||||||
|
if [[ "$ENV_EXISTED" == "yes" ]] && [[ -f "$ENV_BACKUP" ]]; then
|
||||||
|
mv -f "$ENV_BACKUP" .env || echo " ⚠ Could not restore .env from $ENV_BACKUP." > /dev/stderr
|
||||||
|
elif [[ "$ENV_EXISTED" == "no" ]]; then
|
||||||
|
rm -f .env || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only ever the volume this run created — never the NetBird data volume.
|
||||||
|
if [[ -n "$pg_volume" ]] && [[ "$pg_volume" != "null" ]]; then
|
||||||
|
echo "Removing Postgres volume $pg_volume ..."
|
||||||
|
docker volume rm "$pg_volume" &> /dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting the previous deployment ..."
|
||||||
|
if ! $DOCKER_COMPOSE_COMMAND up -d; then
|
||||||
|
echo ""
|
||||||
|
echo " ⚠ Could not start the previous deployment automatically." > /dev/stderr
|
||||||
|
echo " Run: $DOCKER_COMPOSE_COMMAND up -d" > /dev/stderr
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Rolled back. Your docker-compose.yml, config.yaml and the NetBird data"
|
||||||
|
echo "volume were never modified."
|
||||||
|
if [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
|
||||||
|
echo "The SQLite backup taken during this run is kept at:"
|
||||||
|
echo " $BACKUP_DIR"
|
||||||
|
fi
|
||||||
|
echo "──────────────────────────────────────────────────────────────────────"
|
||||||
|
}
|
||||||
|
|
||||||
|
on_exit() {
|
||||||
|
local code=$?
|
||||||
|
trap - EXIT
|
||||||
|
if [[ $code -ne 0 ]] && [[ "$ROLLBACK_STATE" == "armed" ]]; then
|
||||||
|
rollback
|
||||||
|
fi
|
||||||
|
exit $code
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Main
|
# Main
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -541,9 +756,15 @@ init_migration() {
|
|||||||
ENABLE_FLOW="no"
|
ENABLE_FLOW="no"
|
||||||
echo "Step 3 (traffic flow) skipped — requires Postgres."
|
echo "Step 3 (traffic flow) skipped — requires Postgres."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
check_data_directory
|
||||||
|
check_stale_postgres_volume
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_changes() {
|
apply_changes() {
|
||||||
|
# From here on a failure must roll the deployment back.
|
||||||
|
ROLLBACK_STATE="armed"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Writing $OVERRIDE_FILE ..."
|
echo "Writing $OVERRIDE_FILE ..."
|
||||||
install -m 644 /dev/null "$OVERRIDE_FILE"
|
install -m 644 /dev/null "$OVERRIDE_FILE"
|
||||||
@@ -564,6 +785,14 @@ apply_changes() {
|
|||||||
# picks it up automatically.
|
# picks it up automatically.
|
||||||
echo "Writing .env additions (mode 600) ..."
|
echo "Writing .env additions (mode 600) ..."
|
||||||
local ENV_FILE=".env"
|
local ENV_FILE=".env"
|
||||||
|
# Snapshot the operator's .env so a rollback can restore it byte for byte.
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
ENV_EXISTED="yes"
|
||||||
|
ENV_BACKUP="${ENV_FILE}.pre-enterprise-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
cp -p "$ENV_FILE" "$ENV_BACKUP"
|
||||||
|
else
|
||||||
|
ENV_EXISTED="no"
|
||||||
|
fi
|
||||||
touch "$ENV_FILE"
|
touch "$ENV_FILE"
|
||||||
chmod 600 "$ENV_FILE"
|
chmod 600 "$ENV_FILE"
|
||||||
{
|
{
|
||||||
@@ -592,11 +821,16 @@ apply_changes() {
|
|||||||
|
|
||||||
if [[ "$MIGRATE_POSTGRES" == "yes" ]]; then
|
if [[ "$MIGRATE_POSTGRES" == "yes" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "Stopping existing services (volumes preserved) ..."
|
# Stop, but keep the containers: the backup reads the store out of one.
|
||||||
$DOCKER_COMPOSE_COMMAND down
|
echo "Stopping services so the store is quiescent ..."
|
||||||
|
$DOCKER_COMPOSE_COMMAND stop
|
||||||
|
|
||||||
backup_sqlite
|
backup_sqlite
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Removing stopped containers (volumes preserved) ..."
|
||||||
|
$DOCKER_COMPOSE_COMMAND down
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Starting Postgres ..."
|
echo "Starting Postgres ..."
|
||||||
$DOCKER_COMPOSE_COMMAND up -d postgres
|
$DOCKER_COMPOSE_COMMAND up -d postgres
|
||||||
@@ -626,6 +860,9 @@ apply_changes() {
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Migration complete."
|
echo "Migration complete."
|
||||||
|
|
||||||
|
# Nothing left to undo.
|
||||||
|
ROLLBACK_STATE="disarmed"
|
||||||
}
|
}
|
||||||
|
|
||||||
print_summary() {
|
print_summary() {
|
||||||
@@ -643,6 +880,7 @@ print_summary() {
|
|||||||
echo " $OVERRIDE_FILE"
|
echo " $OVERRIDE_FILE"
|
||||||
[[ "$MIGRATE_POSTGRES" == "yes" ]] && echo " $ENTERPRISE_CONFIG_FILE"
|
[[ "$MIGRATE_POSTGRES" == "yes" ]] && echo " $ENTERPRISE_CONFIG_FILE"
|
||||||
echo " .env (license key + secrets, mode 600)"
|
echo " .env (license key + secrets, mode 600)"
|
||||||
|
[[ "$ENV_EXISTED" == "yes" ]] && [[ -f "$ENV_BACKUP" ]] && echo " $ENV_BACKUP (.env as it was before this run)"
|
||||||
[[ "$MIGRATE_POSTGRES" == "yes" ]] && echo " backups/sqlite-pre-enterprise-*/ (SQLite backup)"
|
[[ "$MIGRATE_POSTGRES" == "yes" ]] && echo " backups/sqlite-pre-enterprise-*/ (SQLite backup)"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Tail logs:"
|
echo " Tail logs:"
|
||||||
@@ -651,19 +889,27 @@ print_summary() {
|
|||||||
echo "──────────────────────────────────────────────────────────────────────"
|
echo "──────────────────────────────────────────────────────────────────────"
|
||||||
echo " To revert"
|
echo " To revert"
|
||||||
echo "──────────────────────────────────────────────────────────────────────"
|
echo "──────────────────────────────────────────────────────────────────────"
|
||||||
echo " $DOCKER_COMPOSE_COMMAND down"
|
|
||||||
if [[ "$MIGRATE_POSTGRES" == "yes" ]]; then
|
if [[ "$MIGRATE_POSTGRES" == "yes" ]]; then
|
||||||
# Resolve project-prefixed volume names now (before override is removed).
|
# Resolve the project-prefixed volume name now, before the override is gone.
|
||||||
local pg_volume data_volume_actual
|
local pg_volume
|
||||||
pg_volume=$(resolve_data_volume "netbird_postgres")
|
pg_volume=$(resolve_compose_volume "netbird_postgres")
|
||||||
data_volume_actual=$(resolve_data_volume "$DATA_VOLUME")
|
echo " # Stop, but keep the containers so the store can be copied back in:"
|
||||||
echo " # Remove the Postgres volume FIRST, before deleting the override file:"
|
echo " $DOCKER_COMPOSE_COMMAND stop"
|
||||||
echo " docker volume rm $pg_volume"
|
|
||||||
echo " # Restore SQLite from the backup created during this run:"
|
echo " # Restore SQLite from the backup created during this run:"
|
||||||
echo " docker run --rm -v ${data_volume_actual}:/var/lib/netbird -v ${BACKUP_DIR}:/backup busybox sh -c 'cp -a /backup/. /var/lib/netbird/'"
|
echo " docker cp ${BACKUP_DIR}/. \$($DOCKER_COMPOSE_COMMAND ps -aq $COMBINED_SERVICE):/var/lib/netbird/"
|
||||||
|
echo " $DOCKER_COMPOSE_COMMAND down"
|
||||||
|
echo " docker volume rm $pg_volume"
|
||||||
|
else
|
||||||
|
echo " $DOCKER_COMPOSE_COMMAND down"
|
||||||
fi
|
fi
|
||||||
echo " rm -f $OVERRIDE_FILE $ENTERPRISE_CONFIG_FILE"
|
echo " rm -f $OVERRIDE_FILE $ENTERPRISE_CONFIG_FILE"
|
||||||
echo " # Remove migrate-to-enterprise.sh additions from .env (search for the timestamp marker)"
|
if [[ "$ENV_EXISTED" == "yes" ]] && [[ -f "$ENV_BACKUP" ]]; then
|
||||||
|
echo " mv $ENV_BACKUP .env # restores .env as it was before this run"
|
||||||
|
elif [[ "$ENV_EXISTED" == "no" ]]; then
|
||||||
|
echo " rm -f .env # created by this run"
|
||||||
|
else
|
||||||
|
echo " # Remove migrate-to-enterprise.sh additions from .env (search for the timestamp marker)"
|
||||||
|
fi
|
||||||
echo " $DOCKER_COMPOSE_COMMAND up -d"
|
echo " $DOCKER_COMPOSE_COMMAND up -d"
|
||||||
echo "──────────────────────────────────────────────────────────────────────"
|
echo "──────────────────────────────────────────────────────────────────────"
|
||||||
}
|
}
|
||||||
@@ -672,6 +918,10 @@ print_summary() {
|
|||||||
# Run
|
# Run
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
trap on_exit EXIT
|
||||||
|
# Turn signals into a normal exit so the EXIT trap can roll back.
|
||||||
|
trap 'exit 130' INT TERM
|
||||||
|
|
||||||
init_migration
|
init_migration
|
||||||
apply_changes
|
apply_changes
|
||||||
print_summary
|
print_summary
|
||||||
|
|||||||
Reference in New Issue
Block a user