mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
fix(web-app-docs): replay the published instructions against a server, not against the recipe container
The harness rewrote HOST to localhost, and the provisioner turns localhost into ansible_connection local, so the replayed deploy configured the throwaway container the recipe starts rather than any server. PID 1 there is the entrypoint, not systemd, and sys-svc-container refused with "PID 1 is infinito, not systemd" in all five rows that deploy web-app-docs alongside their own role. The block now runs on the stack host and deploys over ssh into a target container that carries systemd and an ssh server and nothing else. sshd.sh installs that server and authorizes the key pair the run generates; machine.sh reads the target bridge address with container inspect, substitutes it for HOST, and only then runs the placeholder guard, because the address does not exist before that point. The target joins the default bridge so the recipe nested docker run can reach it, and that bridge has no DNS, so the value is an address. PREPARE and engine.sh both fall away. The target is bare on purpose: the replayed deploy installs docker-ce itself, which is the claim under test, so the restart collision engine.sh worked around can no longer occur. Its /var/lib/docker stays a named volume so the installed daemon writes to a real filesystem instead of stacking fuse-overlayfs on fuse-overlayfs. A shape guard refuses a block that starts no container, because this harness would otherwise configure the stack host itself; that guard was exercised in both directions. Teardown now also removes the generated key pair and the inventory the recipe writes, the latter only when the run created the directory. A schema in the test folder documents the flow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
de0c4e0e00
commit
9aa65a7731
8 files changed
+220
-94
No files matched your search
@@ -24,17 +24,19 @@ Reading the block off the page rather than out of the tree is the point: a reade
|
||||
|
||||
## The machine 🐳
|
||||
|
||||
Placeholders (`<your-domain>`, `<your-ssh-public-key>`, `<your-clearnet-resolver>`) are filled from the deployment's own values, `HOST` is forced to `localhost`, and `-it` is dropped.
|
||||
Placeholders (`<your-domain>`, `<your-clearnet-resolver>`) are filled from the deployment's own values, and `<your-ssh-public-key>` / `<your-ssh-private-key-file>` from an ed25519 pair generated per run under `DOCS_TEST_WORK`.
|
||||
|
||||
The block then runs inside a throwaway machine, never on the stack host itself. [machine.sh](../../../../../roles/web-app-docs/files/test/machine.sh) brings it up through [files/test/compose.yml](../../../../../roles/web-app-docs/files/test/compose.yml) under the project `docs-guide-<role>`, and tears it down with `down -v --rmi local`.
|
||||
The block runs on the stack host and deploys into a throwaway target container, which is what the published recipe describes: a managed server reached over SSH. [machine.sh](../../../../../roles/web-app-docs/files/test/machine.sh) brings that target up through [files/test/compose.yml](../../../../../roles/web-app-docs/files/test/compose.yml) under the project `docs-guide-<role>`, and tears it down with `down -v --rmi local`.
|
||||
|
||||
`HOST` is filled with the target's own bridge address, resolved once the container is up. The target joins the default bridge so the recipe's nested `docker run` can reach it; that bridge has no DNS, so the value must be an address rather than a name. The placeholder guard therefore runs after this fill, not before it.
|
||||
|
||||
The machine starts from the **distribution base image**, not from an image this project publishes, so the replay also exercises the installation. `MACHINE_BASE_IMAGE` is read as `INFINITO_PARENT_IMAGE` out of the `.env` of the mounted checkout, where the env layer has already composed it from the [meta/distros.yml](../../../../../meta/distros.yml) SPOT and honoured any `custom.env` override. A missing `.env` fails the test with a pointer to `make dotenv`. The one build step of [files/test/Dockerfile](../../../../../roles/web-app-docs/files/test/Dockerfile) runs [init.sh](../../../../../roles/web-app-docs/files/test/init.sh), which returns immediately when the base already carries `/sbin/init`.
|
||||
|
||||
Once the machine is up, the repository checkout mounted into it is prepared with [scripts/tests/workspace/base/01_install.sh](../../../../../scripts/tests/workspace/base/01_install.sh), the same step the workspace suite uses: system packages through `INFINITO_PACKAGE_INSTALL_SCRIPT`, then `make install`. Only then is the block replayed, so it meets a machine prepared exactly the way the documentation tells a reader to prepare one.
|
||||
The target receives nothing but an ssh server before the replay. [sshd.sh](../../../../../roles/web-app-docs/files/test/sshd.sh) installs one, authorizes the run's public key for `root`, and `machine.sh` waits for it to answer. Everything else arrives through the deploy, which is the claim under test.
|
||||
|
||||
One service serves both block shapes: both need `/sbin/init` as the entrypoint and the cgroup mount, and a containerised block additionally needs a work directory mounted at the same absolute path on both sides so the bind sources it names resolve on the daemon.
|
||||
The target carries no container engine of its own. The replayed deploy installs `docker-ce` and restarts `docker.service`; a target that already ran the distribution's engine fails that restart with `failed to load listeners: no sockets found via socket activation`, because the running `docker.socket` belongs to the package being replaced. Keeping the target bare removes the collision instead of working around it. Its `/var/lib/docker` is a named volume so the installed daemon lands on a real filesystem rather than stacking `fuse-overlayfs` on `fuse-overlayfs`.
|
||||
|
||||
The container engine is not part of that image. [engine.sh](../../../../../roles/web-app-docs/files/test/engine.sh) installs and starts one inside the running machine, and [machine.sh](../../../../../roles/web-app-docs/files/test/machine.sh) pipes it in only for a block that nests a `docker run`, which needs a daemon before the deploy inside it installs anything. A host install must not get it: `sys-svc-container` replaces the engine with `docker-ce` and restarts `docker.service`, and that restart fails with `failed to load listeners: no sockets found via socket activation` when the running `docker.socket` belongs to the distribution package being replaced. Such a machine gets its engine from the deploy itself.
|
||||
The deploy reaches `root` over SSH, so a first run needs `PermitRootLogin` to allow it. `SYS_SVC_SSHD_PERMIT_ROOT_LOGIN` defaults to `no`; the published recipe passes `prohibit-password` through the `--vars` JSON it already carries.
|
||||
|
||||
The block is replayed once systemd reports `running` or `degraded`. A block that opens with `git clone` is a host install: those two lines are dropped and the mounted checkout is used instead, so the replay runs against the tree under test.
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Instruction replay
|
||||
|
||||
The CLI test of `web-app-docs` reads the Production block out of the running documentation site and executes it, so the published instructions are proven rather than reviewed.
|
||||
|
||||
## Cast
|
||||
|
||||
| | What it is | What runs there |
|
||||
| --- | --- | --- |
|
||||
| 🖥️ **Stack host** | the worker the outer play already runs on | `test.sh`, `machine.sh`, and the published block itself |
|
||||
| 🎯 **Target** | a throwaway container, project `docs-guide-<role>` | systemd and an ssh server, nothing else until the deploy lands |
|
||||
| 🐳 **Recipe container** | started by the block's own `docker run`, on the stack daemon | the deploy that the instructions tell a reader to run |
|
||||
|
||||
The target is what the instructions call "a managed server". It is deliberately bare: everything else it ends up with arrives through the deploy under test.
|
||||
|
||||
## Schema
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph HOST["🖥️ stack host"]
|
||||
direction TB
|
||||
TEST["1️⃣ 🧪 test.sh<br>returns early unless DOCS_TEST_MODE=compose"]
|
||||
EXTRACT["2️⃣ 📖 extract.py<br>reads the block off the live docs site"]
|
||||
FILL["3️⃣ 🔑 ed25519 pair + placeholder fill<br>everything except HOST"]
|
||||
MACHINE["4️⃣ 🚜 machine.sh"]
|
||||
ADDR["7️⃣ 📍 HOST = the target's bridge address"]
|
||||
GUARD["8️⃣ 🚦 placeholder guard"]
|
||||
BLOCK["9️⃣ ▶️ bash -e block.sh"]
|
||||
end
|
||||
|
||||
subgraph TARGET["🎯 target container"]
|
||||
direction TB
|
||||
INIT["5️⃣ ⚙️ systemd, no engine<br>Dockerfile + init.sh"]
|
||||
SSHD["6️⃣ 🔐 sshd.sh<br>ssh server + run key for root"]
|
||||
DEPLOYED["🔟 📦 docker-ce and the role"]
|
||||
end
|
||||
|
||||
RECIPE["🐳 docker run <deploy image><br>on the stack daemon"]
|
||||
|
||||
TEST --> EXTRACT --> FILL --> MACHINE
|
||||
MACHINE -->|"🌉 compose up"| INIT
|
||||
INIT -->|"⏳ await systemd"| SSHD
|
||||
SSHD --> ADDR --> GUARD --> BLOCK
|
||||
BLOCK --> RECIPE
|
||||
RECIPE -->|"🔗 ssh as root"| DEPLOYED
|
||||
MACHINE -.->|"🧹 trap EXIT"| TARGET
|
||||
|
||||
classDef host fill:#e8f0fe,stroke:#1a73e8,color:#102a43;
|
||||
classDef target fill:#e6f4ea,stroke:#137333,color:#0b2e13;
|
||||
classDef pivot fill:#fef7e0,stroke:#b06000,color:#3d2600;
|
||||
classDef recipe fill:#f3e8fd,stroke:#8430ce,color:#2a0b45;
|
||||
class TEST,EXTRACT,FILL,MACHINE,BLOCK host;
|
||||
class INIT,SSHD,DEPLOYED target;
|
||||
class ADDR,GUARD pivot;
|
||||
class RECIPE recipe;
|
||||
```
|
||||
|
||||
🔵 stack host · 🟢 target · 🟣 the recipe's own container · 🟠 the two steps that bridge them
|
||||
|
||||
## Flow
|
||||
|
||||
| # | Step | Detail |
|
||||
| --- | --- | --- |
|
||||
| 1️⃣ | 🧪 `test.sh` starts on the stack host | returns early unless the deployment mode is compose |
|
||||
| 2️⃣ | 📖 `extract.py` writes `block.sh` | fetches the role's page over `DOCS_TEST_BASE_URL`; an empty block fails the test, and `bash -n` parses it before anything runs it |
|
||||
| 3️⃣ | 🔑 key pair and placeholders | an ed25519 pair per run under `DOCS_TEST_WORK`; every placeholder except `HOST` is substituted |
|
||||
| 4️⃣ | 🚜 `machine.sh` brings the target up | from `compose.yml`, on the default bridge |
|
||||
| 5️⃣ | ⚙️ the target boots | image built from `Dockerfile` + `init.sh`; `MACHINE_BASE_IMAGE` comes from `INFINITO_PARENT_IMAGE` in the checkout's `.env` |
|
||||
| 6️⃣ | 🔐 `sshd.sh` is piped in | installs an ssh server, authorizes the run's public key for `root`, writes a `PermitRootLogin prohibit-password` drop-in |
|
||||
| 7️⃣ | 📍 `HOST` is filled | `container inspect --type container` reads the bridge address; the bridge has no DNS, so it is an address and not a name |
|
||||
| 8️⃣ | 🚦 the guard runs | fails on any remaining `<your-`; it runs **after** step 7, because the address does not exist before it |
|
||||
| 9️⃣ | ▶️ the block executes on the stack host | its `docker run` starts the published deploy image on the stack daemon |
|
||||
| 🔟 | 📦 the deploy reaches the target over ssh as `root` | the target receives `docker-ce` and the role; its `/var/lib/docker` is a named volume, so the installed daemon writes to a real filesystem |
|
||||
| 🧹 | teardown | `compose down -v --rmi local` from an `EXIT` trap |
|
||||
|
||||
## Environment
|
||||
|
||||
| Variable | Source | Use |
|
||||
| --- | --- | --- |
|
||||
| `DOCS_TEST_BASE_URL` | `templates/test.env.j2` | documentation site the block is read from |
|
||||
| `DOCS_TEST_ROLE` | `templates/test.env.j2` | role whose page carries the instructions; names the compose project |
|
||||
| `DOCS_TEST_MODE` | `templates/test.env.j2` | deployment mode; the test runs only for compose |
|
||||
| `DOCS_TEST_DOMAIN` | `templates/test.env.j2` | fills `<your-domain>` |
|
||||
| `DOCS_TEST_DNS` | `templates/test.env.j2` | fills `<your-clearnet-resolver>` |
|
||||
| `DOCS_TEST_SRC_DIR` | `templates/test.env.j2` | checkout holding the generated `.env` |
|
||||
| `DOCS_TEST_TIMEOUT` | `templates/test.env.j2` | wait for the on-demand documentation build |
|
||||
| `DOCS_TEST_WORK` | `test.sh` | holds the replay key pair |
|
||||
| `DOCS_TEST_REPLAY_PUBKEY` | `test.sh` | public half, consumed by `sshd.sh` |
|
||||
|
||||
## Files
|
||||
|
||||
| File | Role |
|
||||
| --- | --- |
|
||||
| `test.sh` | entry point; extraction, key pair, placeholder fill |
|
||||
| `extract.py` | pulls the Production block off the live documentation site |
|
||||
| `machine.sh` | target lifecycle, `HOST` fill, guard, replay, teardown |
|
||||
| `compose.yml` | the target service, on the default bridge, with a named volume at `/var/lib/docker` |
|
||||
| `Dockerfile` | builds the target from the distribution base image |
|
||||
| `init.sh` | installs systemd into that image, and deliberately no container engine |
|
||||
| `sshd.sh` | ssh server and the run's authorized key, piped into the running target |
|
||||
@@ -8,12 +8,11 @@ services:
|
||||
init: false
|
||||
privileged: true
|
||||
cgroup: host
|
||||
network_mode: bridge
|
||||
tmpfs:
|
||||
- /run
|
||||
- /run/lock
|
||||
volumes:
|
||||
- ${DOCS_TEST_SRC_DIR}:${DOCS_TEST_SRC_DIR}
|
||||
- ${DOCS_TEST_WORK}:${DOCS_TEST_WORK}
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
- engine:/var/lib/docker
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# A block that deploys the machine itself must NOT get this: sys-svc-container
|
||||
# replaces the engine with docker-ce, and its docker.service restart fails
|
||||
# when the running docker.socket belongs to the package being replaced.
|
||||
|
||||
set -e
|
||||
|
||||
install_engine() {
|
||||
if command -v dockerd >/dev/null 2>&1 && command -v docker >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends docker.io
|
||||
command -v docker >/dev/null 2>&1 ||
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends docker-cli
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y --allowerasing docker
|
||||
dnf clean all
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
pacman -Sy --noconfirm --needed docker
|
||||
rm -rf /var/cache/pacman/pkg/*
|
||||
else
|
||||
echo "no supported package manager to install a container engine with" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "the container engine install left no docker client on PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_engine
|
||||
systemctl enable --now docker.service
|
||||
@@ -4,10 +4,10 @@
|
||||
# Installs systemd into whatever it runs in: the Dockerfile beside it RUNs it
|
||||
# in a build stage, never run it on a host.
|
||||
#
|
||||
# No container engine here: a machine that already carries the distribution's
|
||||
# engine kills sys-svc-container's docker.service restart with "failed to load
|
||||
# listeners: no sockets found via socket activation". engine.sh adds one at
|
||||
# replay time for the blocks that need it before any deploy.
|
||||
# No container engine here: the replayed deploy installs docker-ce itself, and a
|
||||
# target that already carries the distribution's engine kills that deploy's
|
||||
# docker.service restart with "failed to load listeners: no sockets found via
|
||||
# socket activation".
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
# $1 the prepared production block
|
||||
#
|
||||
# Env (test.env):
|
||||
# DOCS_TEST_ROLE role under replay, names the compose project
|
||||
# DOCS_TEST_SRC_DIR repository checkout, mounted into the machine
|
||||
# DOCS_TEST_WORK work directory, mounted at the same path on both sides
|
||||
# DOCS_TEST_ROLE role under replay, names the compose project
|
||||
# DOCS_TEST_SRC_DIR repository checkout, holds the generated .env
|
||||
# DOCS_TEST_WORK work directory, holds the replay key pair
|
||||
# DOCS_TEST_REPLAY_PUBKEY public half, authorized for root on the target
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -15,24 +16,26 @@ BLOCK="${1:?}"
|
||||
: "${DOCS_TEST_ROLE:?}"
|
||||
: "${DOCS_TEST_SRC_DIR:?}"
|
||||
: "${DOCS_TEST_WORK:?}"
|
||||
: "${DOCS_TEST_REPLAY_PUBKEY:?}"
|
||||
|
||||
STAGE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_PREFIX="docs-guide-"
|
||||
PROJECT="${PROJECT_PREFIX}${DOCS_TEST_ROLE}"
|
||||
PREPARE="scripts/tests/workspace/base/01_install.sh"
|
||||
SYSTEMD_TRIES=40
|
||||
SYSTEMD_DELAY=3
|
||||
SSH_TRIES=30
|
||||
SSH_DELAY=2
|
||||
|
||||
machine() {
|
||||
docker compose --project-directory "${STAGE}" -p "${PROJECT}" "$@"
|
||||
}
|
||||
|
||||
in_machine() {
|
||||
machine exec -T -w "${DOCS_TEST_SRC_DIR}" machine "$@"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
machine down -v --rmi local --remove-orphans >/dev/null 2>&1
|
||||
rm -f "${REPLAY_KEY}" "${REPLAY_KEY}.pub"
|
||||
if [ "${RECIPE_WORKDIR_PREEXISTED}" = "0" ]; then
|
||||
rm -rf "${RECIPE_WORKDIR}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -60,52 +63,68 @@ await_systemd() {
|
||||
case "${state}" in running | degraded) return 0 ;; esac
|
||||
sleep "${SYSTEMD_DELAY}"
|
||||
done
|
||||
echo "systemd never came up in the ${DOCS_TEST_ROLE} machine (last: ${state:-unknown})" >&2
|
||||
echo "systemd never came up in the ${DOCS_TEST_ROLE} target (last: ${state:-unknown})" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
await_engine() {
|
||||
local state=""
|
||||
for _ in $(seq 1 "${SYSTEMD_TRIES}"); do
|
||||
state="$(machine exec -T machine docker version --format '{{.Server.Version}}' 2>&1 || true)"
|
||||
case "${state}" in [0-9]*) return 0 ;; esac
|
||||
sleep "${SYSTEMD_DELAY}"
|
||||
target_address() {
|
||||
container inspect --type container "$(machine ps -q machine)" \
|
||||
--format '{{.NetworkSettings.IPAddress}}'
|
||||
}
|
||||
|
||||
await_ssh() {
|
||||
local address="$1"
|
||||
for _ in $(seq 1 "${SSH_TRIES}"); do
|
||||
if ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes \
|
||||
-i "${REPLAY_KEY}" "root@${address}" true >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep "${SSH_DELAY}"
|
||||
done
|
||||
echo "no container engine answered in the ${DOCS_TEST_ROLE} machine (last: ${state:-unknown})" >&2
|
||||
echo "sshd never answered on the ${DOCS_TEST_ROLE} target at ${address}" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
nests_a_container() {
|
||||
grep -qE '^docker run ' "${BLOCK}"
|
||||
}
|
||||
|
||||
if grep -q '^git clone ' "${BLOCK}"; then
|
||||
echo "=== Dropping the clone; ${DOCS_TEST_SRC_DIR} is the tree under test ==="
|
||||
sed -i -E '/^git clone /d; /^cd core$/d' "${BLOCK}"
|
||||
workdir="${DOCS_TEST_SRC_DIR}"
|
||||
REPLAY_KEY="${DOCS_TEST_WORK}/docs-guide-${DOCS_TEST_ROLE}-key"
|
||||
RECIPE_WORKDIR="${HOME}/.infinito.nexus"
|
||||
if [ -e "${RECIPE_WORKDIR}" ]; then
|
||||
RECIPE_WORKDIR_PREEXISTED=1
|
||||
else
|
||||
workdir="${DOCS_TEST_WORK}"
|
||||
RECIPE_WORKDIR_PREEXISTED=0
|
||||
fi
|
||||
|
||||
if ! grep -qE '^docker run ' "${BLOCK}"; then
|
||||
echo "the ${DOCS_TEST_ROLE} block starts no container; this harness replays it on the stack host and would configure the host itself" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MACHINE_BASE_IMAGE="$(base_image)"
|
||||
export MACHINE_BASE_IMAGE
|
||||
|
||||
echo "=== Machine: ${MACHINE_BASE_IMAGE} ==="
|
||||
echo "=== Target: ${MACHINE_BASE_IMAGE} ==="
|
||||
sweep_dead_machines
|
||||
trap cleanup EXIT
|
||||
machine up -d --build --pull always
|
||||
echo "=== Machine base digest: $(container image inspect "${MACHINE_BASE_IMAGE}" --format '{{index .RepoDigests 0}}' 2>/dev/null || echo unresolved) ==="
|
||||
await_systemd
|
||||
|
||||
if nests_a_container; then
|
||||
echo "=== Giving it the engine the block's own docker run needs ==="
|
||||
machine exec -T machine sh -s <"${STAGE}/engine.sh"
|
||||
await_engine
|
||||
echo "=== Giving the target an ssh server the published recipe can reach ==="
|
||||
machine exec -T -e DOCS_TEST_REPLAY_PUBKEY machine sh -s <"${STAGE}/sshd.sh"
|
||||
|
||||
TARGET_ADDRESS="$(target_address)"
|
||||
if [ -z "${TARGET_ADDRESS}" ]; then
|
||||
echo "the ${DOCS_TEST_ROLE} target reported no bridge address" >&2
|
||||
exit 1
|
||||
fi
|
||||
await_ssh "${TARGET_ADDRESS}"
|
||||
|
||||
sed -i "s#^HOST=.*#HOST=${TARGET_ADDRESS}#" "${BLOCK}"
|
||||
|
||||
if grep -q '<your-' "${BLOCK}"; then
|
||||
echo "ERROR: the instructions carry a placeholder this test does not fill:" >&2
|
||||
grep -o '<your-[a-z-]*>' "${BLOCK}" | sort -u >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Preparing it for Infinito.Nexus (${PREPARE}) ==="
|
||||
in_machine bash "${PREPARE}"
|
||||
|
||||
echo "=== Replay image: $(sed -nE 's#^[[:space:]]*([^[:space:]]+) bash -c .*#\1#p' "${BLOCK}" | head -1 || true) ==="
|
||||
echo "=== Replaying the instructions in that machine ==="
|
||||
machine exec -T -w "${workdir}" machine bash -e -s <"${BLOCK}"
|
||||
echo "=== Replaying the instructions against ${TARGET_ADDRESS} ==="
|
||||
(cd "${DOCS_TEST_WORK}" && bash -e "${BLOCK}")
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# Env:
|
||||
# DOCS_TEST_REPLAY_PUBKEY public half of the replay key pair, authorized for root
|
||||
|
||||
set -e
|
||||
|
||||
: "${DOCS_TEST_REPLAY_PUBKEY:?}"
|
||||
|
||||
install_sshd() {
|
||||
if command -v sshd >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y openssh-server
|
||||
dnf clean all
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
pacman -Sy --noconfirm --needed openssh
|
||||
rm -rf /var/cache/pacman/pkg/*
|
||||
else
|
||||
echo "no supported package manager to install an ssh server with" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v sshd >/dev/null 2>&1; then
|
||||
echo "the ssh server install left no sshd on PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_sshd
|
||||
ssh-keygen -A
|
||||
|
||||
mkdir -p /root/.ssh
|
||||
chmod 700 /root/.ssh
|
||||
printf '%s\n' "${DOCS_TEST_REPLAY_PUBKEY}" >/root/.ssh/authorized_keys
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
|
||||
mkdir -p /etc/ssh/sshd_config.d
|
||||
printf 'PermitRootLogin prohibit-password\nPubkeyAuthentication yes\n' >/etc/ssh/sshd_config.d/00-docs-replay.conf
|
||||
grep -q '^Include /etc/ssh/sshd_config.d/\*.conf' /etc/ssh/sshd_config ||
|
||||
sed -i '1i Include /etc/ssh/sshd_config.d/*.conf' /etc/ssh/sshd_config
|
||||
|
||||
systemctl enable --now ssh.service 2>/dev/null || systemctl enable --now sshd.service
|
||||
@@ -46,16 +46,12 @@ bash -n "${BLOCK}"
|
||||
rm -f "${REPLAY_KEY}" "${REPLAY_KEY}.pub"
|
||||
ssh-keygen -q -t ed25519 -N "" -C "docs-guide@infinito" -f "${REPLAY_KEY}"
|
||||
|
||||
sed -i "s#<your-ssh-public-key>#$(cat "${REPLAY_KEY}.pub")#g" "${BLOCK}"
|
||||
DOCS_TEST_REPLAY_PUBKEY="$(cat "${REPLAY_KEY}.pub")"
|
||||
export DOCS_TEST_REPLAY_PUBKEY
|
||||
|
||||
sed -i "s#<your-ssh-public-key>#${DOCS_TEST_REPLAY_PUBKEY}#g" "${BLOCK}"
|
||||
sed -i "s#<your-ssh-private-key-file>#${REPLAY_KEY}#g" "${BLOCK}"
|
||||
sed -i "s#<your-domain>#${DOCS_TEST_DOMAIN}#g" "${BLOCK}"
|
||||
sed -i "s#<your-clearnet-resolver>#${DOCS_TEST_DNS:-}#g" "${BLOCK}"
|
||||
sed -i "s#^HOST=.*#HOST=localhost#" "${BLOCK}"
|
||||
|
||||
if grep -q '<your-' "${BLOCK}"; then
|
||||
echo "ERROR: the instructions carry a placeholder this test does not fill:" >&2
|
||||
grep -o '<your-[a-z-]*>' "${BLOCK}" | sort -u >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bash "${STAGE}/machine.sh" "${BLOCK}"
|
||||
Reference in new issue
Block a user