Move the gateway to junior.gib: CI deploy, compose copy, renewal script, docs
This commit is contained in:
1 parent
27469dbc77
commit
f46c803ad4
7 files changed
+62
-47
No files matched your search
@@ -1,11 +1,11 @@
|
|||||||
name: Gateway
|
name: Gateway
|
||||||
|
|
||||||
# Every push touching the gateway: typecheck and tests. On main, also build the image, push it to
|
# Every push touching the gateway: typecheck and tests. On main, also build the image, push it to
|
||||||
# the Gitea registry tagged with the commit and `latest`, and ask server-gib to deploy it now.
|
# the Gitea registry tagged with the commit and `latest`, and ask junior.gib (the VPS) to deploy it
|
||||||
# Watchtower's nightly pull there is the fallback when the deploy trigger fails.
|
# now. Its nightly update-containers timer is the fallback when the deploy trigger fails.
|
||||||
#
|
#
|
||||||
# Uses the user-level secrets REGISTRY_USER and REGISTRY_PASSWORD, and the repo secret
|
# Uses the user-level secrets REGISTRY_USER and REGISTRY_PASSWORD, and the repo secret
|
||||||
# DEPLOY_SSH_KEY_HOME_B64, a base64 encoded key that server-gib only accepts for
|
# DEPLOY_SSH_KEY_VPS_B64, a base64 encoded key that junior.gib only accepts for
|
||||||
# ~/Server/ci-deploy.sh.
|
# ~/Server/ci-deploy.sh.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -79,10 +79,11 @@ jobs:
|
|||||||
docker push "$IMAGE:latest"
|
docker push "$IMAGE:latest"
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
# The secret is the private key base64 encoded on one line. The runner prints each step's
|
# 192.168.2.2 is junior's WireGuard address, reachable from every runner. The secret is the
|
||||||
|
# private key base64 encoded on one line. The runner prints each step's
|
||||||
# env, and Gitea only masks a secret it can match in full, which a multi-line key defeats.
|
# env, and Gitea only masks a secret it can match in full, which a multi-line key defeats.
|
||||||
env:
|
env:
|
||||||
DEPLOY_SSH_KEY_B64: ${{ secrets.DEPLOY_SSH_KEY_HOME_B64 }}
|
DEPLOY_SSH_KEY_B64: ${{ secrets.DEPLOY_SSH_KEY_VPS_B64 }}
|
||||||
run: |
|
run: |
|
||||||
install -m600 /dev/null "$HOME/.deploy_key"
|
install -m600 /dev/null "$HOME/.deploy_key"
|
||||||
printf '%s' "$DEPLOY_SSH_KEY_B64" | base64 -d > "$HOME/.deploy_key"
|
printf '%s' "$DEPLOY_SSH_KEY_B64" | base64 -d > "$HOME/.deploy_key"
|
||||||
@@ -90,6 +91,6 @@ jobs:
|
|||||||
-o BatchMode=yes \
|
-o BatchMode=yes \
|
||||||
-o StrictHostKeyChecking=accept-new \
|
-o StrictHostKeyChecking=accept-new \
|
||||||
-o ConnectTimeout=15 \
|
-o ConnectTimeout=15 \
|
||||||
[email protected].1.2 wristcode \
|
[email protected].2.2 wristcode \
|
||||||
|| echo "::warning::deploy trigger failed; Watchtower will deploy at 04:00 instead"
|
|| echo "::warning::deploy trigger failed; the nightly update timer will deploy instead"
|
||||||
rm -f "$HOME/.deploy_key"
|
rm -f "$HOME/.deploy_key"
|
||||||
@@ -25,23 +25,24 @@ Apple Watch --HTTPS--> gateway --WebSocket--> T3 Code server (one per enviro
|
|||||||
|
|
||||||
## Running the gateway
|
## Running the gateway
|
||||||
|
|
||||||
The gateway is one container with one SQLite file in `/data`. [`deploy/compose.yml`](deploy/compose.yml)
|
The gateway is one container with one SQLite file in `/data`. It runs under rootless podman on
|
||||||
is the stack on server-gib, and [`gateway/Containerfile`](gateway/Containerfile) builds the image.
|
junior.gib, the VPS. [`deploy/compose.yml`](deploy/compose.yml) is a copy of that stack, and
|
||||||
CI builds and deploys every push to `main` that touches `gateway/`.
|
[`gateway/Containerfile`](gateway/Containerfile) builds the image. CI builds and deploys every push
|
||||||
|
to `main` that touches `gateway/`.
|
||||||
|
|
||||||
Operators use the `wristcode` CLI inside the container:
|
Operators use the `wristcode` CLI inside the container, on junior.gib:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Connect a T3 Code server. Make the one-time credential on that server first:
|
# Connect a T3 Code server. Make the one-time credential on that server first:
|
||||||
t3 auth pairing create --scope orchestration:read --scope orchestration:operate --label "WristCode gateway"
|
t3 auth pairing create --scope orchestration:read --scope orchestration:operate --label "WristCode gateway"
|
||||||
docker exec -it wristcode-gateway wristcode host add https://t3mac.gbrown.org <credential> --label "Mac mini"
|
podman exec -it wristcode-gateway wristcode host add https://t3mac.gbrown.org <credential> --label "Mac mini"
|
||||||
|
|
||||||
# Pair a watch: enter the printed code on the watch within 5 minutes.
|
# Pair a watch: enter the printed code on the watch within 5 minutes.
|
||||||
docker exec -it wristcode-gateway wristcode pair
|
podman exec -it wristcode-gateway wristcode pair
|
||||||
|
|
||||||
docker exec -it wristcode-gateway wristcode host list
|
podman exec -it wristcode-gateway wristcode host list
|
||||||
docker exec -it wristcode-gateway wristcode device list
|
podman exec -it wristcode-gateway wristcode device list
|
||||||
docker exec -it wristcode-gateway wristcode device revoke <id or name>
|
podman exec -it wristcode-gateway wristcode device revoke <id or name>
|
||||||
```
|
```
|
||||||
|
|
||||||
The gateway exchanges the credential for a 30 day T3 session limited to reading and driving
|
The gateway exchanges the credential for a 30 day T3 session limited to reading and driving
|
||||||
|
|||||||
+20
-11
@@ -1,27 +1,36 @@
|
|||||||
# The gateway on server-gib, at ~/Server/Wristcode/compose.yml. Nginx Proxy Manager serves
|
# A copy of the stack on junior.gib (the VPS). The live file is server/vps/Wristcode/compose.yml in
|
||||||
# wristcode.gbrown.org and forwards to wristcode-gateway:8080 over the nginx-bridge network.
|
# gib/Panama, symlinked into ~/Server/Wristcode/, beside podman-wristcode.service.
|
||||||
# CI deploys new images through ~/Server/ci-deploy.sh. Watchtower's nightly pull is the fallback.
|
|
||||||
#
|
#
|
||||||
# .env beside this file (mode 600) holds the APNs settings:
|
# WristCode gateway: the backend for the WristCode Apple Watch app (git.gbrown.org/gib/wristcode).
|
||||||
# APNS_KEY_ID=... the key's ID from the Apple Developer portal
|
# It holds a WebSocket to every T3 Code server, serves the watch's HTTPS API, and sends APNs pushes.
|
||||||
|
# NPM serves wristcode.gbrown.org and forwards to wristcode-gateway:8080 over nginx-bridge.
|
||||||
|
#
|
||||||
|
# .env beside this file (mode 600):
|
||||||
|
# APNS_KEY_ID=... the APNs key's ID
|
||||||
# APNS_TEAM_ID=GQJNM5YK2M
|
# APNS_TEAM_ID=GQJNM5YK2M
|
||||||
# APNS_KEY_PATH=/data/apns.p8 the .p8 file, placed in ./data
|
# APNS_KEY_PATH=/data/apns.p8 the .p8 file, in ./data
|
||||||
name: wristcode
|
#
|
||||||
|
# Operator CLI: podman exec -it wristcode-gateway wristcode <host add|pair|device list|...>
|
||||||
networks:
|
networks:
|
||||||
nginx-bridge:
|
nginx-bridge:
|
||||||
external: true
|
external: true # ALWAYS external; see AGENTS.md §5
|
||||||
|
|
||||||
services:
|
services:
|
||||||
wristcode-gateway:
|
wristcode-gateway:
|
||||||
image: git.gbrown.org/gib/wristcode-gateway:latest
|
image: git.gbrown.org/gib/wristcode-gateway:latest
|
||||||
container_name: wristcode-gateway
|
container_name: wristcode-gateway
|
||||||
hostname: wristcode-gateway
|
hostname: wristcode-gateway
|
||||||
|
domainname: wristcode.gbrown.org
|
||||||
networks: ['nginx-bridge']
|
networks: ['nginx-bridge']
|
||||||
env_file: [.env]
|
env_file: [.env]
|
||||||
environment:
|
environment:
|
||||||
- TZ=America/New_York
|
- TZ=America/New_York
|
||||||
|
# The image runs as `bun` (uid 1000). keep-id maps it to gib, so ./data stays gib-owned (AGENTS.md §3.2).
|
||||||
|
userns_mode: "keep-id:uid=1000,gid=1000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data:z # lowercase :z survives update-containers recreates, as in Agentchat
|
||||||
labels: ['com.centurylinklabs.watchtower.enable=true']
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: wget -qO- http://127.0.0.1:8080/v1/health || exit 1
|
||||||
|
interval: 30s
|
||||||
|
start_period: 10s
|
||||||
+17
-15
@@ -10,7 +10,7 @@ builds on it.
|
|||||||
## Shape
|
## Shape
|
||||||
|
|
||||||
```
|
```
|
||||||
watchOS app --HTTPS + device token--> gateway (Bun, server-gib) --WebSocket + bearer--> T3 server x N
|
watchOS app --HTTPS + device token--> gateway (Bun, junior.gib) --WebSocket + bearer--> T3 server x N
|
||||||
|
|
|
|
||||||
+--HTTP/2--> APNs --> watch notifications
|
+--HTTP/2--> APNs --> watch notifications
|
||||||
```
|
```
|
||||||
@@ -51,8 +51,8 @@ recommends. It needs no Effect and no vendored contracts.
|
|||||||
```sh
|
```sh
|
||||||
# on the T3 host
|
# on the T3 host
|
||||||
t3 auth pairing create --scope orchestration:read --scope orchestration:operate --label "WristCode gateway"
|
t3 auth pairing create --scope orchestration:read --scope orchestration:operate --label "WristCode gateway"
|
||||||
# on server-gib
|
# on junior.gib
|
||||||
docker exec -it wristcode-gateway wristcode host add https://t3mac.gbrown.org <pairing credential>
|
podman exec -it wristcode-gateway wristcode host add https://t3mac.gbrown.org <pairing credential>
|
||||||
```
|
```
|
||||||
|
|
||||||
The gateway exchanges the credential at `POST /oauth/token` for a 30 day bearer session and stores
|
The gateway exchanges the credential at `POST /oauth/token` for a 30 day bearer session and stores
|
||||||
@@ -124,15 +124,16 @@ to end check is a manual run against server-gib's own T3 before deploying.
|
|||||||
typecheck and tests on the `build` runner. On `main`, it also builds the image and pushes
|
typecheck and tests on the `build` runner. On `main`, it also builds the image and pushes
|
||||||
`git.gbrown.org/gib/wristcode-gateway:<sha>` and `:latest`. It needs the repo secrets
|
`git.gbrown.org/gib/wristcode-gateway:<sha>` and `:latest`. It needs the repo secrets
|
||||||
`REGISTRY_USER` and `REGISTRY_PASSWORD`.
|
`REGISTRY_USER` and `REGISTRY_PASSWORD`.
|
||||||
- On server-gib the stack lives in `~/Server/Wristcode/compose.yml`, modeled on `~/Server/Gbrown`.
|
- The stack runs under rootless podman on junior.gib, the VPS, beside Gitea and most other
|
||||||
It joins `nginx-bridge`, publishes no host ports, bind mounts `./data`, and carries the watchtower
|
services. Its compose file and systemd user unit live in gib/Panama under
|
||||||
label. The APNs key and other config come from a mode 600 `.env`.
|
`server/vps/Wristcode/` and are symlinked into `~/Server/Wristcode/` and
|
||||||
- Nginx Proxy Manager gets a proxy host for `wristcode.gbrown.org` that points at
|
`~/.config/systemd/user/`. It joins `nginx-bridge`, publishes no host ports, and bind mounts
|
||||||
`wristcode-gateway:8080`, using the `*.gbrown.org` certificate. DNS and the Cloudflare tunnel
|
`./data`. The APNs key and other config come from a mode 600 `.env`.
|
||||||
already cover the subdomain.
|
- junior's Nginx Proxy Manager serves `wristcode.gbrown.org` and forwards to
|
||||||
- Deploys: after pushing the image, CI calls `~/Server/ci-deploy.sh wristcode` on server-gib over
|
`wristcode-gateway:8080`. The Cloudflare record is proxied, as every VPS hostname must be.
|
||||||
SSH with a deploy key, which pulls and restarts the stack. Watchtower's nightly pull is the
|
- Deploys: after pushing the image, CI SSHes to junior's WireGuard address with a deploy key
|
||||||
fallback.
|
pinned to `~/Server/ci-deploy.sh`, which pulls and restarts the stack. The nightly
|
||||||
|
`update-containers` timer is the fallback.
|
||||||
- Local development: `bun --watch src/main.ts` against real T3 hosts, with no local image builds.
|
- Local development: `bun --watch src/main.ts` against real T3 hosts, with no local image builds.
|
||||||
|
|
||||||
## Watch app
|
## Watch app
|
||||||
@@ -181,7 +182,7 @@ Each phase ends with something usable.
|
|||||||
pairing, and `GET /v1/feed`. Checked against server-gib's T3 from the Fedora desktop.
|
pairing, and `GET /v1/feed`. Checked against server-gib's T3 from the Fedora desktop.
|
||||||
2. **Gateway write path and pairing.** Send, respond, stop, launch, the projects route, watch
|
2. **Gateway write path and pairing.** Send, respond, stop, launch, the projects route, watch
|
||||||
pairing codes, and device tokens. Checked with curl against a real thread.
|
pairing codes, and device tokens. Checked with curl against a real thread.
|
||||||
3. **Ship the gateway.** Containerfile, CI, registry, the compose stack on server-gib, and the NPM
|
3. **Ship the gateway.** Containerfile, CI, registry, the compose stack on junior.gib, and the NPM
|
||||||
proxy host. All four hosts are added.
|
proxy host. All four hosts are added.
|
||||||
4. **Watch app MVP.** Pair, feed, thread, reply, approvals, and new thread, running in the simulator
|
4. **Watch app MVP.** Pair, feed, thread, reply, approvals, and new thread, running in the simulator
|
||||||
against the deployed gateway.
|
against the deployed gateway.
|
||||||
@@ -198,8 +199,9 @@ they push to `docs/`.
|
|||||||
|
|
||||||
- Threads started from the watch inherit the host's default runtime mode (`full-access` on these
|
- Threads started from the watch inherit the host's default runtime mode (`full-access` on these
|
||||||
hosts).
|
hosts).
|
||||||
- CI deploys right away through a `wristcode` entry in `~/Server/ci-deploy.sh`, with Watchtower's
|
- The gateway runs on junior.gib, the VPS, which is more reliable than the home server. CI deploys
|
||||||
nightly pull as the fallback.
|
right away through junior's `~/Server/ci-deploy.sh`, with the nightly update timer as the
|
||||||
|
fallback.
|
||||||
- The App Store record uses `org.gbrown.wristcode`, an empty iOS wrapper the App Store requires
|
- The App Store record uses `org.gbrown.wristcode`, an empty iOS wrapper the App Store requires
|
||||||
around a watch-only app. The watch app itself is `org.gbrown.wristcode.watchkitapp`, which is also
|
around a watch-only app. The watch app itself is `org.gbrown.wristcode.watchkitapp`, which is also
|
||||||
its APNs topic.
|
its APNs topic.
|
||||||
@@ -9,7 +9,7 @@ WORKDIR /app
|
|||||||
COPY package.json tsconfig.json ./
|
COPY package.json tsconfig.json ./
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
|
|
||||||
# `wristcode <command>` for operators: docker exec -it wristcode-gateway wristcode pair
|
# `wristcode <command>` for operators: podman exec -it wristcode-gateway wristcode pair
|
||||||
RUN printf '#!/bin/sh\nexec bun /app/src/cli.ts "$@"\n' > /usr/local/bin/wristcode \
|
RUN printf '#!/bin/sh\nexec bun /app/src/cli.ts "$@"\n' > /usr/local/bin/wristcode \
|
||||||
&& chmod +x /usr/local/bin/wristcode \
|
&& chmod +x /usr/local/bin/wristcode \
|
||||||
&& mkdir -p /data && chown bun:bun /data
|
&& mkdir -p /data && chown bun:bun /data
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
// Operator commands. They edit the same SQLite file the running gateway reads, so changes apply
|
// Operator commands. They edit the same SQLite file the running gateway reads, so changes apply
|
||||||
// within a few seconds. In the container: `docker exec -it wristcode-gateway wristcode <command>`.
|
// within a few seconds. In the container: `podman exec -it wristcode-gateway wristcode <command>`.
|
||||||
|
|
||||||
import { loadConfig } from "./config.ts";
|
import { loadConfig } from "./config.ts";
|
||||||
import { openDb } from "./db.ts";
|
import { openDb } from "./db.ts";
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
# scripts/renew-hosts.sh "Mac mini" renew one host by label
|
# scripts/renew-hosts.sh "Mac mini" renew one host by label
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
GATEWAY_SSH="${GATEWAY_SSH:-server.gib}"
|
GATEWAY_SSH="${GATEWAY_SSH:-junior.gib}"
|
||||||
|
# The container runtime on the gateway host: podman on junior, docker on the home server.
|
||||||
|
GATEWAY_RUNTIME="${GATEWAY_RUNTIME:-podman}"
|
||||||
GATEWAY_CONTAINER="${GATEWAY_CONTAINER:-wristcode-gateway}"
|
GATEWAY_CONTAINER="${GATEWAY_CONTAINER:-wristcode-gateway}"
|
||||||
|
|
||||||
# label | SSH host, or "local" for the machine running this script | public T3 URL
|
# label | SSH host, or "local" for the machine running this script | public T3 URL
|
||||||
@@ -46,7 +48,7 @@ renew() {
|
|||||||
fi
|
fi
|
||||||
# The credential travels on stdin, not on a command line other processes could read.
|
# The credential travels on stdin, not on a command line other processes could read.
|
||||||
ssh -o BatchMode=yes "$GATEWAY_SSH" \
|
ssh -o BatchMode=yes "$GATEWAY_SSH" \
|
||||||
"read -r credential && docker exec $GATEWAY_CONTAINER wristcode host add '$url' \"\$credential\" --label '$label'" \
|
"read -r credential && $GATEWAY_RUNTIME exec $GATEWAY_CONTAINER wristcode host add '$url' \"\$credential\" --label '$label'" \
|
||||||
<<<"$credential"
|
<<<"$credential"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,5 +59,5 @@ for entry in "${HOSTS[@]}"; do
|
|||||||
renew "$label" "$ssh_host" "$url" || failed=1
|
renew "$label" "$ssh_host" "$url" || failed=1
|
||||||
done
|
done
|
||||||
|
|
||||||
ssh -o BatchMode=yes "$GATEWAY_SSH" "docker exec $GATEWAY_CONTAINER wristcode host list" | cut -f1,4
|
ssh -o BatchMode=yes "$GATEWAY_SSH" "$GATEWAY_RUNTIME exec $GATEWAY_CONTAINER wristcode host list" | cut -f1,4
|
||||||
exit "$failed"
|
exit "$failed"
|
||||||
Reference in new issue
Block a user