| index 5f0ed03..8917e6a 100755 |
| --- a/deploy/ops-canary.sh |
| +++ b/deploy/ops-canary.sh |
| @@ -4,6 +4,7 @@ |
| # |
| # ./deploy/ops canary create <slug> # test-<slug>.11ch.at, flagged --canary |
| # ./deploy/ops canary deploy <slug> # build THIS branch -> stage -> deploy --pin |
| +# ./deploy/ops canary reset <slug> # wipe back to first-run (iterate onboarding) |
| # ./deploy/ops canary destroy <slug> # reset (archive DB) + remove --stop |
| # ./deploy/ops canary list # every canary + pinned row on the box |
| # |
| @@ -49,11 +50,18 @@ DOMAIN="${LCHAT_CANARY_DOMAIN:-fi.dev.11ch.at}" |
| LCHAT=/opt/lchat/lchat |
| |
| usage() { |
| - echo "usage: ops canary create|deploy|destroy <slug> | ops canary list" >&2 |
| + echo "usage: ops canary create|deploy|reset|destroy <slug> | ops canary list" >&2 |
| echo " slug: lowercase letters/digits/hyphens — your branch or feature name" >&2 |
| exit 2 |
| } |
| |
| +# Canary links (the admin link's token, the setup/onboarding link's one-time |
| +# secret) must never land in a session transcript — transcripts are public. So |
| +# every verb that mints one writes the full output to ~/<host>.link ON THE BOX |
| +# and this filter redacts the URL fragments/tokens from what echoes locally. |
| +# Fetch the real links from your own terminal: ssh <box> cat <host>.link |
| +redact_secrets() { sed -E 's/#[^[:space:]]+/#<redacted>/g'; } |
| + |
| cmd="${1:-}"; slug="${2:-}" |
| |
| host_for() { |
| @@ -70,13 +78,21 @@ case "$cmd" in |
| create) |
| host="$(host_for "$slug")" |
| echo "ops canary: creating $host on $BOX (flagged --canary)" |
| - ssh "$BOX" "sudo -n $LCHAT add-instance --canary '$host'" |
| + # add-instance prints the admin link + the one-time setup link — both carry |
| + # secrets, so the full output is staged box-side and echoed here redacted. |
| + out="$(ssh "$BOX" "sudo -n $LCHAT add-instance --canary '$host' > \$HOME/$host.link 2>&1; st=\$?; cat \$HOME/$host.link; exit \$st")" || { |
| + printf '%s\n' "$out" | redact_secrets >&2 |
| + exit 1 |
| + } |
| + printf '%s\n' "$out" | redact_secrets |
| cat <<EOF |
| |
| -Canary up. The admin/setup link above is stored nowhere else — canary tokens |
| -deliberately die with the session (don't vault them). |
| - next: ./deploy/ops canary deploy $slug # put THIS branch's build on it |
| - tear down: ./deploy/ops canary destroy $slug # when the work merges |
| +Canary up. Its links live in $host.link on the box, nowhere else — canary |
| +tokens deliberately die with the session (don't vault them). |
| + fetch links: ssh $BOX cat $host.link # run in YOUR terminal |
| + next: ./deploy/ops canary deploy $slug # put THIS branch's build on it |
| + reset: ./deploy/ops canary reset $slug # back to first-run, same token |
| + tear down: ./deploy/ops canary destroy $slug # when the work merges |
| EOF |
| ;; |
| |
| @@ -117,6 +133,29 @@ $host is running $ver (pinned — the auto-updater will leave it alone). |
| EOF |
| ;; |
| |
| +reset) |
| + host="$(host_for "$slug")" |
| + echo "ops canary: resetting $host in place (DB archived to /data/trash; env/token kept)" |
| + # `lchat reset` is canary-gated (a real instance refuses), restarts the |
| + # instance on an empty DB, and mints a fresh one-time onboarding link — the |
| + # loop for iterating setup/onboarding scripts without a new host (each new |
| + # dev host would burn another cert from the shared 50/week LE budget). |
| + out="$(ssh "$BOX" "sudo -n $LCHAT reset '$host' --yes > \$HOME/$host.link 2>&1; st=\$?; cat \$HOME/$host.link; exit \$st")" || { |
| + printf '%s\n' "$out" | redact_secrets >&2 |
| + echo "ops canary: reset refused — if $host is genuinely a throwaway, flag it:" >&2 |
| + echo " ssh $BOX sudo -n $LCHAT canary $host --on # then re-run reset" >&2 |
| + exit 1 |
| + } |
| + printf '%s\n' "$out" | redact_secrets |
| + cat <<EOF |
| + |
| +$host is back at first-run (same admin token, same pinned build). A bot |
| +sidecar, if the canary had one, must be re-provisioned — its identities died |
| +with the DB (add-bot again after the new admin claims the space). |
| + fetch the fresh onboarding link: ssh $BOX cat $host.link # YOUR terminal |
| +EOF |
| + ;; |
| + |
| destroy) |
| host="$(host_for "$slug")" |
| echo "ops canary: destroying $host on $BOX" |
| @@ -124,14 +163,18 @@ destroy) |
| # blobs to /data/trash — canary-gated, so a host that was never flagged |
| # --canary refuses here rather than wiping. That refusal is the safety |
| # model working; flag it by hand only if it is genuinely a throwaway. |
| - ssh "$BOX" "sudo -n $LCHAT reset '$host' --yes" || { |
| + out="$(ssh "$BOX" "sudo -n $LCHAT reset '$host' --yes")" || { |
| + printf '%s\n' "$out" | redact_secrets >&2 |
| echo "ops canary: reset refused — if $host is genuinely a throwaway, flag it:" >&2 |
| echo " ssh $BOX sudo -n $LCHAT canary $host --on # then re-run destroy" >&2 |
| exit 1 |
| } |
| + printf '%s\n' "$out" | redact_secrets |
| # remove --stop: drops the route (and in.<host>), stops + disables the |
| - # instance unit and any bot/hooks sidecar units. |
| - ssh "$BOX" "sudo -n $LCHAT remove '$host' --stop" |
| + # instance unit and any bot/hooks sidecar units. The staged link file goes |
| + # with it — its secrets are dead, and a stale file would mislead the next |
| + # session to reuse this slug. |
| + ssh "$BOX" "sudo -n $LCHAT remove '$host' --stop; rm -f \$HOME/$host.link" |
| cat <<EOF |
| |
| $host destroyed. Its DB is archived in /data/trash (the reaper's trash purge |