deploy: remove the CI pipeline; deploys are manual
No runner was ever active, so the pipeline never ran. Removes .gitea/workflows/deploy.yml and the CI key and runner IP from host/bootstrap.sh (a re-run removes the key an earlier bootstrap installed). The README makes "Manual deploy" the one documented procedure: commit on main, shellcheck in Docker (-S warning; the pipeline's lint step), git archive over root SSH, deploy.sh as user deploy, the outside checks, then push. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bf96edc828
commit
bc27bf2356
5 files changed
+71
-141
No files matched your search
@@ -1,57 +0,0 @@
|
|||||||
# Every push to main deploys everything: Traefik first, then each stack under
|
|
||||||
# stacks/. The runner only uploads; the server validates, deploys and
|
|
||||||
# smoke-tests (deploy/deploy.sh), so this job needs no Docker of its own.
|
|
||||||
name: deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# Serializes CI runs; deploy.sh's host lock also covers manual rollbacks.
|
|
||||||
concurrency:
|
|
||||||
group: deploy-petrolyst
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 45
|
|
||||||
env:
|
|
||||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Lint shell scripts
|
|
||||||
run: |
|
|
||||||
if ! command -v shellcheck >/dev/null; then
|
|
||||||
SUDO=$(command -v sudo || true)
|
|
||||||
$SUDO apt-get update -qq && $SUDO apt-get install -y -qq shellcheck
|
|
||||||
fi
|
|
||||||
shellcheck -x host/*.sh deploy/*.sh stacks/*/superset/init.sh stacks/*/fuel-db/init/*.sh
|
|
||||||
|
|
||||||
- name: Install the deploy key
|
|
||||||
env:
|
|
||||||
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
||||||
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
|
|
||||||
run: |
|
|
||||||
install -d -m 0700 "$HOME/.ssh"
|
|
||||||
printf '%s\n' "$DEPLOY_SSH_KEY" > "$HOME/.ssh/deploy_key"
|
|
||||||
chmod 0600 "$HOME/.ssh/deploy_key"
|
|
||||||
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
|
|
||||||
command -v rsync >/dev/null || { SUDO=$(command -v sudo || true); $SUDO apt-get install -y -qq rsync; }
|
|
||||||
|
|
||||||
- name: Upload and deploy
|
|
||||||
run: |
|
|
||||||
ssh_cmd="ssh -i $HOME/.ssh/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts"
|
|
||||||
# A fresh directory per run; only deploy.sh, under the host lock,
|
|
||||||
# turns it into releases/<sha>. --omit-dir-times: the upload's age
|
|
||||||
# must be the server's, because retention prunes uploads by mtime.
|
|
||||||
incoming="/opt/petrolyst/incoming/${GITHUB_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT:-1}-$(date -u +%s)"
|
|
||||||
rsync -az --omit-dir-times --exclude .git -e "$ssh_cmd" ./ "deploy@${DEPLOY_HOST}:${incoming}/"
|
|
||||||
$ssh_cmd "deploy@${DEPLOY_HOST}" "${incoming}/deploy/deploy.sh" "$GITHUB_SHA" "$incoming"
|
|
||||||
|
|
||||||
- name: Check from outside
|
|
||||||
run: |
|
|
||||||
test "$(curl -fsS --max-time 20 https://atoma.petrolyst.com/health)" = OK
|
|
||||||
curl -fsS --max-time 20 -o /dev/null -w '%{http_code}\n' https://traefik.petrolyst.com/ | grep -qx 302
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
# petrolyst-infra
|
# petrolyst-infra
|
||||||
|
|
||||||
Everything that runs on the petrolyst server (109.199.127.84, Ubuntu 24.04), deployed on every
|
Everything that runs on the petrolyst server (109.199.127.84, Ubuntu 24.04):
|
||||||
push to `main`:
|
|
||||||
|
|
||||||
- **Traefik** at the edge for every project on the server. Its dashboard is at
|
- **Traefik** at the edge for every project on the server. Its dashboard is at
|
||||||
https://traefik.petrolyst.com, behind Keycloak.
|
https://traefik.petrolyst.com, behind Keycloak.
|
||||||
@@ -9,6 +8,9 @@ push to `main`:
|
|||||||
store `fuel-db` (Postgres 17 with TimescaleDB) through Trino. Sign-in goes through Keycloak
|
store `fuel-db` (Postgres 17 with TimescaleDB) through Trino. Sign-in goes through Keycloak
|
||||||
client `petrolyst`, and the UI carries the Petrolyst brand.
|
client `petrolyst`, and the UI carries the Petrolyst brand.
|
||||||
|
|
||||||
|
**Deploys are manual.** A person deploys a clean commit of `main` from the Mac ("Manual deploy"
|
||||||
|
under Operating). There is no CI runner, and a push to Gitea deploys nothing.
|
||||||
|
|
||||||
Customization and deployment are separate, as with media-insights and media-intelligence-docker:
|
Customization and deployment are separate, as with media-insights and media-intelligence-docker:
|
||||||
|
|
||||||
| Folder | Holds |
|
| Folder | Holds |
|
||||||
@@ -16,19 +18,19 @@ Customization and deployment are separate, as with media-insights and media-inte
|
|||||||
| `petrolyst-insights/` | how Superset looks and behaves for Petrolyst: config, Keycloak login code, branding, patches, dashboards. See its README. |
|
| `petrolyst-insights/` | how Superset looks and behaves for Petrolyst: config, Keycloak login code, branding, patches, dashboards. See its README. |
|
||||||
| `traefik/` | edge proxy, TLS, the `sso` middleware (oauth2-proxy) that guards the dashboard |
|
| `traefik/` | edge proxy, TLS, the `sso` middleware (oauth2-proxy) that guards the dashboard |
|
||||||
| `stacks/fuel-monitoring/` | Compose stack, Superset Dockerfile, Trino config and catalog, fuel-db roles |
|
| `stacks/fuel-monitoring/` | Compose stack, Superset Dockerfile, Trino config and catalog, fuel-db roles |
|
||||||
| `deploy/` | `deploy.sh` (run by CI), `rollback.sh` (manual), `lib.sh` (shared) |
|
| `deploy/` | `deploy.sh` (run on the server by a manual deploy), `rollback.sh` (manual), `lib.sh` (shared) |
|
||||||
| `host/` | one-time server setup: `bootstrap.sh`, `secrets-init.sh` |
|
| `host/` | one-time server setup: `bootstrap.sh`, `secrets-init.sh` |
|
||||||
| `keycloak/` | the `petrolyst` client for Keycloak's partial import |
|
| `keycloak/` | the `petrolyst` client for Keycloak's partial import |
|
||||||
| `.gitea/workflows/deploy.yml` | the pipeline |
|
|
||||||
|
|
||||||
No credential is in this repository. Secrets exist only on the server, under
|
No credential is in this repository. Secrets exist only on the server, under
|
||||||
`/opt/petrolyst/secrets/`; CI holds only the SSH deploy key.
|
`/opt/petrolyst/secrets/`. A deploy needs only root SSH access to the server (`ssh petrolyst`).
|
||||||
|
|
||||||
## How a deploy works
|
## How a deploy works
|
||||||
|
|
||||||
1. A push to `main` (or a manual run) starts `.gitea/workflows/deploy.yml` on the act_runner. It
|
1. A person runs "Manual deploy" (Operating, below) from a clean checkout of `main`: the scripts
|
||||||
lints the scripts, rsyncs the checkout to a fresh `/opt/petrolyst/incoming/<sha>-<run>/`, and
|
are linted, `git archive` sends exactly that commit to a fresh
|
||||||
calls `deploy/deploy.sh <sha> <that dir>` over SSH as user `deploy`.
|
`/opt/petrolyst/incoming/<sha>-manual-<time>/`, and `deploy/deploy.sh <sha> <that dir>` runs
|
||||||
|
there as user `deploy` (root hands over with `sudo -u deploy`).
|
||||||
2. `deploy.sh` takes the host lock `/opt/petrolyst/deploy.lock`, which it shares with
|
2. `deploy.sh` takes the host lock `/opt/petrolyst/deploy.lock`, which it shares with
|
||||||
`rollback.sh`, and keeps it until it exits.
|
`rollback.sh`, and keeps it until it exits.
|
||||||
3. It finalizes the upload. A new commit is moved to `releases/<sha>`, and the release is made
|
3. It finalizes the upload. A new commit is moved to `releases/<sha>`, and the release is made
|
||||||
@@ -52,7 +54,7 @@ No credential is in this repository. Secrets exist only on the server, under
|
|||||||
`superset`, writes `backups/superset-meta_<old tag>_<revision>_<time>.dump`, then runs
|
`superset`, writes `backups/superset-meta_<old tag>_<revision>_<time>.dump`, then runs
|
||||||
`superset-init` (`db upgrade`, `init`, registers "Fuel (Trino)").
|
`superset-init` (`db upgrade`, `init`, registers "Fuel (Trino)").
|
||||||
- If the image is unchanged and the schema is at its head, it does none of this.
|
- If the image is unchanged and the schema is at its head, it does none of this.
|
||||||
8. Smoke tests. A failure turns CI red and prints the stacks' state.
|
8. Smoke tests. A failure ends the deploy with an error and prints the stacks' state.
|
||||||
- `/health` answers through Traefik with a valid certificate.
|
- `/health` answers through Traefik with a valid certificate.
|
||||||
- `/login/` leads to Keycloak with `client_id=petrolyst` and PKCE.
|
- `/login/` leads to Keycloak with `client_id=petrolyst` and PKCE.
|
||||||
- The dashboard redirects to Keycloak.
|
- The dashboard redirects to Keycloak.
|
||||||
@@ -63,14 +65,14 @@ No credential is in this repository. Secrets exist only on the server, under
|
|||||||
- the 3 newest Superset images, plus any image that a kept release or backup names;
|
- the 3 newest Superset images, plus any image that a kept release or backup names;
|
||||||
- uploads older than a day.
|
- uploads older than a day.
|
||||||
|
|
||||||
The runner then checks both hosts from outside.
|
The manual deploy then checks both hosts from outside.
|
||||||
|
|
||||||
## Server paths
|
## Server paths
|
||||||
|
|
||||||
| Path | Owner | What |
|
| Path | Owner | What |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/opt/petrolyst/infra` | deploy | live tree (the promoted release) |
|
| `/opt/petrolyst/infra` | deploy | live tree (the promoted release) |
|
||||||
| `/opt/petrolyst/incoming/<sha>-<run>` | deploy | one CI upload; only `deploy.sh` turns it into a release |
|
| `/opt/petrolyst/incoming/<sha>-manual-<time>` | deploy | one upload; only `deploy.sh` turns it into a release |
|
||||||
| `/opt/petrolyst/releases/<sha>` | deploy, read-only | finalized release plus `ARTIFACTS`; the last 5 are kept |
|
| `/opt/petrolyst/releases/<sha>` | deploy, read-only | finalized release plus `ARTIFACTS`; the last 5 are kept |
|
||||||
| `/opt/petrolyst/secrets/{shared,traefik,fuel-monitoring}/` | root:deploy 0750, files 0444 | secrets, mounted as `/run/secrets/<name>` |
|
| `/opt/petrolyst/secrets/{shared,traefik,fuel-monitoring}/` | root:deploy 0750, files 0444 | secrets, mounted as `/run/secrets/<name>` |
|
||||||
| `/opt/petrolyst/env/fuel-monitoring.env` | root:deploy 0440 | `FUEL_DB_TRINO_PASSWORD` for Trino's `${ENV:…}` |
|
| `/opt/petrolyst/env/fuel-monitoring.env` | root:deploy 0440 | `FUEL_DB_TRINO_PASSWORD` for Trino's `${ENV:…}` |
|
||||||
@@ -81,24 +83,9 @@ The runner then checks both hosts from outside.
|
|||||||
|
|
||||||
### 1. Gitea
|
### 1. Gitea
|
||||||
|
|
||||||
1. Create the private repository `Petrolyst/petrolyst-infra` and push this repository's
|
Create the private repository `Petrolyst/petrolyst-infra` and push this repository's `main` to
|
||||||
`main` to it. Enable Actions under Settings → Repository → Units.
|
it. Gitea only keeps the history: deploys are manual, so it needs no runner, no Actions and no
|
||||||
2. Runner. Check Site/Org Administration → Actions → Runners. If none is registered, run
|
deploy secrets.
|
||||||
[Gitea Runner](https://docs.gitea.com/runner/installation/docker/) as the container
|
|
||||||
`docker.io/gitea/runner:5.2.2-dind` (privileged, with `/data` and `/var/lib/docker` volumes),
|
|
||||||
for example on the Gitea host (164.68.121.117):
|
|
||||||
- register it as an **organization runner** for `Petrolyst`: take the token from
|
|
||||||
Settings → Actions → Runners → Create new Runner;
|
|
||||||
- set `GITEA_RUNNER_LABELS=ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest`;
|
|
||||||
- note its egress IPv4 (`curl -4 -s https://ifconfig.me`).
|
|
||||||
3. Generate a deploy key on your Mac, used only for this:
|
|
||||||
`ssh-keygen -t ed25519 -N '' -C petrolyst-ci -f petrolyst-ci`
|
|
||||||
4. Add repository secrets (Settings → Actions → Secrets):
|
|
||||||
- `DEPLOY_SSH_KEY`: contents of `petrolyst-ci` (the private key)
|
|
||||||
- `DEPLOY_HOST`: `109.199.127.84`
|
|
||||||
- `DEPLOY_KNOWN_HOSTS`: output of `ssh-keyscan -t ed25519 109.199.127.84`. Compare its
|
|
||||||
fingerprint (`ssh-keygen -lf <file>`) with `ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub`
|
|
||||||
run on the server.
|
|
||||||
|
|
||||||
### 2. Keycloak (realm `thedigitalbridge` on iam.thedigitalbridge.io)
|
### 2. Keycloak (realm `thedigitalbridge` on iam.thedigitalbridge.io)
|
||||||
|
|
||||||
@@ -132,24 +119,21 @@ The server needs only the two host scripts; it never gets Gitea credentials.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# on the Mac, in this repository
|
# on the Mac, in this repository
|
||||||
scp host/bootstrap.sh host/secrets-init.sh petrolyst-ci.pub petrolyst:/root/
|
scp host/bootstrap.sh host/secrets-init.sh petrolyst:/root/
|
||||||
ssh petrolyst
|
ssh petrolyst
|
||||||
# on the server, as root
|
# on the server, as root
|
||||||
/root/bootstrap.sh --ci-key /root/petrolyst-ci.pub --runner-ip <runner egress IP>
|
/root/bootstrap.sh
|
||||||
/root/secrets-init.sh # generates every secret; reports the missing Keycloak one
|
/root/secrets-init.sh # generates every secret; reports the missing Keycloak one
|
||||||
/root/secrets-init.sh keycloak-secret # paste the secret from Keycloak step 2
|
/root/secrets-init.sh keycloak-secret # paste the secret from Keycloak step 2
|
||||||
```
|
```
|
||||||
|
|
||||||
No runner yet? Pass `--runner-ip 127.0.0.1`. That installs the CI key but leaves it unusable from
|
|
||||||
outside. Deploy manually until a runner exists (see "Manual deploy" below). When the runner is
|
|
||||||
running, run `bootstrap.sh` again with its egress IP.
|
|
||||||
|
|
||||||
`bootstrap.sh` does the following:
|
`bootstrap.sh` does the following:
|
||||||
- installs Docker from Docker's apt repository;
|
- installs Docker from Docker's apt repository;
|
||||||
- adds 4 GB swap;
|
- adds 4 GB swap;
|
||||||
- turns on ufw for 22, 80 and 443 only;
|
- turns on ufw for 22, 80 and 443 only;
|
||||||
- turns off SSH password login and root password login;
|
- turns off SSH password login and root password login;
|
||||||
- creates the `deploy` user, whose key works only from the runner IP;
|
- creates the `deploy` user, which runs `deploy.sh`. It has no SSH key and is reached only
|
||||||
|
through `sudo -u deploy` from root; a re-run removes the CI key an earlier version installed;
|
||||||
- creates `/opt/petrolyst` and the Docker network `proxy`.
|
- creates `/opt/petrolyst` and the Docker network `proxy`.
|
||||||
|
|
||||||
**Before closing your root session**, check from a second terminal that `ssh petrolyst` still
|
**Before closing your root session**, check from a second terminal that `ssh petrolyst` still
|
||||||
@@ -157,8 +141,7 @@ works.
|
|||||||
|
|
||||||
### 4. First deploy
|
### 4. First deploy
|
||||||
|
|
||||||
Run the workflow manually (Actions → deploy → Run workflow) or push to `main`. Without a runner,
|
Use "Manual deploy" below. The first run builds the Superset image and takes the fresh-install
|
||||||
use "Manual deploy" below. The first run builds the Superset image and takes the fresh-install
|
|
||||||
path. Let's Encrypt needs about a minute
|
path. Let's Encrypt needs about a minute
|
||||||
for the two certificates; the smoke tests wait up to two minutes.
|
for the two certificates; the smoke tests wait up to two minutes.
|
||||||
|
|
||||||
@@ -169,7 +152,7 @@ becomes the first Superset admin; there is no local admin account.
|
|||||||
|
|
||||||
| Task | How |
|
| Task | How |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Deploy | push to `main` (without a runner: "Manual deploy" below) |
|
| Deploy | "Manual deploy" below, from a clean `main` |
|
||||||
| Re-deploy a kept release | `sudo -u deploy /opt/petrolyst/infra/deploy/deploy.sh <sha>` |
|
| Re-deploy a kept release | `sudo -u deploy /opt/petrolyst/infra/deploy/deploy.sh <sha>` |
|
||||||
| Roll back | see below |
|
| Roll back | see below |
|
||||||
| Traefik dashboard | https://traefik.petrolyst.com (role `traefik-admin`) |
|
| Traefik dashboard | https://traefik.petrolyst.com (role `traefik-admin`) |
|
||||||
@@ -178,21 +161,41 @@ becomes the first Superset admin; there is no local admin account.
|
|||||||
| Logs | `docker compose -p fuel-monitoring logs -f superset` (or `-p traefik`) |
|
| Logs | `docker compose -p fuel-monitoring logs -f superset` (or `-p traefik`) |
|
||||||
| Superset break-glass | `docker exec -it fuel-monitoring-superset-1 superset fab ...` |
|
| Superset break-glass | `docker exec -it fuel-monitoring-superset-1 superset fab ...` |
|
||||||
|
|
||||||
**Manual deploy** (no runner): commit first, so that `git status` is clean. Then run this on the
|
**Manual deploy**, the only way to deploy (there is no CI runner). On the Mac, in this
|
||||||
Mac, in this repository:
|
repository:
|
||||||
|
|
||||||
```bash
|
1. Commit on `main`, so that `git status` is clean. The live commit is in
|
||||||
sha=$(git rev-parse HEAD)
|
`/opt/petrolyst/state/deployed-sha` (`ssh petrolyst cat /opt/petrolyst/state/deployed-sha`).
|
||||||
dir=/opt/petrolyst/incoming/$sha-manual-$(date -u +%s)
|
2. Lint the shell scripts. Warnings and errors must be zero; the info notes on the deliberate
|
||||||
git archive --format=tar "$sha" |
|
`test && test || die` guards in `deploy/lib.sh` (SC2015) are filtered out:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:v0.10.0 -S warning -x \
|
||||||
|
host/*.sh deploy/*.sh stacks/*/superset/init.sh stacks/*/fuel-db/init/*.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sha=$(git rev-parse HEAD)
|
||||||
|
dir=/opt/petrolyst/incoming/$sha-manual-$(date -u +%s)
|
||||||
|
git archive --format=tar "$sha" |
|
||||||
ssh petrolyst "install -d -o deploy -g deploy -m 0755 $dir && tar -x --no-same-owner -C $dir && chown -R deploy:deploy $dir"
|
ssh petrolyst "install -d -o deploy -g deploy -m 0755 $dir && tar -x --no-same-owner -C $dir && chown -R deploy:deploy $dir"
|
||||||
ssh petrolyst "sudo -u deploy $dir/deploy/deploy.sh $sha $dir"
|
ssh petrolyst "sudo -u deploy $dir/deploy/deploy.sh $sha $dir"
|
||||||
```
|
```
|
||||||
|
|
||||||
`git archive` sends exactly the commit, never uncommitted or ignored files such as the deploy
|
4. Check both hosts from outside:
|
||||||
key. The server then runs the same `deploy.sh` that CI runs, under the same lock. A later push
|
|
||||||
queues a CI run that waits for a runner. Cancel stale waiting runs under Actions before you
|
```bash
|
||||||
register one.
|
test "$(curl -fsS --max-time 20 https://atoma.petrolyst.com/health)" = OK && echo atoma ok
|
||||||
|
curl -fsS --max-time 20 -o /dev/null -w '%{http_code}\n' https://traefik.petrolyst.com/ # 302
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Push `main` to Gitea, so that the repository matches what is live.
|
||||||
|
|
||||||
|
`git archive` sends exactly the commit, never uncommitted or ignored files. The server runs
|
||||||
|
`deploy.sh` under the host lock it shares with `rollback.sh`, and records the live commit in
|
||||||
|
`/opt/petrolyst/state/deployed-sha`.
|
||||||
|
|
||||||
**Rollback** (`deploy/rollback.sh`, manual, under the same lock as deploys):
|
**Rollback** (`deploy/rollback.sh`, manual, under the same lock as deploys):
|
||||||
|
|
||||||
@@ -291,8 +294,7 @@ Results:
|
|||||||
Not verified locally:
|
Not verified locally:
|
||||||
- Let's Encrypt issuance;
|
- Let's Encrypt issuance;
|
||||||
- a real Keycloak sign-in, OTP and logout (they need the client from setup step 2);
|
- a real Keycloak sign-in, OTP and logout (they need the client from setup step 2);
|
||||||
- the theme as rendered in a browser;
|
- the theme as rendered in a browser.
|
||||||
- the Gitea runner.
|
|
||||||
|
|
||||||
## Open items
|
## Open items
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Server-side deploy, run as the non-root deploy user:
|
# Server-side deploy, run as the non-root deploy user:
|
||||||
# deploy.sh <sha> <incoming-dir> CI: finalize the upload as releases/<sha>, then deploy it
|
# deploy.sh <sha> <incoming-dir> manual deploy: finalize the upload as releases/<sha>, then deploy it
|
||||||
# deploy.sh <sha> re-deploy a release that is already finalized
|
# deploy.sh <sha> re-deploy a release that is already finalized
|
||||||
#
|
#
|
||||||
# Order: lock, finalize, artifacts, preflight, image build and downgrade guard
|
# Order: lock, finalize, artifacts, preflight, image build and downgrade guard
|
||||||
@@ -11,7 +11,7 @@ set -euo pipefail
|
|||||||
# shellcheck source=deploy/lib.sh
|
# shellcheck source=deploy/lib.sh
|
||||||
. "$(dirname "$(readlink -f "$0")")/lib.sh"
|
. "$(dirname "$(readlink -f "$0")")/lib.sh"
|
||||||
|
|
||||||
# finalize_release <sha> <upload>: CI never writes into releases/. A new SHA is
|
# finalize_release <sha> <upload>: an upload never writes into releases/. A new SHA is
|
||||||
# moved in atomically; a re-run of a deployed SHA must match it byte for byte
|
# moved in atomically; a re-run of a deployed SHA must match it byte for byte
|
||||||
# and leaves the finalized release, ARTIFACTS included, untouched.
|
# and leaves the finalized release, ARTIFACTS included, untouched.
|
||||||
finalize_release() {
|
finalize_release() {
|
||||||
@@ -100,10 +100,10 @@ retention() {
|
|||||||
rm -rf "$dir"
|
rm -rf "$dir"
|
||||||
log "removed release ${dir##*/}"
|
log "removed release ${dir##*/}"
|
||||||
done < <(find "$RELEASES" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\n' | sort -rn | tail -n +6 | cut -d' ' -f2-)
|
done < <(find "$RELEASES" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\n' | sort -rn | tail -n +6 | cut -d' ' -f2-)
|
||||||
# Uploads older than a day were never finalized (interrupted CI runs). Their
|
# Uploads older than a day were never finalized (interrupted deploys). The
|
||||||
# mtime is the server's (the workflow's rsync omits directory times), and a
|
# upload directory is created on the server when the upload starts, and its
|
||||||
# CI job lives at most 45 minutes, so an upload being sent or waiting for the
|
# mtime is the server's, so an upload being sent or waiting for the lock is
|
||||||
# lock is never this old.
|
# never this old.
|
||||||
while IFS= read -r -d '' dir; do
|
while IFS= read -r -d '' dir; do
|
||||||
chmod -R u+w "$dir"
|
chmod -R u+w "$dir"
|
||||||
rm -rf "$dir"
|
rm -rf "$dir"
|
||||||
|
|||||||
+2
-2
@@ -57,8 +57,8 @@ die() { log "ERROR: $*"; exit 1; }
|
|||||||
# is absolute, so nothing needs the caller's directory.
|
# is absolute, so nothing needs the caller's directory.
|
||||||
cd /
|
cd /
|
||||||
|
|
||||||
# acquire_lock <seconds>: deploy.sh and rollback.sh share this lock, so a CI
|
# acquire_lock <seconds>: deploy.sh and rollback.sh share this lock, so a
|
||||||
# deploy and a manual rollback never run at the same time. Held until exit.
|
# deploy and a rollback never run at the same time. Held until exit.
|
||||||
acquire_lock() {
|
acquire_lock() {
|
||||||
exec 9>>"$LOCK_FILE"
|
exec 9>>"$LOCK_FILE"
|
||||||
flock -w "$1" 9 || die "another deploy or rollback holds $LOCK_FILE (waited $1 s)"
|
flock -w "$1" 9 || die "another deploy or rollback holds $LOCK_FILE (waited $1 s)"
|
||||||
|
|||||||
+8
-23
@@ -2,27 +2,17 @@
|
|||||||
# One-time host preparation for the petrolyst server (Ubuntu 24.04), run as root.
|
# One-time host preparation for the petrolyst server (Ubuntu 24.04), run as root.
|
||||||
# Idempotent: re-running converges to the same state.
|
# Idempotent: re-running converges to the same state.
|
||||||
#
|
#
|
||||||
# bootstrap.sh --ci-key <public-key-file> --runner-ip <ip|any>
|
# bootstrap.sh
|
||||||
#
|
#
|
||||||
# --ci-key public half of the dedicated CI deploy key (DEPLOY_SSH_KEY in Gitea)
|
# Deploys are manual: a person runs them over root SSH, and root hands over to
|
||||||
# --runner-ip egress IP of the Gitea act_runner; the deploy key works only from
|
# the deploy user (README, "Manual deploy"). There is no CI runner, so deploy
|
||||||
# there ("any" drops that restriction, e.g. before the runner exists)
|
# has no SSH key of its own.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
[ "$(id -u)" = 0 ] || die "run as root"
|
[ "$(id -u)" = 0 ] || die "run as root"
|
||||||
|
|
||||||
ci_key='' runner_ip=''
|
[ $# -eq 0 ] || die "bootstrap.sh takes no arguments (deploys are manual; there is no CI key)"
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case $1 in
|
|
||||||
--ci-key) ci_key=$2; shift 2 ;;
|
|
||||||
--runner-ip) runner_ip=$2; shift 2 ;;
|
|
||||||
*) die "unknown argument $1" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
[ -s "$ci_key" ] || die "--ci-key <public-key-file> is required"
|
|
||||||
[ -n "$runner_ip" ] || die "--runner-ip <ip|any> is required"
|
|
||||||
grep -qE '^ssh-(ed25519|rsa) ' "$ci_key" || die "$ci_key does not look like an SSH public key"
|
|
||||||
|
|
||||||
ROOT=/opt/petrolyst
|
ROOT=/opt/petrolyst
|
||||||
|
|
||||||
@@ -69,15 +59,10 @@ ufw default allow outgoing >/dev/null
|
|||||||
for port in 22 80 443; do ufw allow "$port/tcp" >/dev/null; done
|
for port in 22 80 443; do ufw allow "$port/tcp" >/dev/null; done
|
||||||
ufw --force enable >/dev/null
|
ufw --force enable >/dev/null
|
||||||
|
|
||||||
echo "== deploy user (CI)"
|
echo "== deploy user (runs deploy.sh; reached only through sudo from root)"
|
||||||
id deploy >/dev/null 2>&1 || useradd --create-home --shell /bin/bash deploy
|
id deploy >/dev/null 2>&1 || useradd --create-home --shell /bin/bash deploy
|
||||||
usermod -aG docker deploy # docker access makes deploy root-equivalent; the key restrictions below matter
|
usermod -aG docker deploy # docker access makes deploy root-equivalent, so it gets no SSH key
|
||||||
install -d -o deploy -g deploy -m 0700 /home/deploy/.ssh
|
rm -f /home/deploy/.ssh/authorized_keys # the former CI key, if an earlier bootstrap installed it
|
||||||
options=restrict
|
|
||||||
[ "$runner_ip" = any ] || options="restrict,from=\"$runner_ip\""
|
|
||||||
printf '%s %s\n' "$options" "$(cat "$ci_key")" >/home/deploy/.ssh/authorized_keys
|
|
||||||
chown deploy:deploy /home/deploy/.ssh/authorized_keys
|
|
||||||
chmod 0600 /home/deploy/.ssh/authorized_keys
|
|
||||||
|
|
||||||
echo "== SSH: keys only"
|
echo "== SSH: keys only"
|
||||||
[ -s /root/.ssh/authorized_keys ] || die "root has no authorized_keys; refusing to turn off password login"
|
[ -s /root/.ssh/authorized_keys ] || die "root has no authorized_keys; refusing to turn off password login"
|
||||||
|
|||||||
Reference in new issue
Block a user