From 9c3cd983b44cfe3023bc36bae171a7c2a0489ef4 Mon Sep 17 00:00:00 2001 From: saubyk <39208279+saubyk@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:54:55 -0700 Subject: [PATCH] Make the CLN dev-fixture rune creation self-healing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review F5 on #1625: create-rune.sh was a one-shot poststart script — if the RPC wasn't ready within its poll or createrune failed, it exited without ever writing rtl.rune, and since the cln healthcheck gates on that file and rtl waits on service_healthy, a failed pass deadlocked the whole stack until 'down -v'. Drive rune creation from the healthcheck instead: the script is now a quick, idempotent single attempt, and the healthcheck runs it on every interval, so a transient RPC-startup race just retries and self-heals. Moved the script out of lightning-poststart.d to /opt and updated the healthcheck, compose comment and README accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) --- docker/README.md | 15 +++++++----- docker/cln/create-rune.sh | 26 ++++++++++++++++++++ docker/cln/poststart.d/create-rune.sh | 35 --------------------------- docker/docker-compose.yml | 19 +++++++++------ 4 files changed, 46 insertions(+), 49 deletions(-) create mode 100755 docker/cln/create-rune.sh delete mode 100755 docker/cln/poststart.d/create-rune.sh diff --git a/docker/README.md b/docker/README.md index ccc2b6a9..a53ab082 100644 --- a/docker/README.md +++ b/docker/README.md @@ -115,12 +115,15 @@ before she can route to carol. The seed waits for this; anything you script your should too. **Core Lightning auth uses a rune.** RTL talks to `cln` over clnrest and authenticates -with a rune, not a macaroon. On first start `cln/poststart.d/create-rune.sh` runs inside -the node (once the RPC is up), creates a master rune, and writes it as -`LIGHTNING_RUNE="…"` to `rtl.rune` in the shared `cln_data` volume; RTL reads it via the -`runePath` in its config. The `cln` healthcheck only passes once that file exists, so RTL -waits for it. `--clnrest-host=0.0.0.0` is required for RTL (another container) to reach -clnrest; the default `127.0.0.1` would only be reachable from inside the node. +with a rune, not a macaroon. `cln/create-rune.sh` — run from the `cln` healthcheck — +creates a master rune once the RPC is up and writes it as `LIGHTNING_RUNE="…"` to +`rtl.rune` in the shared `cln_data` volume; RTL reads it via the `runePath` in its config. +The healthcheck reports unhealthy until that file exists, so RTL (which waits on +`service_healthy`) starts only once the rune is ready. Because it runs on every +healthcheck tick (idempotent), a transient RPC-startup race just retries and self-heals +rather than wedging the stack. `--clnrest-host=0.0.0.0` is required for RTL (another +container) to reach clnrest; the default `127.0.0.1` would only be reachable from inside +the node. ## Not included diff --git a/docker/cln/create-rune.sh b/docker/cln/create-rune.sh new file mode 100755 index 00000000..b288cf17 --- /dev/null +++ b/docker/cln/create-rune.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# +# Ensure the RTL rune exists. One quick, idempotent attempt: +# - already have it -> succeed +# - RPC up, createrune works -> write it, succeed +# - RPC not ready / failure -> fail, so the caller retries +# +# This is invoked from the cln healthcheck (not a one-shot poststart hook) so a +# transient RPC-startup race self-heals on the next healthcheck tick instead of +# permanently wedging the stack. Stores the rune as LIGHTNING_RUNE="", the +# format RTL reads via its runePath. POSIX sh compatible. +set -u + +RUNE_FILE="${LIGHTNINGD_DATA}/rtl.rune" + +[ -f "${RUNE_FILE}" ] && exit 0 + +lightning-cli --network="${LIGHTNINGD_NETWORK}" getinfo >/dev/null 2>&1 || exit 1 + +rune=$(lightning-cli --network="${LIGHTNINGD_NETWORK}" createrune 2>/dev/null \ + | grep -o '"rune"[[:space:]]*:[[:space:]]*"[^"]*"' \ + | sed -e 's/.*"rune"[[:space:]]*:[[:space:]]*"//' -e 's/"$//') + +[ -n "${rune}" ] || exit 1 + +printf 'LIGHTNING_RUNE="%s"\n' "${rune}" > "${RUNE_FILE}" diff --git a/docker/cln/poststart.d/create-rune.sh b/docker/cln/poststart.d/create-rune.sh deleted file mode 100755 index 45a4108b..00000000 --- a/docker/cln/poststart.d/create-rune.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# -# Runs from Core Lightning's lightning-poststart.d. Creates a master rune (once) -# and stores it in the format RTL expects: a file containing -# LIGHTNING_RUNE="" -# which RTL reads via its runePath. -# -# The image entrypoint can invoke poststart scripts before the RPC socket is -# ready (it watches the datadir with a race that loses on a fresh node), so poll -# for `getinfo` before calling createrune. Idempotent: keeps the same rune across -# restarts so RTL's stored auth stays valid. - -RUNE_FILE="${LIGHTNINGD_DATA}/rtl.rune" - -[ -f "${RUNE_FILE}" ] && exit 0 - -# Wait (up to ~120s) for the RPC to accept commands. -for _ in $(seq 1 120); do - if lightning-cli --network="${LIGHTNINGD_NETWORK}" getinfo >/dev/null 2>&1; then - break - fi - sleep 1 -done - -rune=$(lightning-cli --network="${LIGHTNINGD_NETWORK}" createrune 2>/dev/null \ - | grep -o '"rune"[[:space:]]*:[[:space:]]*"[^"]*"' \ - | sed -e 's/.*"rune"[[:space:]]*:[[:space:]]*"//' -e 's/"$//') - -if [ -z "${rune}" ]; then - echo "create-rune.sh: failed to create rune" >&2 - exit 1 -fi - -printf 'LIGHTNING_RUNE="%s"\n' "${rune}" > "${RUNE_FILE}" -echo "create-rune.sh: wrote rune to ${RUNE_FILE}" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 2fe456c7..21fd6642 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -145,9 +145,9 @@ services: # Core Lightning node. Unlike the LND nodes it talks to RTL over clnrest (the # built-in REST plugin) using rune auth, so it needs --clnrest-* options and a - # rune written where RTL can read it. The entrypoint runs scripts dropped in - # lightning-poststart.d once the RPC socket is up; create-rune.sh generates the - # rune there and writes it to /root/.lightning/rtl.rune (RTL mounts that read-only). + # rune written where RTL can read it. create-rune.sh (run from the healthcheck) + # creates the rune and writes it to /root/.lightning/rtl.rune (RTL mounts that + # read-only); the healthcheck is unhealthy until it exists, so rtl waits for it. # --clnrest-host=0.0.0.0 is required so the rtl container can reach it; the default # 127.0.0.1 would only be reachable from inside this container. Protocol stays https # (clnrest default, self-signed) — RTL connects with rejectUnauthorized:false. @@ -175,13 +175,16 @@ services: - "${CLN_REST_PORT:-3010}:3010" volumes: - cln_data:/root/.lightning - - ./cln/poststart.d:/root/.lightning/lightning-poststart.d:ro + - ./cln/create-rune.sh:/opt/create-rune.sh:ro healthcheck: - # Healthy only once the RPC is up AND the rune file has been written, so the - # rtl service (which reads the rune at startup) can wait on this. - test: ["CMD-SHELL", "lightning-cli --network=regtest getinfo >/dev/null 2>&1 && test -f /root/.lightning/rtl.rune"] + # create-rune.sh ensures the rune exists (idempotent, one quick attempt) and + # this reports healthy only once it does. Driving it from the healthcheck — which + # retries on its interval — means a transient RPC-startup race self-heals instead + # of a one-shot script permanently wedging the stack. rtl waits on this via + # depends_on: condition: service_healthy before reading the rune at startup. + test: ["CMD-SHELL", "sh /opt/create-rune.sh && test -f /root/.lightning/rtl.rune"] interval: 5s - timeout: 5s + timeout: 10s retries: 40 # RTL rewrites its config file on startup, so it cannot be given the tracked