Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/docker-portal-domain-controller-runtime-smoke.sh
T

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}"