mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:07:04 +01:00
refactor(dot-resolver): install the tracked sync-dot-cert.sh instead of generating it
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Every script under /usr/local/bin must be a repo file. install_cert_sync wrote /usr/local/bin/sync-dot-cert.sh from a heredoc with the domain, Caddy container and cert dir baked in. The script now lives at linux_configuration/scripts/sync-dot-cert.sh, takes those three as arguments (defaults = the installer's constants), and is installed with install -m 755. dot-cert-sync.service and the first sync pass the arguments, so --domain reaches the timer too. Test asserts the installed file is the tracked one byte for byte and runs the script itself against a docker shim (reload only on change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014yhTkYV6ZeuAbQYvykFGFc Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2dc4081dc9
commit
7b06fc3c4a
@ -0,0 +1,15 @@
|
||||
{
|
||||
"title": "Install the tracked sync-dot-cert.sh instead of generating it",
|
||||
"objective": "setup_dot_resolver.sh installs linux_configuration/scripts/sync-dot-cert.sh to /usr/local/bin with install -m 755 rather than writing it from a heredoc, with the same runtime behaviour (same docker cp paths, reload stunnel only on change) for the default domain and correct behaviour for a non-default --domain.",
|
||||
"acceptance_criteria": [
|
||||
"The installed /usr/local/bin/sync-dot-cert.sh is byte-identical to the tracked file and mode 755; a stale generated copy is replaced",
|
||||
"dot-cert-sync.service ExecStart passes domain, Caddy container and cert dir",
|
||||
"The tracked script assembles cert+key into a 600 combined.pem, reloads stunnel on change and is silent when unchanged",
|
||||
"test_dot_resolver_install.sh passes inside shell_coverage_jail.sh and the change passes in vmbox"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Rewriting the live host files (kuhy re-runs setup_dot_resolver.sh)",
|
||||
"Tracking deluge-fix-lt.py (blocked by repo lint for a verbatim copy)"
|
||||
],
|
||||
"verifier": "meta/scripts/shell_coverage_jail.sh --subject linux_configuration/features/lib/tests/test_dot_resolver_install.sh <jail_args> --min 0 --fail-on-case-error; vmbox guest run"
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
{
|
||||
"intent": "Every script installed to /usr/local/bin is a tracked repo file: sync-dot-cert.sh is installed from linux_configuration/scripts/ instead of being generated from a heredoc.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/sync-dot-cert.sh, linux_configuration/features/setup_dot_resolver.sh, linux_configuration/features/lib/dot_resolver_install.sh, linux_configuration/features/lib/tests/test_dot_resolver_install.sh",
|
||||
"Non-goal: stunnel config and units stay generated; deluge-fix-lt.py is not in this commit (repo lint blocks a verbatim copy)"
|
||||
],
|
||||
"changes": [
|
||||
"scripts/sync-dot-cert.sh takes <domain> <caddy-container> <cert-dir> (defaults = the installer's) instead of baking them in; install_cert_sync does sudo install -m 755 of it and dies if it is missing",
|
||||
"dot-cert-sync.service ExecStart and the installer's first sync pass the three arguments, so a non-default --domain reaches the timer; unused cert_source_dir removed"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "shell_coverage_jail.sh --subject features/lib/tests/test_dot_resolver_install.sh <jail_args> --min 0 --fail-on-case-error",
|
||||
"result": "pass",
|
||||
"evidence": "no case errors; a mutated sync script (reload removed) makes the jail fail"
|
||||
},
|
||||
{
|
||||
"command": "vmbox guest cco: install_cert_sync + install_units as root over a stale generated copy, then the installed script against a docker shim",
|
||||
"result": "pass",
|
||||
"evidence": "installed == tracked (cmp), 755 root, ExecStart passes domain/container/dir, docker cp paths identical to the live script, second run silent. Passed in vmbox, not verified on the host"
|
||||
},
|
||||
{
|
||||
"command": "shellcheck -x on the four shell files",
|
||||
"result": "pass",
|
||||
"evidence": "clean"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The live /usr/local/bin/sync-dot-cert.sh and dot-cert-sync.service stay the old generated ones until setup_dot_resolver.sh is re-run",
|
||||
"A bare manual sync-dot-cert.sh run uses the defaults, which must track the installer's constants"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit and re-run setup_dot_resolver.sh",
|
||||
"Check systemctl start dot-cert-sync.service and setup_dot_resolver.sh --check"
|
||||
]
|
||||
}
|
||||
@ -9,25 +9,17 @@ install_cert_sync() {
|
||||
# Caddy renews on its own schedule; stunnel reads files from disk and must
|
||||
# be told. A timer copies the current cert out of the container and
|
||||
# reloads stunnel only when the content actually changed.
|
||||
# The script is the tracked scripts/sync-dot-cert.sh, installed as-is; the
|
||||
# values it used to have baked in are now its arguments (sync_script_args).
|
||||
sudo install -d -m 700 "$CERT_DIR"
|
||||
sudo tee "$SYNC_SCRIPT" >/dev/null <<EOF
|
||||
#!/bin/bash
|
||||
# Copy the Caddy-managed certificate for $DOMAIN out to stunnel.
|
||||
# Generated by $SCRIPT_NAME - edit that instead.
|
||||
set -euo pipefail
|
||||
src="$(cert_source_dir)"
|
||||
tmp="\$(mktemp -d)"
|
||||
trap 'rm -rf "\$tmp"' EXIT
|
||||
docker cp "$CADDY_CONTAINER:\$src/$DOMAIN.crt" "\$tmp/cert.pem"
|
||||
docker cp "$CADDY_CONTAINER:\$src/$DOMAIN.key" "\$tmp/key.pem"
|
||||
cat "\$tmp/cert.pem" "\$tmp/key.pem" >"\$tmp/combined.pem"
|
||||
if ! cmp -s "\$tmp/combined.pem" "$CERT_DIR/combined.pem"; then
|
||||
install -m 600 "\$tmp/combined.pem" "$CERT_DIR/combined.pem"
|
||||
systemctl reload-or-restart stunnel-dot.service
|
||||
echo "certificate updated for $DOMAIN"
|
||||
fi
|
||||
EOF
|
||||
sudo chmod 755 "$SYNC_SCRIPT"
|
||||
[[ -f $SYNC_SCRIPT_SRC ]] || die "tracked sync script missing: $SYNC_SCRIPT_SRC"
|
||||
sudo install -m 755 "$SYNC_SCRIPT_SRC" "$SYNC_SCRIPT"
|
||||
}
|
||||
|
||||
# The arguments every caller of $SYNC_SCRIPT passes, so a non-default --domain
|
||||
# reaches the timer's runs too.
|
||||
sync_script_args() {
|
||||
printf '%s %s %s' "$DOMAIN" "$CADDY_CONTAINER" "$CERT_DIR"
|
||||
}
|
||||
|
||||
install_stunnel_conf() {
|
||||
@ -74,7 +66,7 @@ Description=Copy the Caddy certificate for $DOMAIN to stunnel
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=$SYNC_SCRIPT
|
||||
ExecStart=$SYNC_SCRIPT $(sync_script_args)
|
||||
EOF
|
||||
|
||||
sudo tee /etc/systemd/system/dot-cert-sync.timer >/dev/null <<EOF
|
||||
|
||||
@ -38,15 +38,12 @@ DOMAIN="dns.kuhy.duckdns.org"
|
||||
BIND_ADDR="10.8.0.1"
|
||||
CADDY_CONTAINER="caddy"
|
||||
|
||||
# install_cert_sync interpolates this at GENERATION time (the heredoc is
|
||||
# unquoted), baking the resolved path into the script it writes. That is
|
||||
# deliberate, not a bug -- the live /usr/local/bin/sync-dot-cert.sh carries a
|
||||
# fully populated src=. The fixture must therefore define it exactly as the
|
||||
# entry script does, or the generated script gets src="" and the assertion
|
||||
# below silently passes over an empty path.
|
||||
cert_source_dir() {
|
||||
printf '/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/%s' \
|
||||
"$DOMAIN"
|
||||
# install_cert_sync installs this tracked file verbatim (no heredoc): every
|
||||
# script under /usr/local/bin must be a file in a repo.
|
||||
SYNC_SCRIPT_SRC="$(cd "$SCRIPT_DIR/../../../scripts" && pwd)/sync-dot-cert.sh"
|
||||
die() {
|
||||
printf 'die: %s\n' "$1" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# shellcheck source=../dot_resolver_install.sh
|
||||
@ -64,9 +61,16 @@ mkdir -p /etc/stunnel /etc/systemd/system /usr/local/bin
|
||||
|
||||
# --- install_cert_sync ------------------------------------------------------
|
||||
install_cert_sync
|
||||
_t_file_has "$SYNC_SCRIPT" 'combined.pem' "cert sync script assembles combined.pem"
|
||||
_t_file_has "$SYNC_SCRIPT" "src=\"/data/caddy/certificates" "cert source path is baked in, not left empty"
|
||||
_t_file_has "$SYNC_SCRIPT" 'reload-or-restart' "cert sync restarts stunnel after renewal"
|
||||
if cmp -s "$SYNC_SCRIPT_SRC" "$SYNC_SCRIPT"; then
|
||||
_t_pass "the installed sync script is the tracked file, byte for byte"
|
||||
else
|
||||
_t_fail "the installed sync script is the tracked file, byte for byte"
|
||||
fi
|
||||
if grep -q 'Generated by' "$SYNC_SCRIPT"; then
|
||||
_t_fail "the sync script is not generated"
|
||||
else
|
||||
_t_pass "the sync script is not generated"
|
||||
fi
|
||||
if [[ -d $CERT_DIR ]]; then
|
||||
_t_pass "cert dir created"
|
||||
else
|
||||
@ -74,6 +78,46 @@ else
|
||||
fi
|
||||
_t_eq "700" "$(stat -c '%a' "$CERT_DIR")" "cert dir is mode 700 (it holds a private key)"
|
||||
_t_eq "755" "$(stat -c '%a' "$SYNC_SCRIPT")" "sync script is executable"
|
||||
# Reinstall over a stale generated copy: install replaces it (idempotent).
|
||||
printf '#!/bin/bash\n# Generated by setup_dot_resolver.sh\n' >"$SYNC_SCRIPT"
|
||||
install_cert_sync
|
||||
_t_eq "$(sha256sum <"$SYNC_SCRIPT_SRC")" "$(sha256sum <"$SYNC_SCRIPT")" "a stale generated copy is replaced"
|
||||
out="$(SYNC_SCRIPT_SRC="$TEST_TMPDIR/absent.sh" install_cert_sync 2>&1 || true)"
|
||||
_t_has "$out" 'tracked sync script missing' "a missing tracked file stops the install"
|
||||
|
||||
# --- the tracked sync script itself -----------------------------------------
|
||||
# docker cp is stubbed to copy from a fixture tree, so the script's own logic
|
||||
# (assemble, compare, install, reload-only-on-change) runs for real.
|
||||
SYNC_CERTS="$TEST_TMPDIR/caddy"
|
||||
mkdir -p "$SYNC_CERTS"
|
||||
cat >"$TEST_TMPDIR/bin/docker" <<DOCKER
|
||||
#!/usr/bin/env bash
|
||||
printf 'docker %s\n' "\$*" >>"$TEST_TMPDIR/calls.log"
|
||||
cp "$SYNC_CERTS/\$(basename "\${2#*:}")" "\$3"
|
||||
DOCKER
|
||||
chmod +x "$TEST_TMPDIR/bin/docker"
|
||||
_t_stub systemctl
|
||||
sync_dir="$TEST_TMPDIR/stunnel-dot"
|
||||
mkdir -p "$sync_dir"
|
||||
printf 'CERT1\n' >"$SYNC_CERTS/d.example.crt"
|
||||
printf 'KEY1\n' >"$SYNC_CERTS/d.example.key"
|
||||
out="$("$SYNC_SCRIPT_SRC" d.example box "$sync_dir")"
|
||||
_t_eq "$(printf 'CERT1\nKEY1')" "$(cat "$sync_dir/combined.pem")" "the sync assembles cert + key"
|
||||
_t_eq "600" "$(stat -c '%a' "$sync_dir/combined.pem")" "combined.pem is mode 600"
|
||||
_t_has "$out" 'certificate updated for d.example' "a new cert is announced"
|
||||
_t_called 'docker cp box:/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/d.example/d.example.crt' \
|
||||
"the cert is read from the domain's Caddy dir in the named container"
|
||||
_t_called 'systemctl reload-or-restart stunnel-dot.service' "a new cert reloads stunnel"
|
||||
: >"$TEST_TMPDIR/calls.log"
|
||||
out="$("$SYNC_SCRIPT_SRC" d.example box "$sync_dir")"
|
||||
_t_eq "" "$out" "an unchanged cert is silent"
|
||||
if grep -q reload "$TEST_TMPDIR/calls.log"; then
|
||||
_t_fail "an unchanged cert does not reload stunnel"
|
||||
else
|
||||
_t_pass "an unchanged cert does not reload stunnel"
|
||||
fi
|
||||
_t_unstub docker
|
||||
_t_unstub systemctl
|
||||
|
||||
# --- install_stunnel_conf ---------------------------------------------------
|
||||
install_stunnel_conf
|
||||
@ -94,7 +138,8 @@ if grep -q '^Requires=dnsmasq' /etc/systemd/system/stunnel-dot.service; then
|
||||
else
|
||||
_t_pass "no Requires=dnsmasq.service"
|
||||
fi
|
||||
_t_file_has /etc/systemd/system/dot-cert-sync.service "$SYNC_SCRIPT" "cert-sync service runs the sync script"
|
||||
_t_file_has /etc/systemd/system/dot-cert-sync.service "^ExecStart=$SYNC_SCRIPT $DOMAIN $CADDY_CONTAINER $CERT_DIR\$" \
|
||||
"cert-sync service passes domain, container and cert dir"
|
||||
_t_file_has /etc/systemd/system/dot-cert-sync.timer 'OnCalendar' "cert-sync timer is scheduled"
|
||||
_t_called 'daemon-reload' "units are followed by a daemon-reload"
|
||||
|
||||
|
||||
@ -77,12 +77,6 @@ require_tools() {
|
||||
fi
|
||||
}
|
||||
|
||||
cert_source_dir() {
|
||||
# Caddy stores certs per-domain under its data volume.
|
||||
printf '/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/%s' \
|
||||
"$DOMAIN"
|
||||
}
|
||||
|
||||
verify_prerequisites() {
|
||||
# A DoT endpoint in front of a resolver that is not running would answer
|
||||
# every query with a connection error, which looks exactly like a network
|
||||
@ -110,6 +104,8 @@ verify_prerequisites() {
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
||||
# Installed verbatim to $SYNC_SCRIPT; never generated (see install_cert_sync).
|
||||
readonly SYNC_SCRIPT_SRC="$SCRIPT_DIR/../scripts/sync-dot-cert.sh"
|
||||
# shellcheck source=lib/dot_resolver_install.sh
|
||||
source "$SCRIPT_DIR/lib/dot_resolver_install.sh"
|
||||
|
||||
@ -126,7 +122,7 @@ main() {
|
||||
install_units
|
||||
|
||||
log "Syncing certificate..."
|
||||
sudo "$SYNC_SCRIPT" || die "certificate sync failed - has Caddy issued $DOMAIN yet?"
|
||||
sudo "$SYNC_SCRIPT" "$DOMAIN" "$CADDY_CONTAINER" "$CERT_DIR" || die "certificate sync failed - has Caddy issued $DOMAIN yet?"
|
||||
|
||||
sudo systemctl enable --now stunnel-dot.service
|
||||
sudo systemctl enable --now dot-cert-sync.timer
|
||||
|
||||
28
linux_configuration/scripts/sync-dot-cert.sh
Executable file
28
linux_configuration/scripts/sync-dot-cert.sh
Executable file
@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# Copy the Caddy-managed certificate for the DoT hostname out to stunnel, and
|
||||
# reload stunnel only when the content actually changed.
|
||||
#
|
||||
# Installed to /usr/local/bin/sync-dot-cert.sh by
|
||||
# features/setup_dot_resolver.sh (install -m 755 of THIS file) and run daily by
|
||||
# dot-cert-sync.timer. The installer passes all three arguments, so the
|
||||
# defaults below only matter for a bare manual run; they match the installer's.
|
||||
#
|
||||
# Usage: sync-dot-cert.sh [<domain> [<caddy-container> [<cert-dir>]]]
|
||||
set -euo pipefail
|
||||
|
||||
domain="${1:-dns.kuhy.duckdns.org}"
|
||||
container="${2:-gitea-caddy}"
|
||||
cert_dir="${3:-/etc/stunnel/dot}"
|
||||
|
||||
# Caddy stores certs per-domain under its data volume.
|
||||
src="/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/$domain"
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
docker cp "$container:$src/$domain.crt" "$tmp/cert.pem"
|
||||
docker cp "$container:$src/$domain.key" "$tmp/key.pem"
|
||||
cat "$tmp/cert.pem" "$tmp/key.pem" >"$tmp/combined.pem"
|
||||
if ! cmp -s "$tmp/combined.pem" "$cert_dir/combined.pem"; then
|
||||
install -m 600 "$tmp/combined.pem" "$cert_dir/combined.pem"
|
||||
systemctl reload-or-restart stunnel-dot.service
|
||||
echo "certificate updated for $domain"
|
||||
fi
|
||||
Loading…
Reference in New Issue
Block a user