207 lines
11 KiB
Bash
Executable File
207 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
DOCKER_SMOKE_RUN_ID="${DOCKER_SMOKE_RUN_ID:-$$}"
|
|
if [[ -z "$DOCKER_SMOKE_RUN_ID" || ${#DOCKER_SMOKE_RUN_ID} -gt 80 || ! $DOCKER_SMOKE_RUN_ID =~ ^[A-Za-z0-9_.-]+$ ]]; then
|
|
echo "DOCKER_SMOKE_RUN_ID must contain 1-80 Docker-safe characters" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# shellcheck source=scripts/lib/docker-runtime-smoke.sh
|
|
source "$(dirname "${BASH_SOURCE[0]}")/lib/docker-runtime-smoke.sh"
|
|
|
|
IMAGE="${1:-hivegrid-portal-domain-controller:smoke}"
|
|
CONTAINER_NAME="${DOCKER_PORTAL_DOMAIN_CONTROLLER_SMOKE_CONTAINER:-hivegrid-portal-domain-controller-smoke-${DOCKER_SMOKE_RUN_ID}}"
|
|
REDIS_CONTAINER_NAME="${DOCKER_PORTAL_DOMAIN_CONTROLLER_SMOKE_REDIS_CONTAINER:-hivegrid-portal-domain-controller-redis-${DOCKER_SMOKE_RUN_ID}}"
|
|
NETWORK_NAME="${DOCKER_PORTAL_DOMAIN_CONTROLLER_SMOKE_NETWORK:-hivegrid-portal-domain-controller-smoke-${DOCKER_SMOKE_RUN_ID}}"
|
|
CONFIG_VOLUME="${DOCKER_PORTAL_DOMAIN_CONTROLLER_SMOKE_VOLUME:-hivegrid-portal-domain-controller-config-${DOCKER_SMOKE_RUN_ID}}"
|
|
RELOAD_TRIGGER_VOLUME="${DOCKER_PORTAL_DOMAIN_CONTROLLER_SMOKE_RELOAD_VOLUME:-hivegrid-portal-domain-controller-reload-${DOCKER_SMOKE_RUN_ID}}"
|
|
ASSERT_CONTAINER_NAME="${DOCKER_PORTAL_DOMAIN_CONTROLLER_SMOKE_ASSERT_CONTAINER:-hivegrid-portal-domain-controller-assert-${DOCKER_SMOKE_RUN_ID}}"
|
|
CONFIG_INIT_CONTAINER_NAME="${DOCKER_PORTAL_DOMAIN_CONTROLLER_SMOKE_CONFIG_INIT_CONTAINER:-hivegrid-portal-domain-controller-config-init-${DOCKER_SMOKE_RUN_ID}}"
|
|
RELOAD_INIT_CONTAINER_NAME="${DOCKER_PORTAL_DOMAIN_CONTROLLER_SMOKE_RELOAD_INIT_CONTAINER:-hivegrid-portal-domain-controller-reload-init-${DOCKER_SMOKE_RUN_ID}}"
|
|
EXPECTED_REVISION="${DOCKER_PORTAL_DOMAIN_CONTROLLER_SMOKE_REVISION:-}"
|
|
COMMAND_SECRET="smoke-command-secret-that-is-not-a-production-secret"
|
|
METRICS_TOKEN="smoke-metrics-token-that-is-not-a-production-token"
|
|
|
|
cleanup() {
|
|
docker_smoke_cleanup_container "portal-controller:cleanup-containers" \
|
|
"$CONTAINER_NAME" "$REDIS_CONTAINER_NAME" "$ASSERT_CONTAINER_NAME" \
|
|
"$CONFIG_INIT_CONTAINER_NAME" "$RELOAD_INIT_CONTAINER_NAME"
|
|
docker_smoke_cleanup_network "portal-controller:cleanup-network" "$NETWORK_NAME"
|
|
docker_smoke_cleanup_volume "portal-controller:cleanup-volumes" "$CONFIG_VOLUME" "$RELOAD_TRIGGER_VOLUME"
|
|
}
|
|
|
|
http_status() {
|
|
local path="$1"
|
|
local token="${2:-}"
|
|
local code
|
|
|
|
if ! docker_smoke_capture code "portal-controller:http-status" "$DOCKER_SMOKE_PROBE_TIMEOUT_SECONDS" \
|
|
docker exec "$CONTAINER_NAME" node -e '
|
|
const [path, token] = process.argv.slice(1);
|
|
const headers = token ? { authorization: `Bearer ${token}` } : {};
|
|
fetch(`http://127.0.0.1:8080${path}`, { headers })
|
|
.then((response) => { process.stdout.write(String(response.status)); })
|
|
.catch(() => { process.stdout.write("000"); });
|
|
' "$path" "$token" 2>/dev/null; then
|
|
code="000"
|
|
fi
|
|
|
|
printf "%s" "$code"
|
|
}
|
|
|
|
expect_status() {
|
|
local path="$1"
|
|
local expected="$2"
|
|
local token="${3:-}"
|
|
local actual
|
|
|
|
actual="$(http_status "$path" "$token")"
|
|
if [[ "$actual" != "$expected" ]]; then
|
|
echo "Portal-domain controller ${path} returned ${actual:-<empty>}, expected ${expected}" >&2
|
|
docker_smoke_logs "portal-controller:logs-status" "$CONTAINER_NAME" >&2 || true
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
docker_smoke_install_cleanup_traps cleanup
|
|
cleanup
|
|
|
|
docker_smoke_capture IMAGE_USER "portal-controller:inspect-user" "$DOCKER_SMOKE_PROBE_TIMEOUT_SECONDS" \
|
|
docker image inspect -f '{{ .Config.User }}' "$IMAGE"
|
|
if [[ "$IMAGE_USER" != "node" && "$IMAGE_USER" != "1000" && "$IMAGE_USER" != "1000:1000" ]]; then
|
|
echo "Portal-domain controller image must declare the non-root node user, got ${IMAGE_USER:-<empty>}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
docker_smoke_capture IMAGE_REVISION "portal-controller:inspect-revision" "$DOCKER_SMOKE_PROBE_TIMEOUT_SECONDS" \
|
|
docker image inspect -f '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$IMAGE"
|
|
if [[ ! "$IMAGE_REVISION" =~ ^[0-9a-fA-F]{40}$ ]]; then
|
|
echo "Portal-domain controller image does not expose a concrete org.opencontainers.image.revision" >&2
|
|
exit 1
|
|
fi
|
|
if [[ -n "$EXPECTED_REVISION" && "$IMAGE_REVISION" != "$EXPECTED_REVISION" ]]; then
|
|
echo "Portal-domain controller image revision ${IMAGE_REVISION:-<empty>} does not match ${EXPECTED_REVISION}" >&2
|
|
exit 1
|
|
fi
|
|
EXPECTED_REVISION="$IMAGE_REVISION"
|
|
|
|
docker_smoke_run "portal-controller:assert-runtime" "$DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS" docker run --rm --name "$ASSERT_CONTAINER_NAME" --entrypoint node "$IMAGE" -e '
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
if (process.getuid?.() !== 1000) throw new Error(`expected uid 1000, got ${process.getuid?.()}`);
|
|
if (!fs.existsSync("/usr/bin/flock")) throw new Error("runtime is missing the kernel flock binary");
|
|
for (const executable of ["/usr/local/bin/corepack", "/usr/local/bin/npm", "/usr/local/bin/npx", "/usr/local/bin/pnpm", "/usr/local/bin/pnpx", "/usr/local/bin/yarn"]) {
|
|
if (fs.existsSync(executable)) throw new Error(`runtime retained package manager ${executable}`);
|
|
}
|
|
for (const root of ["/myapp/apps/portal-domain-controller/dist", "/myapp/node_modules"]) {
|
|
const pending = [root];
|
|
while (pending.length > 0) {
|
|
const current = pending.pop();
|
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
const absolute = path.join(current, entry.name);
|
|
if (entry.isDirectory() && !entry.isSymbolicLink()) pending.push(absolute);
|
|
if (entry.name.endsWith(".map") || entry.name.endsWith(".d.ts")) throw new Error(`runtime retained source artifact ${absolute}`);
|
|
}
|
|
}
|
|
}
|
|
const sbom = JSON.parse(fs.readFileSync("/myapp/sbom-pnpm.json", "utf8"));
|
|
if (sbom.schema !== "hivegrid.sbom-pnpm/v2" || sbom.target !== "portal-domain-controller") throw new Error("wrong controller SBOM target");
|
|
if (sbom.source !== "installed pruned pnpm runtime tree" || !Array.isArray(sbom.packages) || sbom.packageCount !== sbom.packages.length) throw new Error("invalid controller SBOM inventory");
|
|
const packages = new Set(sbom.packages.map((entry) => entry.name));
|
|
for (const required of ["bullmq", "ioredis", "pino", "prom-client"]) if (!packages.has(required)) throw new Error(`SBOM missing ${required}`);
|
|
for (const forbidden of ["@prisma/client", "pg", "prisma", "tsx", "typescript", "vitest", "esbuild"]) if (packages.has(forbidden)) throw new Error(`SBOM retained forbidden package ${forbidden}`);
|
|
' >/dev/null
|
|
|
|
docker_smoke_run "portal-controller:create-network" "$DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS" \
|
|
docker network create "$NETWORK_NAME" >/dev/null
|
|
docker_smoke_run "portal-controller:create-config-volume" "$DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS" \
|
|
docker volume create "$CONFIG_VOLUME" >/dev/null
|
|
docker_smoke_run "portal-controller:create-reload-volume" "$DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS" \
|
|
docker volume create "$RELOAD_TRIGGER_VOLUME" >/dev/null
|
|
docker_smoke_run "portal-controller:init-config-volume" "$DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS" docker run --rm --name "$CONFIG_INIT_CONTAINER_NAME" --user 0 --entrypoint node \
|
|
-v "$CONFIG_VOLUME:/data/coolify/proxy/dynamic/hivegrid-portal-domains" \
|
|
"$IMAGE" -e 'require("node:fs").chownSync("/data/coolify/proxy/dynamic/hivegrid-portal-domains", 1000, 1000)' >/dev/null
|
|
docker_smoke_run "portal-controller:init-reload-volume" "$DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS" docker run --rm --name "$RELOAD_INIT_CONTAINER_NAME" --user 0 --entrypoint node \
|
|
-v "$RELOAD_TRIGGER_VOLUME:/sentinel" \
|
|
"$IMAGE" -e '
|
|
const fs = require("node:fs");
|
|
const file = "/sentinel/.hivegrid-portal-domains.reload";
|
|
fs.writeFileSync(file, "ready\n", { mode: 0o600, flag: "wx" });
|
|
fs.chownSync(file, 1000, 1000);
|
|
fs.chmodSync(file, 0o600);
|
|
' >/dev/null
|
|
docker_smoke_run "portal-controller:start-redis" "$DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS" \
|
|
docker run -d --name "$REDIS_CONTAINER_NAME" --network "$NETWORK_NAME" --network-alias redis redis:7-alpine >/dev/null
|
|
|
|
for attempt in $(seq 1 30); do
|
|
if docker_smoke_probe "portal-controller:probe-redis" docker exec "$REDIS_CONTAINER_NAME" redis-cli ping 2>/dev/null | grep -q PONG; then break; fi
|
|
if [[ "$attempt" == "30" ]]; then
|
|
echo "Controller smoke Redis did not become ready" >&2
|
|
docker_smoke_logs "portal-controller:logs-redis" "$REDIS_CONTAINER_NAME" >&2 || true
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
docker_smoke_run "portal-controller:start-runtime" "$DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS" docker run -d --name "$CONTAINER_NAME" --network "$NETWORK_NAME" \
|
|
-v "$CONFIG_VOLUME:/data/coolify/proxy/dynamic/hivegrid-portal-domains" \
|
|
--mount "type=volume,source=$RELOAD_TRIGGER_VOLUME,destination=/data/coolify/proxy/dynamic/.hivegrid-portal-domains.reload,volume-subpath=.hivegrid-portal-domains.reload" \
|
|
-e PORTAL_DOMAIN_CONTROLLER_REDIS_URL="redis://redis:6379/0" \
|
|
-e PORTAL_DOMAIN_CONTROLLER_COMMAND_SECRET="$COMMAND_SECRET" \
|
|
-e PORTAL_DOMAIN_CONTROLLER_METRICS_TOKEN="$METRICS_TOKEN" \
|
|
-e PORTAL_DOMAIN_CONTROLLER_EXPECTED_CNAME="portal-ingress.tool-ctrl.de" \
|
|
-e PORTAL_DOMAIN_CONTROLLER_ENTRYPOINT="https" \
|
|
-e PORTAL_DOMAIN_CONTROLLER_BACKEND_SERVICE="hivegrid-portal@docker" \
|
|
-e COOLIFY_BRANCH="main" \
|
|
-e COOLIFY_CONTAINER_NAME="hivegrid-portal-domain-controller-smoke" \
|
|
-e COOLIFY_FQDN="" \
|
|
-e COOLIFY_RESOURCE_UUID="joyyh5b89vpfp8ocri8xr96m" \
|
|
-e COOLIFY_URL="" \
|
|
-e GIT_SHA="$EXPECTED_REVISION" \
|
|
"$IMAGE" >/dev/null
|
|
|
|
for attempt in $(seq 1 60); do
|
|
if [[ "$(http_status /readyz)" == "200" ]]; then break; fi
|
|
if [[ "$attempt" == "60" ]]; then
|
|
echo "Portal-domain controller did not become ready" >&2
|
|
docker_smoke_logs "portal-controller:logs-readiness" "$CONTAINER_NAME" >&2 || true
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
expect_status /healthz 200
|
|
expect_status /readyz 200
|
|
expect_status /metrics 401
|
|
expect_status /metrics 200 "$METRICS_TOKEN"
|
|
|
|
docker_smoke_run "portal-controller:assert-health-payload" "$DOCKER_SMOKE_PROBE_TIMEOUT_SECONDS" docker exec "$CONTAINER_NAME" node -e '
|
|
fetch("http://127.0.0.1:8080/healthz")
|
|
.then((response) => response.json())
|
|
.then((payload) => {
|
|
if (payload.status !== "ok" || payload.service !== "hivegrid-portal-domain-controller" || payload.revision !== process.argv[1]) process.exit(1);
|
|
});
|
|
' "$EXPECTED_REVISION"
|
|
|
|
docker_smoke_run "portal-controller:deploy-health" "$DOCKER_SMOKE_PROBE_TIMEOUT_SECONDS" \
|
|
docker exec "$CONTAINER_NAME" node apps/portal-domain-controller/dist/deploy-health.js "$EXPECTED_REVISION" >/dev/null
|
|
FOREIGN_REVISION="$(printf 'f%.0s' {1..40})"
|
|
if [[ "$FOREIGN_REVISION" == "$EXPECTED_REVISION" ]]; then
|
|
FOREIGN_REVISION="$(printf 'e%.0s' {1..40})"
|
|
fi
|
|
docker_smoke_expect_docker_exec_rejection "portal-controller:reject-foreign-revision" "$DOCKER_SMOKE_PROBE_TIMEOUT_SECONDS" "$CONTAINER_NAME" \
|
|
node apps/portal-domain-controller/dist/deploy-health.js "$FOREIGN_REVISION"
|
|
|
|
RUNTIME_ENV_KEYS=""
|
|
docker_smoke_capture RUNTIME_ENV_KEYS "portal-controller:inspect-runtime-env" "$DOCKER_SMOKE_PROBE_TIMEOUT_SECONDS" \
|
|
bash -o pipefail -c 'docker inspect -f '\''{{range .Config.Env}}{{println .}}{{end}}'\'' "$1" | sed '\''s/=.*//'\''' \
|
|
_ "$CONTAINER_NAME"
|
|
for forbidden in DATABASE_URL DIRECT_URL DOCKER_HOST COOLIFY_TOKEN CLOUDFLARE_API_TOKEN SSH_PRIVATE_KEY; do
|
|
if grep -qx "$forbidden" <<<"$RUNTIME_ENV_KEYS"; then
|
|
echo "Controller runtime received forbidden credential ${forbidden}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
echo "Portal-domain controller runtime smoke passed for ${IMAGE}"
|