update installation

This commit is contained in:
2026-07-28 17:30:17 -05:00
parent 81f65b5173
commit 941ef0691f
14 changed files with 740 additions and 82 deletions
+15 -11
View File
@@ -1,6 +1,7 @@
BROKER_DOMAIN=broker.example.com BROKER_DOMAIN=broker.example.com
GITEA_DOMAIN=git.example.com GITEA_DOMAIN=git.example.com
LETSENCRYPT_EMAIL=admin@example.com GITEA_PUBLIC_URL=http://git.example.com:8680/
LETSENCRYPT_EMAIL=
HTTP_PORT=8680 HTTP_PORT=8680
HTTPS_PORT=8643 HTTPS_PORT=8643
@@ -9,25 +10,28 @@ BROKER_DIRECT_PORT=8080
GITEA_DIRECT_PORT=3000 GITEA_DIRECT_PORT=3000
GITEA_DIRECT_ROOT_URL=http://localhost:3000/ GITEA_DIRECT_ROOT_URL=http://localhost:3000/
# none: HTTP-only testing before DNS and port forwarding are ready.
# letsencrypt: manage a certificate automatically with Certbot. # letsencrypt: manage a certificate automatically with Certbot.
# custom: read the named certificate files from ./config/ssl. # custom: read the named certificate files from ./config/ssl.
SSL_MODE=letsencrypt SSL_MODE=none
SSL_CERTIFICATE_DIR=./config/letsencrypt/live/broker.example.com SSL_CERTIFICATE_DIR=./config/letsencrypt/live/broker.example.com
SSL_CERTIFICATE_FILE=fullchain.pem SSL_CERTIFICATE_FILE=fullchain.pem
SSL_CERTIFICATE_KEY_FILE=privkey.pem SSL_CERTIFICATE_KEY_FILE=privkey.pem
TAPM_DATABASE_DSN=file:/data/tapm.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL) TAPM_DATABASE_DSN=file:/data/tapm.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)
TAPM_PUBLIC_URL=https://broker.example.com TAPM_PUBLIC_URL=http://broker.example.com:8680
TAPM_GITEA_URL=https://git.example.com TAPM_GITEA_URL=http://git.example.com:8680
TAPM_GITEA_CLIENT_ID=replace-after-creating-the-gitea-oauth-app # These values are intentionally blank during initial Gitea bootstrap.
TAPM_GITEA_CLIENT_SECRET=replace-after-creating-the-gitea-oauth-app # Run ./manage.sh configure-broker before starting the broker.
TAPM_GITEA_CLIENT_ID=
TAPM_GITEA_CLIENT_SECRET=
TAPM_GITEA_PACKAGE_OWNER=TAI TAPM_GITEA_PACKAGE_OWNER=TAI
TAPM_GITEA_PACKAGE_USERNAME=tapm-packages TAPM_GITEA_PACKAGE_USERNAME=
TAPM_GITEA_PACKAGE_TOKEN=replace-with-read-package-token TAPM_GITEA_PACKAGE_TOKEN=
TAPM_GITEA_PACKAGE_WRITE_USERNAME=tapm-publisher TAPM_GITEA_PACKAGE_WRITE_USERNAME=
TAPM_GITEA_PACKAGE_WRITE_TOKEN=replace-with-write-package-token TAPM_GITEA_PACKAGE_WRITE_TOKEN=
TAPM_ALLOWED_GITEA_USERS=taiadmin TAPM_ALLOWED_GITEA_USERS=taiadmin
TAPM_COOKIE_SECRET=replace-with-output-of-openssl-rand-base64-48 TAPM_COOKIE_SECRET=
TAPM_DEFAULT_DURATION=3h TAPM_DEFAULT_DURATION=3h
TAPM_DEFAULT_HOST_LIMIT=3 TAPM_DEFAULT_HOST_LIMIT=3
TAPM_MAX_HOST_LIMIT=25 TAPM_MAX_HOST_LIMIT=25
+25
View File
@@ -23,11 +23,36 @@ See [docs/deployment.md](docs/deployment.md) for the installation, Gitea setup,
TLS bootstrap, repository move, renewal, backup, and update procedures. The TLS bootstrap, repository move, renewal, backup, and update procedures. The
ProxMenu integration contract is in [docs/client-api.md](docs/client-api.md). ProxMenu integration contract is in [docs/client-api.md](docs/client-api.md).
## Guided installation
After cloning the repository, run:
```sh
./install.sh
```
The installer defaults to `SSL_MODE=none`, writes a mode-`0600` `.env`, creates
the runtime directories and Docker network, and starts Gitea behind the
HTTP-only Nginx test configuration. It does not invent Gitea OAuth or package
credentials. After creating those resources in Gitea, run
`./manage.sh configure-broker` and `./manage.sh start`.
Once public DNS and TCP 80/443 forwarding are ready, switch the same persistent
deployment to Let's Encrypt:
```sh
./manage.sh set-ssl letsencrypt
./manage.sh bootstrap
./manage.sh start
```
## Local checks ## Local checks
```sh ```sh
GOCACHE=/tmp/tapm-go-cache go test ./... GOCACHE=/tmp/tapm-go-cache go test ./...
docker compose --env-file .env.example config docker compose --env-file .env.example config
docker compose --env-file .env.example \
-f compose.yaml -f compose.http.yaml config
docker compose --env-file .env.example \ docker compose --env-file .env.example \
-f compose.yaml -f compose.tls.yaml -f compose.direct.yaml config -f compose.yaml -f compose.tls.yaml -f compose.direct.yaml config
docker build -t tai/tapm-deployment-broker:local . docker build -t tai/tapm-deployment-broker:local .
+14
View File
@@ -0,0 +1,14 @@
# Plain-HTTP testing mode. Do not expose this mode to the public Internet.
services:
broker:
environment:
TAPM_ALLOW_INSECURE_HTTP: "true"
gitea:
environment:
GITEA__server__ROOT_URL: ${GITEA_PUBLIC_URL}
GITEA__session__COOKIE_SECURE: "false"
nginx:
volumes:
- ./deploy/nginx/templates/http.conf.template:/etc/nginx/templates/default.conf.template:ro
+2
View File
@@ -1,5 +1,7 @@
services: services:
nginx: nginx:
ports:
- "${HTTPS_PORT:-8643}:443"
volumes: volumes:
- ./deploy/nginx/templates/tls.conf.template:/etc/nginx/templates/default.conf.template:ro - ./deploy/nginx/templates/tls.conf.template:/etc/nginx/templates/default.conf.template:ro
- ${SSL_CERTIFICATE_DIR:-./config/ssl}:/etc/nginx/ssl:ro - ${SSL_CERTIFICATE_DIR:-./config/ssl}:/etc/nginx/ssl:ro
+1 -2
View File
@@ -51,7 +51,7 @@ services:
GITEA__database__DB_TYPE: sqlite3 GITEA__database__DB_TYPE: sqlite3
GITEA__database__PATH: /var/lib/gitea/data/gitea.db GITEA__database__PATH: /var/lib/gitea/data/gitea.db
GITEA__server__DOMAIN: ${GITEA_DOMAIN} GITEA__server__DOMAIN: ${GITEA_DOMAIN}
GITEA__server__ROOT_URL: https://${GITEA_DOMAIN}/ GITEA__server__ROOT_URL: ${GITEA_PUBLIC_URL}
GITEA__server__HTTP_ADDR: 0.0.0.0 GITEA__server__HTTP_ADDR: 0.0.0.0
GITEA__server__HTTP_PORT: "3000" GITEA__server__HTTP_PORT: "3000"
GITEA__server__DISABLE_SSH: "true" GITEA__server__DISABLE_SSH: "true"
@@ -78,7 +78,6 @@ services:
- edge - edge
ports: ports:
- "${HTTP_PORT:-8680}:80" - "${HTTP_PORT:-8680}:80"
- "${HTTPS_PORT:-8643}:443"
volumes: volumes:
- ./deploy/nginx/templates/bootstrap.conf.template:/etc/nginx/templates/default.conf.template:ro - ./deploy/nginx/templates/bootstrap.conf.template:/etc/nginx/templates/default.conf.template:ro
- ./config/certbot-webroot:/var/www/certbot:ro - ./config/certbot-webroot:/var/www/certbot:ro
+83
View File
@@ -0,0 +1,83 @@
# Testing-only HTTP reverse proxy. Production should use tls.conf.template.
server_tokens off;
reset_timedout_connection on;
client_header_timeout 15s;
client_body_timeout 60s;
keepalive_timeout 30s;
send_timeout 60s;
limit_conn_zone $binary_remote_addr zone=per_address_connections:10m;
limit_req_zone $binary_remote_addr zone=broker_requests:10m rate=20r/s;
map $uri $gitea_login_client {
default "";
"/user/login" $binary_remote_addr;
}
limit_req_zone $gitea_login_client zone=gitea_login_requests:10m rate=10r/m;
resolver 127.0.0.11 valid=30s;
upstream gitea_backend {
zone gitea_backend 64k;
server gitea:3000 resolve;
}
server {
listen 80 default_server;
server_name _;
return 444;
}
server {
listen 80;
server_name ${BROKER_DOMAIN};
client_max_body_size 1100m;
limit_conn per_address_connections 30;
limit_req zone=broker_requests burst=40 nodelay;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
location / {
# Resolve at request time so Gitea-only bootstrap works before the
# broker container exists. Requests return 502 until broker setup ends.
set $broker_backend_url http://broker:8080;
proxy_pass $broker_backend_url;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto http;
proxy_connect_timeout 30s;
proxy_send_timeout 30m;
proxy_read_timeout 30m;
proxy_request_buffering off;
proxy_buffering off;
}
}
server {
listen 80;
server_name ${GITEA_DOMAIN};
client_max_body_size 1100m;
limit_conn per_address_connections 50;
limit_req zone=gitea_login_requests burst=10 nodelay;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
location / {
proxy_pass http://gitea_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto http;
proxy_read_timeout 300s;
}
}
+71 -43
View File
@@ -11,6 +11,11 @@ One Compose project manages:
- Nginx, terminating TLS for the broker and Gitea - Nginx, terminating TLS for the broker and Gitea
- Certbot, as an on-demand utility for certificate issuance and renewal - Certbot, as an on-demand utility for certificate issuance and renewal
The same deployment supports an explicitly insecure HTTP testing phase before
public DNS and port forwarding are ready. HTTP mode changes only routing, URL
schemes, and cookie security; it does not create separate databases or move
runtime data.
In production, only the configured Nginx HTTP and HTTPS ports are published. In production, only the configured Nginx HTTP and HTTPS ports are published.
They default to 8680 and 8643. Inside Docker, Nginx listens on ports 80 and 443, They default to 8680 and 8643. Inside Docker, Nginx listens on ports 80 and 443,
the broker on 8080, and Gitea is explicitly pinned to `0.0.0.0:3000`. Gitea the broker on 8080, and Gitea is explicitly pinned to `0.0.0.0:3000`. Gitea
@@ -48,20 +53,29 @@ sudo mkdir -p /YOUR/INSTALL/PARENT
sudo git clone GITEA-REPOSITORY-URL /YOUR/INSTALL/PARENT/TA-Deployment-Access sudo git clone GITEA-REPOSITORY-URL /YOUR/INSTALL/PARENT/TA-Deployment-Access
sudo chown -R 1000:1000 /YOUR/INSTALL/PARENT/TA-Deployment-Access sudo chown -R 1000:1000 /YOUR/INSTALL/PARENT/TA-Deployment-Access
cd /YOUR/INSTALL/PARENT/TA-Deployment-Access cd /YOUR/INSTALL/PARENT/TA-Deployment-Access
cp .env.example .env ./install.sh
chmod 600 .env
``` ```
Set `BROKER_DOMAIN`, `GITEA_DOMAIN`, and `LETSENCRYPT_EMAIL` first. `HTTP_PORT` The guided installer validates both hostnames and Docker ports, generates the
and `HTTPS_PORT` control the VM-side Docker bindings and default to 8680 and broker cookie secret, writes `.env` with mode `0600`, prepares persistent
8643. The datacenter edge should therefore forward public ports 80 and 443 to storage, and starts only Gitea and Nginx. It defaults to `SSL_MODE=none`, so no
VM ports 8680 and 8643. Replace all example domains in the TAPM variables. certificate or public port forwarding is required.
Generate the cookie secret with:
With the default `HTTP_PORT=8680`, test URLs include that port:
```sh ```sh
openssl rand -base64 48 http://BROKER_DOMAIN:8680
http://GITEA_DOMAIN:8680
``` ```
Both names must resolve to the Docker VM from the test workstation. Temporary
hosts-file records are sufficient. Do not forward this HTTP test listener to
the public Internet.
For a manual installation instead, copy `.env.example` to `.env`, replace its
example hostnames, generate `TAPM_COOKIE_SECRET` with
`openssl rand -base64 48`, and run `./manage.sh bootstrap`.
Runtime state is deliberately visible below the checkout: Runtime state is deliberately visible below the checkout:
```text ```text
@@ -78,15 +92,49 @@ config/
The contents are ignored by Git. They must never be committed. The contents are ignored by Git. They must never be committed.
## 3. Bootstrap Gitea and TLS ## 3. Configure Gitea and the broker
The installer has already bootstrapped Gitea. Create the initial administrator:
```sh ```sh
./manage.sh bootstrap ./manage.sh create-admin
``` ```
This prepares the runtime directories, creates the private Docker network, Sign in at the Gitea URL printed by the command and change the temporary
starts Gitea and the HTTP-only Nginx configuration, obtains a certificate when password. Create the `TAI` organization, then create:
using Let's Encrypt, and switches Nginx to TLS.
1. `tapm-packages`, with a read-only package token.
2. `tapm-publisher`, with a write package token.
3. An OAuth2 application. In HTTP testing mode its callback is
`http://BROKER_DOMAIN:HTTP_PORT/auth/callback`.
Save the resulting values without echoing secrets to the terminal:
```sh
./manage.sh configure-broker
./manage.sh start
```
The broker is deliberately not started during the first bootstrap. Blank
OAuth/package values are valid for Gitea-only setup, while `start` refuses to
launch the broker until all required credentials are present.
## 4. Enable TLS when forwarding is ready
After public DNS resolves and the datacenter edge forwards public TCP 80 and
443 to the configured VM ports, run:
```sh
./manage.sh set-ssl letsencrypt
./manage.sh bootstrap
./manage.sh start
```
`set-ssl` changes all public URLs to HTTPS and removes the temporary `:8680`
test port. `bootstrap` obtains the certificate and switches Nginx to TLS.
`start` recreates Gitea and the broker with secure cookies. Existing SQLite
databases, repositories, packages, installation identities, and secrets remain
in place.
For an administrator-provided certificate instead, place the certificate and For an administrator-provided certificate instead, place the certificate and
key in `config/ssl/` and set: key in `config/ssl/` and set:
@@ -98,46 +146,27 @@ SSL_CERTIFICATE_FILE=fullchain.pem
SSL_CERTIFICATE_KEY_FILE=privkey.pem SSL_CERTIFICATE_KEY_FILE=privkey.pem
``` ```
The certificate must cover both `BROKER_DOMAIN` and `GITEA_DOMAIN`. With custom The certificate must cover both `BROKER_DOMAIN` and `GITEA_DOMAIN`. Run
`./manage.sh set-ssl custom` before bootstrap. With custom
mode, `bootstrap` skips Certbot and `renew` is intentionally unavailable; mode, `bootstrap` skips Certbot and `renew` is intentionally unavailable;
replace the files through the certificate provider's process and restart or replace the files through the certificate provider's process and restart or
reload Nginx. `SSL_CERTIFICATE_DIR` can point at another directory relative to reload Nginx. `SSL_CERTIFICATE_DIR` can point at another directory relative to
the repository or at an absolute host path. the repository or at an absolute host path.
Create the initial Gitea administrator: ## 5. Start and inspect the complete deployment
```sh
./manage.sh create-admin
```
No Gitea login is hard-coded or stored in `.env`. The command prompts for the
administrator username and email, generates a temporary password, and requires
it to be changed at first login. The initial web installer is intentionally
locked and public registration is disabled.
Sign in to Gitea at `https://GITEA_DOMAIN`, create the `TAI` organization, and
create:
1. `tapm-packages`, with a read-only package token.
2. `tapm-publisher`, with a write package token.
3. An OAuth2 application whose callback is
`https://BROKER_DOMAIN/auth/callback`.
Place those token and OAuth values in `.env`. The broker cannot start until all
required credentials are present.
## 4. Start the complete deployment
```sh ```sh
./manage.sh start ./manage.sh start
./manage.sh status ./manage.sh status
curl --fail "https://BROKER_DOMAIN/health/ready" curl --fail "http://BROKER_DOMAIN:HTTP_PORT/health/ready" # SSL_MODE=none
curl --fail "https://GITEA_DOMAIN/api/healthz" curl --fail "http://GITEA_DOMAIN:HTTP_PORT/api/healthz" # SSL_MODE=none
``` ```
The broker automatically applies SQLite schema migrations before starting. The broker automatically applies SQLite schema migrations before starting.
Gitea uses its own SQLite database at Gitea uses its own SQLite database at
`config/gitea/data/data/gitea.db`. `config/gitea/data/data/gitea.db`.
After TLS is enabled, use the equivalent `https://` URLs without the temporary
HTTP test port.
For local testing that bypasses Nginx, set `BROKER_DIRECT_PORT` and For local testing that bypasses Nginx, set `BROKER_DIRECT_PORT` and
`GITEA_DIRECT_PORT` in `.env`. Set `GITEA_DIRECT_ROOT_URL` to the exact URL the `GITEA_DIRECT_PORT` in `.env`. Set `GITEA_DIRECT_ROOT_URL` to the exact URL the
@@ -164,10 +193,9 @@ production URL and secure-cookie policy with:
```sh ```sh
./manage.sh start ./manage.sh start
docker compose -f compose.yaml -f compose.tls.yaml up -d --force-recreate broker gitea
``` ```
## 5. Move repositories ## 6. Move repositories
For each existing repository, create an empty matching repository in the new For each existing repository, create an empty matching repository in the new
Gitea and mirror all refs: Gitea and mirror all refs:
@@ -183,7 +211,7 @@ module path only if the hostname embedded in the module path is also changing.
Move the deployment repository last so this checkout remains updateable during Move the deployment repository last so this checkout remains updateable during
the transition. the transition.
## 6. Renewal and backups ## 7. Renewal and backups
When `SSL_MODE=letsencrypt`, run renewal twice daily from root's crontab: When `SSL_MODE=letsencrypt`, run renewal twice daily from root's crontab:
@@ -201,7 +229,7 @@ The backup briefly stops both SQLite writers. Copy `config/backups/` to storage
outside the VM. A backup left only on this VM does not protect against VM or outside the VM. A backup left only on this VM does not protect against VM or
datacenter loss. Also back up `.env` through a secrets-aware system. datacenter loss. Also back up `.env` through a secrets-aware system.
## 7. Updates ## 8. Updates
```sh ```sh
cd /YOUR/INSTALL/PARENT/TA-Deployment-Access cd /YOUR/INSTALL/PARENT/TA-Deployment-Access
Executable
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env bash
set -Eeuo pipefail
DEPLOY_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
cd "$DEPLOY_ROOT"
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
prompt_default() {
local prompt="$1"
local fallback="$2"
local value
printf '%s [%s]: ' "$prompt" "$fallback" >&2
read -r value
printf '%s' "${value:-$fallback}"
}
valid_hostname() {
[[ "$1" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$ &&
"$1" != *..* ]]
}
valid_port() {
[[ "$1" =~ ^[0-9]+$ ]] && (( 10#$1 >= 1 && 10#$1 <= 65535 ))
}
[[ $# -eq 0 ]] || fail "usage: ./install.sh"
[[ ! -e .env ]] ||
fail ".env already exists; use ./manage.sh instead of overwriting this deployment"
for command_name in docker openssl; do
command -v "$command_name" >/dev/null 2>&1 ||
fail "$command_name is required"
done
docker compose version >/dev/null 2>&1 ||
fail "Docker Compose v2 is required"
docker info >/dev/null 2>&1 ||
fail "the Docker daemon is unavailable or this user cannot access it"
printf '\nTAPM Deployment Access installer\n'
printf 'This creates the initial environment and starts Gitea behind Nginx.\n'
printf 'The broker starts only after its Gitea OAuth and package credentials are configured.\n\n'
broker_domain="$(prompt_default 'Broker hostname' 'tapm.example.com')"
gitea_domain="$(prompt_default 'Gitea hostname' 'git.example.com')"
valid_hostname "$broker_domain" || fail "invalid broker hostname"
valid_hostname "$gitea_domain" || fail "invalid Gitea hostname"
[[ "$broker_domain" != "$gitea_domain" ]] ||
fail "broker and Gitea hostnames must be different"
http_port="$(prompt_default 'VM port mapped to Nginx HTTP port 80' '8680')"
https_port="$(prompt_default 'VM port mapped to Nginx HTTPS port 443' '8643')"
valid_port "$http_port" || fail "invalid HTTP port"
valid_port "$https_port" || fail "invalid HTTPS port"
[[ "$http_port" != "$https_port" ]] || fail "HTTP and HTTPS ports must differ"
ssl_mode="$(prompt_default 'SSL mode (none, letsencrypt, or custom)' 'none')"
case "$ssl_mode" in
none)
public_scheme=http
public_port_suffix=":${http_port}"
letsencrypt_email=''
certificate_dir="./config/letsencrypt/live/${broker_domain}"
;;
letsencrypt)
public_scheme=https
public_port_suffix=''
printf 'Let'\''s Encrypt account email: '
read -r letsencrypt_email
[[ "$letsencrypt_email" == *@* ]] || fail "a valid email is required"
certificate_dir="./config/letsencrypt/live/${broker_domain}"
;;
custom)
public_scheme=https
public_port_suffix=''
letsencrypt_email=''
certificate_dir='./config/ssl'
;;
*)
fail "SSL mode must be none, letsencrypt, or custom"
;;
esac
cookie_secret="$(openssl rand -base64 48)"
umask 077
{
printf 'BROKER_DOMAIN=%s\n' "$broker_domain"
printf 'GITEA_DOMAIN=%s\n' "$gitea_domain"
printf 'GITEA_PUBLIC_URL=%s://%s%s/\n' "$public_scheme" "$gitea_domain" "$public_port_suffix"
printf 'LETSENCRYPT_EMAIL=%s\n' "$letsencrypt_email"
printf 'HTTP_PORT=%s\n' "$http_port"
printf 'HTTPS_PORT=%s\n\n' "$https_port"
printf 'BROKER_DIRECT_PORT=8080\n'
printf 'GITEA_DIRECT_PORT=3000\n'
printf 'GITEA_DIRECT_ROOT_URL=http://localhost:3000/\n\n'
printf 'SSL_MODE=%s\n' "$ssl_mode"
printf 'SSL_CERTIFICATE_DIR=%s\n' "$certificate_dir"
printf 'SSL_CERTIFICATE_FILE=fullchain.pem\n'
printf 'SSL_CERTIFICATE_KEY_FILE=privkey.pem\n\n'
printf 'TAPM_DATABASE_DSN=file:/data/tapm.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)\n'
printf 'TAPM_PUBLIC_URL=%s://%s%s\n' "$public_scheme" "$broker_domain" "$public_port_suffix"
printf 'TAPM_GITEA_URL=%s://%s%s\n' "$public_scheme" "$gitea_domain" "$public_port_suffix"
printf 'TAPM_GITEA_CLIENT_ID=\n'
printf 'TAPM_GITEA_CLIENT_SECRET=\n'
printf 'TAPM_GITEA_PACKAGE_OWNER=TAI\n'
printf 'TAPM_GITEA_PACKAGE_USERNAME=\n'
printf 'TAPM_GITEA_PACKAGE_TOKEN=\n'
printf 'TAPM_GITEA_PACKAGE_WRITE_USERNAME=\n'
printf 'TAPM_GITEA_PACKAGE_WRITE_TOKEN=\n'
printf 'TAPM_ALLOWED_GITEA_USERS=taiadmin\n'
printf 'TAPM_COOKIE_SECRET=%s\n' "$cookie_secret"
printf 'TAPM_DEFAULT_DURATION=3h\n'
printf 'TAPM_DEFAULT_HOST_LIMIT=3\n'
printf 'TAPM_MAX_HOST_LIMIT=25\n'
printf 'TAPM_MAX_UPLOAD_BYTES=1073741824\n'
printf 'TAPM_TRUST_PROXY_HEADERS=true\n'
printf 'TAPM_DISPLAY_TIME_ZONE=America/Chicago\n'
} >.env
chmod 600 .env
printf '\nInitial configuration saved to %s/.env.\n' "$DEPLOY_ROOT"
if [[ "$ssl_mode" == "custom" ]]; then
printf 'Place the certificate and key in config/ssl, then run ./manage.sh bootstrap.\n'
printf 'Gitea has not been started yet.\n'
exit 0
fi
"${DEPLOY_ROOT}/manage.sh" bootstrap
printf '\nInitial Gitea deployment is running.\n'
printf 'Gitea URL: %s://%s%s\n' "$public_scheme" "$gitea_domain" "$public_port_suffix"
if [[ "$ssl_mode" == "none" ]]; then
printf 'This is HTTP-only testing mode. Do not expose it publicly.\n'
printf 'Make both hostnames resolve to this VM or its test-forwarding address.\n'
fi
printf '\nNext steps:\n'
printf ' 1. Run ./manage.sh create-admin\n'
printf ' 2. Sign in to Gitea and create the organization, package users/tokens,\n'
printf ' and OAuth application documented in docs/deployment.md.\n'
printf ' 3. Run ./manage.sh configure-broker\n'
printf ' 4. Run ./manage.sh start\n'
if [[ "$ssl_mode" == "none" ]]; then
printf ' 5. Later, after DNS and public 80/443 forwarding are ready, run:\n'
printf ' ./manage.sh set-ssl letsencrypt\n'
printf ' ./manage.sh bootstrap\n'
printf ' ./manage.sh start\n'
fi
+4 -4
View File
@@ -39,7 +39,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
Path: "/auth", Path: "/auth",
MaxAge: 600, MaxAge: 600,
HttpOnly: true, HttpOnly: true,
Secure: true, Secure: s.cfg.CookieSecure,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
}) })
@@ -78,7 +78,7 @@ func (s *Server) handleCallback(w http.ResponseWriter, r *http.Request) {
Path: "/auth", Path: "/auth",
MaxAge: -1, MaxAge: -1,
HttpOnly: true, HttpOnly: true,
Secure: true, Secure: s.cfg.CookieSecure,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
}) })
@@ -134,7 +134,7 @@ func (s *Server) handleCallback(w http.ResponseWriter, r *http.Request) {
Expires: expiresAt, Expires: expiresAt,
MaxAge: int(time.Until(expiresAt).Seconds()), MaxAge: int(time.Until(expiresAt).Seconds()),
HttpOnly: true, HttpOnly: true,
Secure: true, Secure: s.cfg.CookieSecure,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
}) })
_ = s.audit(r.Context(), "login_succeeded", user.Login, nil, "", "", s.clientIP(r), "") _ = s.audit(r.Context(), "login_succeeded", user.Login, nil, "", "", s.clientIP(r), "")
@@ -280,7 +280,7 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
Path: "/", Path: "/",
MaxAge: -1, MaxAge: -1,
HttpOnly: true, HttpOnly: true,
Secure: true, Secure: s.cfg.CookieSecure,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
}) })
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
+19 -4
View File
@@ -24,6 +24,7 @@ type Config struct {
GiteaWriteToken string GiteaWriteToken string
AllowedGiteaUsers map[string]struct{} AllowedGiteaUsers map[string]struct{}
CookieSecret []byte CookieSecret []byte
CookieSecure bool
DefaultDuration time.Duration DefaultDuration time.Duration
DefaultHostLimit int DefaultHostLimit int
MaxHostLimit int MaxHostLimit int
@@ -50,14 +51,21 @@ func LoadConfig() (Config, error) {
cfg.GiteaWriteToken = os.Getenv("TAPM_GITEA_PACKAGE_WRITE_TOKEN") cfg.GiteaWriteToken = os.Getenv("TAPM_GITEA_PACKAGE_WRITE_TOKEN")
cfg.CookieSecret = []byte(os.Getenv("TAPM_COOKIE_SECRET")) cfg.CookieSecret = []byte(os.Getenv("TAPM_COOKIE_SECRET"))
cfg.PublicURL, err = parseAbsoluteURL("TAPM_PUBLIC_URL") allowInsecureHTTP, err := strconv.ParseBool(
envDefault("TAPM_ALLOW_INSECURE_HTTP", "false"),
)
if err != nil {
return cfg, fmt.Errorf("TAPM_ALLOW_INSECURE_HTTP: %w", err)
}
cfg.PublicURL, err = parseAbsoluteURL("TAPM_PUBLIC_URL", allowInsecureHTTP)
if err != nil { if err != nil {
return cfg, err return cfg, err
} }
cfg.GiteaURL, err = parseAbsoluteURL("TAPM_GITEA_URL") cfg.GiteaURL, err = parseAbsoluteURL("TAPM_GITEA_URL", allowInsecureHTTP)
if err != nil { if err != nil {
return cfg, err return cfg, err
} }
cfg.CookieSecure = cfg.PublicURL.Scheme == "https"
cfg.DefaultDuration, err = time.ParseDuration( cfg.DefaultDuration, err = time.ParseDuration(
envDefault("TAPM_DEFAULT_DURATION", "3h"), envDefault("TAPM_DEFAULT_DURATION", "3h"),
@@ -128,10 +136,17 @@ func LoadConfig() (Config, error) {
return cfg, nil return cfg, nil
} }
func parseAbsoluteURL(name string) (*url.URL, error) { func parseAbsoluteURL(name string, allowInsecureHTTP bool) (*url.URL, error) {
raw := os.Getenv(name) raw := os.Getenv(name)
parsed, err := url.Parse(raw) parsed, err := url.Parse(raw)
if err != nil || parsed.Scheme != "https" || parsed.Host == "" { validScheme := parsed != nil && parsed.Scheme == "https"
if allowInsecureHTTP && parsed != nil && parsed.Scheme == "http" {
validScheme = true
}
if err != nil || !validScheme || parsed.Host == "" {
if allowInsecureHTTP {
return nil, fmt.Errorf("%s must be an absolute HTTP or HTTPS URL", name)
}
return nil, fmt.Errorf("%s must be an absolute HTTPS URL", name) return nil, fmt.Errorf("%s must be an absolute HTTPS URL", name)
} }
return parsed, nil return parsed, nil
+50
View File
@@ -0,0 +1,50 @@
package app
import (
"strings"
"testing"
)
func setRequiredConfigEnvironment(t *testing.T, scheme string) {
t.Helper()
t.Setenv("TAPM_PUBLIC_URL", scheme+"://broker.example.com")
t.Setenv("TAPM_GITEA_URL", scheme+"://git.example.com")
t.Setenv("TAPM_GITEA_CLIENT_ID", "client-id")
t.Setenv("TAPM_GITEA_CLIENT_SECRET", "client-secret")
t.Setenv("TAPM_GITEA_PACKAGE_USERNAME", "reader")
t.Setenv("TAPM_GITEA_PACKAGE_TOKEN", "reader-token")
t.Setenv("TAPM_GITEA_PACKAGE_WRITE_USERNAME", "writer")
t.Setenv("TAPM_GITEA_PACKAGE_WRITE_TOKEN", "writer-token")
t.Setenv("TAPM_COOKIE_SECRET", "0123456789abcdef0123456789abcdef")
}
func TestConfigRejectsHTTPByDefault(t *testing.T) {
setRequiredConfigEnvironment(t, "http")
_, err := LoadConfig()
if err == nil || !strings.Contains(err.Error(), "absolute HTTPS URL") {
t.Fatalf("LoadConfig() error = %v, want HTTPS requirement", err)
}
}
func TestConfigAllowsExplicitHTTPTestingMode(t *testing.T) {
setRequiredConfigEnvironment(t, "http")
t.Setenv("TAPM_ALLOW_INSECURE_HTTP", "true")
cfg, err := LoadConfig()
if err != nil {
t.Fatal(err)
}
if cfg.CookieSecure {
t.Fatal("HTTP testing mode unexpectedly enabled secure cookies")
}
}
func TestHTTPSConfigUsesSecureCookies(t *testing.T) {
setRequiredConfigEnvironment(t, "https")
cfg, err := LoadConfig()
if err != nil {
t.Fatal(err)
}
if !cfg.CookieSecure {
t.Fatal("HTTPS mode did not enable secure cookies")
}
}
+215 -13
View File
@@ -4,6 +4,7 @@ set -Eeuo pipefail
DEPLOY_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" DEPLOY_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
cd "$DEPLOY_ROOT" cd "$DEPLOY_ROOT"
declare -a COMPOSE_FILES=(-f compose.yaml)
fail() { fail() {
printf 'ERROR: %s\n' "$*" >&2 printf 'ERROR: %s\n' "$*" >&2
@@ -26,10 +27,77 @@ read_env_setting() {
printf '%s' "$value" printf '%s' "$value"
} }
set_env_setting() {
local key="$1"
local value="$2"
local temporary_file
temporary_file="$(mktemp "${DEPLOY_ROOT}/.env.tmp.XXXXXX")" ||
fail "unable to create temporary environment file"
awk -v key="$key" -v value="$value" '
BEGIN { replaced = 0 }
$0 ~ "^[[:space:]]*" key "[[:space:]]*=" {
if (!replaced) {
print key "=" value
replaced = 1
}
next
}
{ print }
END {
if (!replaced) print key "=" value
}
' .env >"$temporary_file"
chmod 600 "$temporary_file"
mv -f "$temporary_file" .env
}
select_compose_stack() {
COMPOSE_FILES=(-f compose.yaml)
if [[ "$SSL_MODE" == "none" ]]; then
COMPOSE_FILES+=(-f compose.http.yaml)
else
COMPOSE_FILES+=(-f compose.tls.yaml)
fi
}
compose_stack() {
docker compose "${COMPOSE_FILES[@]}" "$@"
}
broker_config_ready() {
local key
local value
local -a required=(
TAPM_GITEA_CLIENT_ID
TAPM_GITEA_CLIENT_SECRET
TAPM_GITEA_PACKAGE_USERNAME
TAPM_GITEA_PACKAGE_TOKEN
TAPM_GITEA_PACKAGE_WRITE_USERNAME
TAPM_GITEA_PACKAGE_WRITE_TOKEN
TAPM_COOKIE_SECRET
)
for key in "${required[@]}"; do
value="$(read_env_setting "$key")"
if [[ -z "$value" || "$value" == replace-* ]]; then
printf 'Missing broker setting: %s\n' "$key" >&2
return 1
fi
done
value="$(read_env_setting TAPM_COOKIE_SECRET)"
if (( ${#value} < 32 )); then
printf 'TAPM_COOKIE_SECRET must contain at least 32 characters.\n' >&2
return 1
fi
return 0
}
require_env() { require_env() {
[[ -f .env ]] || fail "copy .env.example to .env and configure it first" [[ -f .env ]] || fail "copy .env.example to .env and configure it first"
BROKER_DOMAIN="$(read_env_setting BROKER_DOMAIN)" BROKER_DOMAIN="$(read_env_setting BROKER_DOMAIN)"
GITEA_DOMAIN="$(read_env_setting GITEA_DOMAIN)" GITEA_DOMAIN="$(read_env_setting GITEA_DOMAIN)"
GITEA_PUBLIC_URL="$(read_env_setting GITEA_PUBLIC_URL)"
LETSENCRYPT_EMAIL="$(read_env_setting LETSENCRYPT_EMAIL)" LETSENCRYPT_EMAIL="$(read_env_setting LETSENCRYPT_EMAIL)"
SSL_MODE="$(read_env_setting SSL_MODE)" SSL_MODE="$(read_env_setting SSL_MODE)"
SSL_MODE="${SSL_MODE:-letsencrypt}" SSL_MODE="${SSL_MODE:-letsencrypt}"
@@ -40,10 +108,35 @@ require_env() {
SSL_CERTIFICATE_KEY_FILE="${SSL_CERTIFICATE_KEY_FILE:-privkey.pem}" SSL_CERTIFICATE_KEY_FILE="${SSL_CERTIFICATE_KEY_FILE:-privkey.pem}"
: "${BROKER_DOMAIN:?BROKER_DOMAIN is required}" : "${BROKER_DOMAIN:?BROKER_DOMAIN is required}"
: "${GITEA_DOMAIN:?GITEA_DOMAIN is required}" : "${GITEA_DOMAIN:?GITEA_DOMAIN is required}"
[[ "$BROKER_DOMAIN" != "$GITEA_DOMAIN" ]] ||
fail "BROKER_DOMAIN and GITEA_DOMAIN must be different hostnames"
if [[ -z "$GITEA_PUBLIC_URL" ]]; then
if [[ "$SSL_MODE" == "none" ]]; then
http_port="$(read_env_setting HTTP_PORT)"
http_port="${http_port:-8680}"
GITEA_PUBLIC_URL="http://${GITEA_DOMAIN}:${http_port}/"
else
GITEA_PUBLIC_URL="https://${GITEA_DOMAIN}/"
fi
fi
case "$SSL_MODE" in case "$SSL_MODE" in
none)
[[ "$(read_env_setting TAPM_PUBLIC_URL)" == http://* ]] ||
fail "TAPM_PUBLIC_URL must use http:// when SSL_MODE=none"
[[ "$(read_env_setting TAPM_GITEA_URL)" == http://* ]] ||
fail "TAPM_GITEA_URL must use http:// when SSL_MODE=none"
[[ "$GITEA_PUBLIC_URL" == http://* ]] ||
fail "GITEA_PUBLIC_URL must use http:// when SSL_MODE=none"
;;
letsencrypt) letsencrypt)
: "${LETSENCRYPT_EMAIL:?LETSENCRYPT_EMAIL is required for SSL_MODE=letsencrypt}" : "${LETSENCRYPT_EMAIL:?LETSENCRYPT_EMAIL is required for SSL_MODE=letsencrypt}"
SSL_CERTIFICATE_DIR="${SSL_CERTIFICATE_DIR:-./config/letsencrypt/live/${BROKER_DOMAIN}}" SSL_CERTIFICATE_DIR="${SSL_CERTIFICATE_DIR:-./config/letsencrypt/live/${BROKER_DOMAIN}}"
[[ "$(read_env_setting TAPM_PUBLIC_URL)" == https://* ]] ||
fail "TAPM_PUBLIC_URL must use https:// when TLS is enabled"
[[ "$(read_env_setting TAPM_GITEA_URL)" == https://* ]] ||
fail "TAPM_GITEA_URL must use https:// when TLS is enabled"
[[ "$GITEA_PUBLIC_URL" == https://* ]] ||
fail "GITEA_PUBLIC_URL must use https:// when TLS is enabled"
;; ;;
custom) custom)
SSL_CERTIFICATE_DIR="${SSL_CERTIFICATE_DIR:-./config/ssl}" SSL_CERTIFICATE_DIR="${SSL_CERTIFICATE_DIR:-./config/ssl}"
@@ -51,12 +144,19 @@ require_env() {
fail "custom certificate was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_FILE}" fail "custom certificate was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_FILE}"
[[ -f "${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}" ]] || [[ -f "${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}" ]] ||
fail "custom certificate key was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}" fail "custom certificate key was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}"
[[ "$(read_env_setting TAPM_PUBLIC_URL)" == https://* ]] ||
fail "TAPM_PUBLIC_URL must use https:// when TLS is enabled"
[[ "$(read_env_setting TAPM_GITEA_URL)" == https://* ]] ||
fail "TAPM_GITEA_URL must use https:// when TLS is enabled"
[[ "$GITEA_PUBLIC_URL" == https://* ]] ||
fail "GITEA_PUBLIC_URL must use https:// when TLS is enabled"
;; ;;
*) *)
fail "SSL_MODE must be letsencrypt or custom" fail "SSL_MODE must be none, letsencrypt, or custom"
;; ;;
esac esac
export SSL_CERTIFICATE_DIR SSL_CERTIFICATE_FILE SSL_CERTIFICATE_KEY_FILE export GITEA_PUBLIC_URL SSL_CERTIFICATE_DIR SSL_CERTIFICATE_FILE SSL_CERTIFICATE_KEY_FILE
select_compose_stack
} }
prepare() { prepare() {
@@ -84,28 +184,35 @@ prepare)
bootstrap) bootstrap)
require_env require_env
prepare prepare
if [[ "$SSL_MODE" == "none" ]]; then
compose_stack up -d gitea nginx
elif [[ "$SSL_MODE" == "letsencrypt" ]]; then
docker compose up -d gitea nginx docker compose up -d gitea nginx
if [[ "$SSL_MODE" == "letsencrypt" ]]; then
docker compose --profile tools run --rm certbot certonly \ docker compose --profile tools run --rm certbot certonly \
--webroot --webroot-path /var/www/certbot \ --webroot --webroot-path /var/www/certbot \
--non-interactive --agree-tos \ --non-interactive --agree-tos \
--email "$LETSENCRYPT_EMAIL" \ --email "$LETSENCRYPT_EMAIL" \
--cert-name "$BROKER_DOMAIN" \ --cert-name "$BROKER_DOMAIN" \
-d "$BROKER_DOMAIN" -d "$GITEA_DOMAIN" -d "$BROKER_DOMAIN" -d "$GITEA_DOMAIN"
compose_stack up -d gitea nginx
else
compose_stack up -d gitea nginx
fi fi
docker compose -f compose.yaml -f compose.tls.yaml up -d nginx
;; ;;
start) start)
require_env require_env
prepare prepare
docker compose -f compose.yaml -f compose.tls.yaml up -d --build broker_config_ready ||
fail "broker configuration is incomplete; run ./manage.sh configure-broker"
compose_stack up -d --build
;; ;;
start-direct) start-direct)
require_env require_env
prepare prepare
broker_config_ready ||
fail "broker configuration is incomplete; run ./manage.sh configure-broker"
docker compose \ docker compose \
-f compose.yaml \ "${COMPOSE_FILES[@]}" \
-f compose.tls.yaml \
-f compose.direct.yaml \ -f compose.direct.yaml \
up -d --build up -d --build
;; ;;
@@ -129,15 +236,101 @@ create-admin)
printf '\nGitea administrator created.\n' printf '\nGitea administrator created.\n'
printf 'Username: %s\n' "$admin_username" printf 'Username: %s\n' "$admin_username"
printf 'Temporary password: %s\n' "$temporary_password" printf 'Temporary password: %s\n' "$temporary_password"
printf 'Sign in at https://%s/ and change the password immediately.\n' \ printf 'Sign in at %s and change the password immediately.\n' \
"$GITEA_DOMAIN" "$GITEA_PUBLIC_URL"
;;
configure-broker)
require_env
command -v openssl >/dev/null 2>&1 ||
fail "openssl is required to generate the cookie secret"
printf 'Create the Gitea OAuth application with this callback URL:\n'
printf ' %s/auth/callback\n\n' "$(read_env_setting TAPM_PUBLIC_URL)"
printf 'Gitea OAuth client ID: '
read -r oauth_client_id
[[ -n "$oauth_client_id" ]] || fail "OAuth client ID is required"
printf 'Gitea OAuth client secret: '
read -r -s oauth_client_secret
printf '\n'
[[ -n "$oauth_client_secret" ]] || fail "OAuth client secret is required"
printf 'Read-only package username [tapm-packages]: '
read -r package_username
package_username="${package_username:-tapm-packages}"
printf 'Read-only package token: '
read -r -s package_token
printf '\n'
[[ -n "$package_token" ]] || fail "read-only package token is required"
printf 'Package publisher username [tapm-publisher]: '
read -r publisher_username
publisher_username="${publisher_username:-tapm-publisher}"
printf 'Package publisher token: '
read -r -s publisher_token
printf '\n'
[[ -n "$publisher_token" ]] || fail "publisher token is required"
printf 'Allowed Gitea users [taiadmin]: '
read -r allowed_users
allowed_users="${allowed_users:-taiadmin}"
cookie_secret="$(read_env_setting TAPM_COOKIE_SECRET)"
if (( ${#cookie_secret} < 32 )); then
cookie_secret="$(openssl rand -base64 48)"
fi
set_env_setting TAPM_GITEA_CLIENT_ID "$oauth_client_id"
set_env_setting TAPM_GITEA_CLIENT_SECRET "$oauth_client_secret"
set_env_setting TAPM_GITEA_PACKAGE_USERNAME "$package_username"
set_env_setting TAPM_GITEA_PACKAGE_TOKEN "$package_token"
set_env_setting TAPM_GITEA_PACKAGE_WRITE_USERNAME "$publisher_username"
set_env_setting TAPM_GITEA_PACKAGE_WRITE_TOKEN "$publisher_token"
set_env_setting TAPM_ALLOWED_GITEA_USERS "$allowed_users"
set_env_setting TAPM_COOKIE_SECRET "$cookie_secret"
printf 'Broker credentials saved to .env with mode 0600.\n'
printf 'Start the complete stack with ./manage.sh start\n'
;;
set-ssl)
require_env
target_mode="${2:-}"
case "$target_mode" in
none)
http_port="$(read_env_setting HTTP_PORT)"
http_port="${http_port:-8680}"
set_env_setting SSL_MODE none
set_env_setting TAPM_PUBLIC_URL "http://${BROKER_DOMAIN}:${http_port}"
set_env_setting TAPM_GITEA_URL "http://${GITEA_DOMAIN}:${http_port}"
set_env_setting GITEA_PUBLIC_URL "http://${GITEA_DOMAIN}:${http_port}/"
printf 'HTTP testing mode configured. Apply it with ./manage.sh start\n'
;;
letsencrypt)
email="$(read_env_setting LETSENCRYPT_EMAIL)"
if [[ -z "$email" ]]; then
printf 'Let'\''s Encrypt account email: '
read -r email
[[ -n "$email" ]] || fail "an email is required for Let's Encrypt"
set_env_setting LETSENCRYPT_EMAIL "$email"
fi
set_env_setting SSL_MODE letsencrypt
set_env_setting SSL_CERTIFICATE_DIR "./config/letsencrypt/live/${BROKER_DOMAIN}"
set_env_setting TAPM_PUBLIC_URL "https://${BROKER_DOMAIN}"
set_env_setting TAPM_GITEA_URL "https://${GITEA_DOMAIN}"
set_env_setting GITEA_PUBLIC_URL "https://${GITEA_DOMAIN}/"
printf 'TLS URLs configured. Obtain the certificate with ./manage.sh bootstrap\n'
;;
custom)
set_env_setting SSL_MODE custom
set_env_setting SSL_CERTIFICATE_DIR ./config/ssl
set_env_setting TAPM_PUBLIC_URL "https://${BROKER_DOMAIN}"
set_env_setting TAPM_GITEA_URL "https://${GITEA_DOMAIN}"
set_env_setting GITEA_PUBLIC_URL "https://${GITEA_DOMAIN}/"
printf 'Custom TLS mode configured. Place the certificate files, then run ./manage.sh bootstrap\n'
;;
*)
fail "usage: ./manage.sh set-ssl {none|letsencrypt|custom}"
;;
esac
;; ;;
renew) renew)
require_env require_env
[[ "$SSL_MODE" == "letsencrypt" ]] || [[ "$SSL_MODE" == "letsencrypt" ]] ||
fail "automatic renewal is only available with SSL_MODE=letsencrypt" fail "automatic renewal is only available with SSL_MODE=letsencrypt"
docker compose --profile tools run --rm certbot renew --quiet docker compose --profile tools run --rm certbot renew --quiet
docker compose -f compose.yaml -f compose.tls.yaml exec nginx nginx -s reload compose_stack exec nginx nginx -s reload
;; ;;
backup) backup)
require_env require_env
@@ -149,14 +342,23 @@ backup)
docker compose stop gitea docker compose stop gitea
cp -a config/broker "$destination/" cp -a config/broker "$destination/"
cp -a config/gitea "$destination/" cp -a config/gitea "$destination/"
docker compose -f compose.yaml -f compose.tls.yaml start gitea broker compose_stack start gitea broker
printf 'Backup created at %s\n' "$destination" printf 'Backup created at %s\n' "$destination"
;; ;;
status) status)
docker compose -f compose.yaml -f compose.tls.yaml ps require_env
compose_stack ps
;;
broker-container-id)
require_env
compose_stack ps -q broker
;;
broker-logs)
require_env
compose_stack logs --tail=100 broker
;; ;;
*) *)
printf 'Usage: %s {prepare|bootstrap|start|start-direct|create-admin|renew|backup|status}\n' "$0" printf 'Usage: %s {prepare|bootstrap|configure-broker|set-ssl|start|start-direct|create-admin|renew|backup|status}\n' "$0"
exit 2 exit 2
;; ;;
esac esac
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
set -Eeuo pipefail
TEST_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
test_dir="$(mktemp -d)"
trap 'rm -rf "$test_dir"' EXIT
deployment="${test_dir}/deployment"
mock_bin="${test_dir}/bin"
mkdir -p "$deployment" "$mock_bin"
cp -R \
"${TEST_ROOT}/install.sh" \
"${TEST_ROOT}/manage.sh" \
"${TEST_ROOT}/compose.yaml" \
"${TEST_ROOT}/compose.http.yaml" \
"${TEST_ROOT}/compose.tls.yaml" \
"${TEST_ROOT}/compose.direct.yaml" \
"${TEST_ROOT}/deploy" \
"$deployment/"
cat >"${mock_bin}/docker" <<'MOCK'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"$MOCK_DOCKER_LOG"
case "$*" in
"compose version" | "info" | "network inspect tapm-edge")
exit 0
;;
esac
exit 0
MOCK
chmod +x "${mock_bin}/docker"
export MOCK_DOCKER_LOG="${test_dir}/docker.log"
export PATH="${mock_bin}:${PATH}"
printf '%s\n' \
'broker.test' \
'git.test' \
'8780' \
'8743' \
'' |
"${deployment}/install.sh" >/dev/null
assert_env() {
local expected="$1"
grep -Fx "$expected" "${deployment}/.env" >/dev/null || {
printf 'FAIL: missing environment line: %s\n' "$expected" >&2
exit 1
}
}
assert_env 'SSL_MODE=none'
assert_env 'TAPM_PUBLIC_URL=http://broker.test:8780'
assert_env 'TAPM_GITEA_URL=http://git.test:8780'
assert_env 'GITEA_PUBLIC_URL=http://git.test:8780/'
grep -F -- '-f compose.yaml -f compose.http.yaml up -d gitea nginx' \
"$MOCK_DOCKER_LOG" >/dev/null || {
printf 'FAIL: HTTP bootstrap did not select compose.http.yaml\n' >&2
exit 1
}
if stat -c '%a' "${deployment}/.env" >/dev/null 2>&1; then
env_mode="$(stat -c '%a' "${deployment}/.env")"
else
env_mode="$(stat -f '%Lp' "${deployment}/.env")"
fi
[[ "$env_mode" == 600 ]] || {
printf 'FAIL: .env mode is %s, want 600\n' "$env_mode" >&2
exit 1
}
if "${deployment}/manage.sh" start >/dev/null 2>&1; then
printf 'FAIL: broker started without Gitea credentials\n' >&2
exit 1
fi
printf '%s\n' 'admin@example.test' |
"${deployment}/manage.sh" set-ssl letsencrypt >/dev/null
assert_env 'SSL_MODE=letsencrypt'
assert_env 'TAPM_PUBLIC_URL=https://broker.test'
assert_env 'GITEA_PUBLIC_URL=https://git.test/'
"${deployment}/manage.sh" set-ssl none >/dev/null
assert_env 'SSL_MODE=none'
assert_env 'TAPM_PUBLIC_URL=http://broker.test:8780'
printf 'PASS: guided HTTP deployment and TLS transition\n'
+4 -4
View File
@@ -57,7 +57,7 @@ printf 'Updating the local Gitea image and deployment stack...\n'
docker compose --env-file .env pull gitea nginx docker compose --env-file .env pull gitea nginx
"${DEPLOY_ROOT}/manage.sh" start "${DEPLOY_ROOT}/manage.sh" start
container_id="$(docker compose -f compose.yaml -f compose.tls.yaml ps -q broker)" container_id="$("${DEPLOY_ROOT}/manage.sh" broker-container-id)"
[[ -n "$container_id" ]] || [[ -n "$container_id" ]] ||
fail "Docker Compose did not return the local broker container" fail "Docker Compose did not return the local broker container"
@@ -71,13 +71,13 @@ for ((attempt = 1; attempt <= 75; attempt++)); do
case "$status" in case "$status" in
healthy) healthy)
printf ' healthy.\n' printf ' healthy.\n'
docker compose -f compose.yaml -f compose.tls.yaml ps "${DEPLOY_ROOT}/manage.sh" status
printf 'Local deployment updated successfully.\n' printf 'Local deployment updated successfully.\n'
exit 0 exit 0
;; ;;
exited | dead | unhealthy) exited | dead | unhealthy)
printf ' %s.\n' "$status" printf ' %s.\n' "$status"
docker compose -f compose.yaml -f compose.tls.yaml logs --tail=100 broker "${DEPLOY_ROOT}/manage.sh" broker-logs
fail "the local broker failed its health check" fail "the local broker failed its health check"
;; ;;
esac esac
@@ -86,5 +86,5 @@ for ((attempt = 1; attempt <= 75; attempt++)); do
done done
printf ' timed out.\n' printf ' timed out.\n'
docker compose -f compose.yaml -f compose.tls.yaml logs --tail=100 broker "${DEPLOY_ROOT}/manage.sh" broker-logs
fail "the local broker did not become healthy within 150 seconds" fail "the local broker did not become healthy within 150 seconds"