update installation
This commit is contained in:
+15
-11
@@ -1,6 +1,7 @@
|
||||
BROKER_DOMAIN=broker.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
|
||||
HTTPS_PORT=8643
|
||||
|
||||
@@ -9,25 +10,28 @@ BROKER_DIRECT_PORT=8080
|
||||
GITEA_DIRECT_PORT=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.
|
||||
# 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_FILE=fullchain.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_PUBLIC_URL=https://broker.example.com
|
||||
TAPM_GITEA_URL=https://git.example.com
|
||||
TAPM_GITEA_CLIENT_ID=replace-after-creating-the-gitea-oauth-app
|
||||
TAPM_GITEA_CLIENT_SECRET=replace-after-creating-the-gitea-oauth-app
|
||||
TAPM_PUBLIC_URL=http://broker.example.com:8680
|
||||
TAPM_GITEA_URL=http://git.example.com:8680
|
||||
# These values are intentionally blank during initial Gitea bootstrap.
|
||||
# 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_USERNAME=tapm-packages
|
||||
TAPM_GITEA_PACKAGE_TOKEN=replace-with-read-package-token
|
||||
TAPM_GITEA_PACKAGE_WRITE_USERNAME=tapm-publisher
|
||||
TAPM_GITEA_PACKAGE_WRITE_TOKEN=replace-with-write-package-token
|
||||
TAPM_GITEA_PACKAGE_USERNAME=
|
||||
TAPM_GITEA_PACKAGE_TOKEN=
|
||||
TAPM_GITEA_PACKAGE_WRITE_USERNAME=
|
||||
TAPM_GITEA_PACKAGE_WRITE_TOKEN=
|
||||
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_HOST_LIMIT=3
|
||||
TAPM_MAX_HOST_LIMIT=25
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
```sh
|
||||
GOCACHE=/tmp/tapm-go-cache go test ./...
|
||||
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 \
|
||||
-f compose.yaml -f compose.tls.yaml -f compose.direct.yaml config
|
||||
docker build -t tai/tapm-deployment-broker:local .
|
||||
|
||||
@@ -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
|
||||
@@ -1,5 +1,7 @@
|
||||
services:
|
||||
nginx:
|
||||
ports:
|
||||
- "${HTTPS_PORT:-8643}:443"
|
||||
volumes:
|
||||
- ./deploy/nginx/templates/tls.conf.template:/etc/nginx/templates/default.conf.template:ro
|
||||
- ${SSL_CERTIFICATE_DIR:-./config/ssl}:/etc/nginx/ssl:ro
|
||||
|
||||
+1
-2
@@ -51,7 +51,7 @@ services:
|
||||
GITEA__database__DB_TYPE: sqlite3
|
||||
GITEA__database__PATH: /var/lib/gitea/data/gitea.db
|
||||
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_PORT: "3000"
|
||||
GITEA__server__DISABLE_SSH: "true"
|
||||
@@ -78,7 +78,6 @@ services:
|
||||
- edge
|
||||
ports:
|
||||
- "${HTTP_PORT:-8680}:80"
|
||||
- "${HTTPS_PORT:-8643}:443"
|
||||
volumes:
|
||||
- ./deploy/nginx/templates/bootstrap.conf.template:/etc/nginx/templates/default.conf.template:ro
|
||||
- ./config/certbot-webroot:/var/www/certbot:ro
|
||||
|
||||
@@ -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
@@ -11,6 +11,11 @@ One Compose project manages:
|
||||
- Nginx, terminating TLS for the broker and Gitea
|
||||
- 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.
|
||||
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
|
||||
@@ -48,20 +53,29 @@ sudo mkdir -p /YOUR/INSTALL/PARENT
|
||||
sudo git clone GITEA-REPOSITORY-URL /YOUR/INSTALL/PARENT/TA-Deployment-Access
|
||||
sudo chown -R 1000:1000 /YOUR/INSTALL/PARENT/TA-Deployment-Access
|
||||
cd /YOUR/INSTALL/PARENT/TA-Deployment-Access
|
||||
cp .env.example .env
|
||||
chmod 600 .env
|
||||
./install.sh
|
||||
```
|
||||
|
||||
Set `BROKER_DOMAIN`, `GITEA_DOMAIN`, and `LETSENCRYPT_EMAIL` first. `HTTP_PORT`
|
||||
and `HTTPS_PORT` control the VM-side Docker bindings and default to 8680 and
|
||||
8643. The datacenter edge should therefore forward public ports 80 and 443 to
|
||||
VM ports 8680 and 8643. Replace all example domains in the TAPM variables.
|
||||
Generate the cookie secret with:
|
||||
The guided installer validates both hostnames and Docker ports, generates the
|
||||
broker cookie secret, writes `.env` with mode `0600`, prepares persistent
|
||||
storage, and starts only Gitea and Nginx. It defaults to `SSL_MODE=none`, so no
|
||||
certificate or public port forwarding is required.
|
||||
|
||||
With the default `HTTP_PORT=8680`, test URLs include that port:
|
||||
|
||||
```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:
|
||||
|
||||
```text
|
||||
@@ -78,15 +92,49 @@ config/
|
||||
|
||||
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
|
||||
./manage.sh bootstrap
|
||||
./manage.sh create-admin
|
||||
```
|
||||
|
||||
This prepares the runtime directories, creates the private Docker network,
|
||||
starts Gitea and the HTTP-only Nginx configuration, obtains a certificate when
|
||||
using Let's Encrypt, and switches Nginx to TLS.
|
||||
Sign in at the Gitea URL printed by the command and change the temporary
|
||||
password. Create the `TAI` organization, then create:
|
||||
|
||||
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
|
||||
key in `config/ssl/` and set:
|
||||
@@ -98,46 +146,27 @@ SSL_CERTIFICATE_FILE=fullchain.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;
|
||||
replace the files through the certificate provider's process and restart or
|
||||
reload Nginx. `SSL_CERTIFICATE_DIR` can point at another directory relative to
|
||||
the repository or at an absolute host path.
|
||||
|
||||
Create the initial Gitea administrator:
|
||||
|
||||
```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
|
||||
## 5. Start and inspect the complete deployment
|
||||
|
||||
```sh
|
||||
./manage.sh start
|
||||
./manage.sh status
|
||||
curl --fail "https://BROKER_DOMAIN/health/ready"
|
||||
curl --fail "https://GITEA_DOMAIN/api/healthz"
|
||||
curl --fail "http://BROKER_DOMAIN:HTTP_PORT/health/ready" # SSL_MODE=none
|
||||
curl --fail "http://GITEA_DOMAIN:HTTP_PORT/api/healthz" # SSL_MODE=none
|
||||
```
|
||||
|
||||
The broker automatically applies SQLite schema migrations before starting.
|
||||
Gitea uses its own SQLite database at
|
||||
`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
|
||||
`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
|
||||
./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
|
||||
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
|
||||
the transition.
|
||||
|
||||
## 6. Renewal and backups
|
||||
## 7. Renewal and backups
|
||||
|
||||
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
|
||||
datacenter loss. Also back up `.env` through a secrets-aware system.
|
||||
|
||||
## 7. Updates
|
||||
## 8. Updates
|
||||
|
||||
```sh
|
||||
cd /YOUR/INSTALL/PARENT/TA-Deployment-Access
|
||||
|
||||
Executable
+151
@@ -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
|
||||
@@ -39,7 +39,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
Path: "/auth",
|
||||
MaxAge: 600,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
Secure: s.cfg.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
@@ -78,7 +78,7 @@ func (s *Server) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
Path: "/auth",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
Secure: s.cfg.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
@@ -134,7 +134,7 @@ func (s *Server) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
Expires: expiresAt,
|
||||
MaxAge: int(time.Until(expiresAt).Seconds()),
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
Secure: s.cfg.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
_ = 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: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
Secure: s.cfg.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
|
||||
+19
-4
@@ -24,6 +24,7 @@ type Config struct {
|
||||
GiteaWriteToken string
|
||||
AllowedGiteaUsers map[string]struct{}
|
||||
CookieSecret []byte
|
||||
CookieSecure bool
|
||||
DefaultDuration time.Duration
|
||||
DefaultHostLimit int
|
||||
MaxHostLimit int
|
||||
@@ -50,14 +51,21 @@ func LoadConfig() (Config, error) {
|
||||
cfg.GiteaWriteToken = os.Getenv("TAPM_GITEA_PACKAGE_WRITE_TOKEN")
|
||||
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 {
|
||||
return cfg, err
|
||||
}
|
||||
cfg.GiteaURL, err = parseAbsoluteURL("TAPM_GITEA_URL")
|
||||
cfg.GiteaURL, err = parseAbsoluteURL("TAPM_GITEA_URL", allowInsecureHTTP)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
cfg.CookieSecure = cfg.PublicURL.Scheme == "https"
|
||||
|
||||
cfg.DefaultDuration, err = time.ParseDuration(
|
||||
envDefault("TAPM_DEFAULT_DURATION", "3h"),
|
||||
@@ -128,10 +136,17 @@ func LoadConfig() (Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func parseAbsoluteURL(name string) (*url.URL, error) {
|
||||
func parseAbsoluteURL(name string, allowInsecureHTTP bool) (*url.URL, error) {
|
||||
raw := os.Getenv(name)
|
||||
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 parsed, nil
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ set -Eeuo pipefail
|
||||
|
||||
DEPLOY_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
cd "$DEPLOY_ROOT"
|
||||
declare -a COMPOSE_FILES=(-f compose.yaml)
|
||||
|
||||
fail() {
|
||||
printf 'ERROR: %s\n' "$*" >&2
|
||||
@@ -26,10 +27,77 @@ read_env_setting() {
|
||||
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() {
|
||||
[[ -f .env ]] || fail "copy .env.example to .env and configure it first"
|
||||
BROKER_DOMAIN="$(read_env_setting BROKER_DOMAIN)"
|
||||
GITEA_DOMAIN="$(read_env_setting GITEA_DOMAIN)"
|
||||
GITEA_PUBLIC_URL="$(read_env_setting GITEA_PUBLIC_URL)"
|
||||
LETSENCRYPT_EMAIL="$(read_env_setting LETSENCRYPT_EMAIL)"
|
||||
SSL_MODE="$(read_env_setting SSL_MODE)"
|
||||
SSL_MODE="${SSL_MODE:-letsencrypt}"
|
||||
@@ -40,10 +108,35 @@ require_env() {
|
||||
SSL_CERTIFICATE_KEY_FILE="${SSL_CERTIFICATE_KEY_FILE:-privkey.pem}"
|
||||
: "${BROKER_DOMAIN:?BROKER_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
|
||||
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_EMAIL:?LETSENCRYPT_EMAIL is required for SSL_MODE=letsencrypt}"
|
||||
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)
|
||||
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}"
|
||||
[[ -f "${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
|
||||
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() {
|
||||
@@ -84,28 +184,35 @@ prepare)
|
||||
bootstrap)
|
||||
require_env
|
||||
prepare
|
||||
docker compose up -d gitea nginx
|
||||
if [[ "$SSL_MODE" == "letsencrypt" ]]; then
|
||||
if [[ "$SSL_MODE" == "none" ]]; then
|
||||
compose_stack up -d gitea nginx
|
||||
elif [[ "$SSL_MODE" == "letsencrypt" ]]; then
|
||||
docker compose up -d gitea nginx
|
||||
docker compose --profile tools run --rm certbot certonly \
|
||||
--webroot --webroot-path /var/www/certbot \
|
||||
--non-interactive --agree-tos \
|
||||
--email "$LETSENCRYPT_EMAIL" \
|
||||
--cert-name "$BROKER_DOMAIN" \
|
||||
-d "$BROKER_DOMAIN" -d "$GITEA_DOMAIN"
|
||||
compose_stack up -d gitea nginx
|
||||
else
|
||||
compose_stack up -d gitea nginx
|
||||
fi
|
||||
docker compose -f compose.yaml -f compose.tls.yaml up -d nginx
|
||||
;;
|
||||
start)
|
||||
require_env
|
||||
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)
|
||||
require_env
|
||||
prepare
|
||||
broker_config_ready ||
|
||||
fail "broker configuration is incomplete; run ./manage.sh configure-broker"
|
||||
docker compose \
|
||||
-f compose.yaml \
|
||||
-f compose.tls.yaml \
|
||||
"${COMPOSE_FILES[@]}" \
|
||||
-f compose.direct.yaml \
|
||||
up -d --build
|
||||
;;
|
||||
@@ -129,15 +236,101 @@ create-admin)
|
||||
printf '\nGitea administrator created.\n'
|
||||
printf 'Username: %s\n' "$admin_username"
|
||||
printf 'Temporary password: %s\n' "$temporary_password"
|
||||
printf 'Sign in at https://%s/ and change the password immediately.\n' \
|
||||
"$GITEA_DOMAIN"
|
||||
printf 'Sign in at %s and change the password immediately.\n' \
|
||||
"$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)
|
||||
require_env
|
||||
[[ "$SSL_MODE" == "letsencrypt" ]] ||
|
||||
fail "automatic renewal is only available with SSL_MODE=letsencrypt"
|
||||
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)
|
||||
require_env
|
||||
@@ -149,14 +342,23 @@ backup)
|
||||
docker compose stop gitea
|
||||
cp -a config/broker "$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"
|
||||
;;
|
||||
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
|
||||
;;
|
||||
esac
|
||||
|
||||
Executable
+85
@@ -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'
|
||||
@@ -57,7 +57,7 @@ printf 'Updating the local Gitea image and deployment stack...\n'
|
||||
docker compose --env-file .env pull gitea nginx
|
||||
"${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" ]] ||
|
||||
fail "Docker Compose did not return the local broker container"
|
||||
|
||||
@@ -71,13 +71,13 @@ for ((attempt = 1; attempt <= 75; attempt++)); do
|
||||
case "$status" in
|
||||
healthy)
|
||||
printf ' healthy.\n'
|
||||
docker compose -f compose.yaml -f compose.tls.yaml ps
|
||||
"${DEPLOY_ROOT}/manage.sh" status
|
||||
printf 'Local deployment updated successfully.\n'
|
||||
exit 0
|
||||
;;
|
||||
exited | dead | unhealthy)
|
||||
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"
|
||||
;;
|
||||
esac
|
||||
@@ -86,5 +86,5 @@ for ((attempt = 1; attempt <= 75; attempt++)); do
|
||||
done
|
||||
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user