From a77d4b801c2595a6fcd26488e36317dc370acaac Mon Sep 17 00:00:00 2001 From: KyuubiYoru Date: Fri, 17 Jul 2026 01:53:50 +0200 Subject: [PATCH] fix(ci): isolate deployment smoke client (#1) --- .gitea/workflows/ci.yml | 43 ++++++++++++------- .gitea/workflows/release.yml | 37 ++++++++++------ docs/deployment/linux.md | 8 ++-- .../Release/ReleaseCompatibilityTests.cs | 9 ++-- 4 files changed, 61 insertions(+), 36 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f8d9756..44afe5d 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -144,18 +144,19 @@ jobs: set -euo pipefail compose_file="deploy/compose/compose.yaml" secret="deploy/compose/secrets/signing-key" - compose_network="" cleanup() { - if [[ -n "$compose_network" ]]; then - docker network disconnect "$compose_network" "$HOSTNAME" >/dev/null 2>&1 || true - fi RENDEZVOUS_UID=1654 RENDEZVOUS_GID=1654 \ docker compose -f "$compose_file" down --volumes >/dev/null 2>&1 || true rm -f "$secret" } trap cleanup EXIT + umask 077 install -d -m 0700 deploy/compose/secrets openssl rand -out "$secret" 32 + chmod 0600 "$secret" + publisher_credential="$(RENDEZVOUS_SMOKE_LOCAL_KEY="$secret" \ + ./scripts/mint-local-publisher-credential.sh)" + export RENDEZVOUS_PUBLISHER_CREDENTIAL="$publisher_credential" chmod 0444 "$secret" export RENDEZVOUS_UID=1654 export RENDEZVOUS_GID=1654 @@ -168,6 +169,10 @@ jobs: export RENDEZVOUS_CONFIG_SOURCE="$runner_workspace_source/deploy/compose/appsettings.Production.json" export RENDEZVOUS_SECRET_SOURCE="$runner_workspace_source/deploy/compose/secrets/signing-key" export SOURCE_REVISION_ID="$GITHUB_SHA" + smoke_client_image="rendezvous-smoke-client:${GITHUB_SHA}" + docker build --file eng/release-builder.Dockerfile \ + --target release-builder \ + --tag "$smoke_client_image" . docker compose -f "$compose_file" build \ --build-arg SOURCE_REVISION_ID="$SOURCE_REVISION_ID" docker compose -f "$compose_file" up --no-build --detach @@ -175,27 +180,33 @@ jobs: test -n "$container_id" compose_network="$(docker inspect "$container_id" | jq -er \ '.[0].NetworkSettings.Networks | keys[0]')" - docker network connect "$compose_network" "$HOSTNAME" service_ip="$(docker inspect "$container_id" | jq -er \ --arg network "$compose_network" \ '.[0].NetworkSettings.Networks[$network].IPAddress')" - export RENDEZVOUS_SMOKE_HTTP_URL="http://${service_ip}:8080/" - export RENDEZVOUS_SMOKE_UDP_ENDPOINT="${service_ip}:9050" test "$(docker inspect --format '{{.Config.User}}' "$container_id")" = "1654:1654" test "$(docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' "$container_id")" = "true" test "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/app/appsettings.Production.json"}}{{.RW}}{{end}}{{end}}' "$container_id")" = "false" test "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/run/secrets/rendezvous-signing-key"}}{{.RW}}{{end}}{{end}}' "$container_id")" = "false" - for attempt in {1..100}; do - if curl --fail --silent "${RENDEZVOUS_SMOKE_HTTP_URL%/}/health/ready" >/dev/null 2>&1; then - break - fi - if (( attempt == 100 )); then + docker run --rm \ + --network "$compose_network" \ + --user "$(id -u):$(id -g)" \ + --env HOME=/tmp \ + --env RENDEZVOUS_SMOKE_HTTP_URL="http://${service_ip}:8080/" \ + --env RENDEZVOUS_SMOKE_UDP_ENDPOINT="${service_ip}:9050" \ + --env RENDEZVOUS_PUBLISHER_CREDENTIAL \ + --volume "$runner_workspace_source:/source:ro" \ + --workdir /source \ + "$smoke_client_image" \ + bash -lc ' + for attempt in {1..300}; do + curl --fail --silent "${RENDEZVOUS_SMOKE_HTTP_URL%/}/health/ready" >/dev/null 2>&1 && exec ./scripts/smoke-deployment.sh + sleep 0.1 + done + exit 1 + ' || { docker compose -f "$compose_file" logs rendezvous exit 1 - fi - sleep 0.1 - done - ./scripts/smoke-deployment.sh + } docker compose -f "$compose_file" stop --timeout 40 rendezvous test "$(docker inspect --format '{{.State.Running}}' "$container_id")" = "false" test "$(docker inspect --format '{{.State.ExitCode}}' "$container_id")" = "0" diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index b6b43ba..8cce815 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -112,18 +112,19 @@ jobs: run: | set -euo pipefail secret="deploy/compose/secrets/signing-key" - compose_network="" cleanup() { - if [[ -n "$compose_network" ]]; then - docker network disconnect "$compose_network" "$HOSTNAME" >/dev/null 2>&1 || true - fi RENDEZVOUS_UID=1654 RENDEZVOUS_GID=1654 RENDEZVOUS_IMAGE="git.finalfactory.de/heikyu/rendezvous:${RENDEZVOUS_VERSION}" \ docker compose -f deploy/compose/compose.yaml down --volumes >/dev/null 2>&1 || true rm -f "$secret" } trap cleanup EXIT + umask 077 install -d -m 0700 deploy/compose/secrets openssl rand -out "$secret" 32 + chmod 0600 "$secret" + publisher_credential="$(RENDEZVOUS_SMOKE_LOCAL_KEY="$secret" \ + ./scripts/mint-local-publisher-credential.sh)" + export RENDEZVOUS_PUBLISHER_CREDENTIAL="$publisher_credential" chmod 0444 "$secret" export RENDEZVOUS_UID=1654 export RENDEZVOUS_GID=1654 @@ -141,21 +142,29 @@ jobs: test -n "$container_id" compose_network="$(docker inspect "$container_id" | jq -er \ '.[0].NetworkSettings.Networks | keys[0]')" - docker network connect "$compose_network" "$HOSTNAME" service_ip="$(docker inspect "$container_id" | jq -er \ --arg network "$compose_network" \ '.[0].NetworkSettings.Networks[$network].IPAddress')" - export RENDEZVOUS_SMOKE_HTTP_URL="http://${service_ip}:8080/" - export RENDEZVOUS_SMOKE_UDP_ENDPOINT="${service_ip}:9050" - for attempt in {1..100}; do - curl --fail --silent "${RENDEZVOUS_SMOKE_HTTP_URL%/}/health/ready" >/dev/null 2>&1 && break - if (( attempt == 100 )); then + docker run --rm \ + --network "$compose_network" \ + --user "$(id -u):$(id -g)" \ + --env HOME=/tmp \ + --env RENDEZVOUS_SMOKE_HTTP_URL="http://${service_ip}:8080/" \ + --env RENDEZVOUS_SMOKE_UDP_ENDPOINT="${service_ip}:9050" \ + --env RENDEZVOUS_PUBLISHER_CREDENTIAL \ + --volume "$runner_workspace_source:/source:ro" \ + --workdir /source \ + "$RENDEZVOUS_RELEASE_BUILDER" \ + bash -lc ' + for attempt in {1..300}; do + curl --fail --silent "${RENDEZVOUS_SMOKE_HTTP_URL%/}/health/ready" >/dev/null 2>&1 && exec ./scripts/smoke-deployment.sh + sleep 0.1 + done + exit 1 + ' || { docker compose -f deploy/compose/compose.yaml logs rendezvous exit 1 - fi - sleep 0.1 - done - ./scripts/smoke-deployment.sh + } - name: Scan candidate for high and critical vulnerabilities uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0, post-incident safe SHA diff --git a/docs/deployment/linux.md b/docs/deployment/linux.md index 90e1a68..0f84e14 100644 --- a/docs/deployment/linux.md +++ b/docs/deployment/linux.md @@ -49,9 +49,11 @@ Compose, then point `RENDEZVOUS_SMOKE_HTTP_URL` and and production-advertised service ports remain 8080/9050. Sibling-container CI runners may also set `RENDEZVOUS_CONFIG_SOURCE` and `RENDEZVOUS_SECRET_SOURCE` to host-visible absolute bind-source paths; local -operators should normally keep the checked-in relative defaults. CI attaches -its ephemeral job container to the isolated Compose network for smoke traffic; -it does not widen the default loopback HTTP publication. +operators should normally keep the checked-in relative defaults. CI starts its +ephemeral smoke client directly on the isolated Compose network; it does not +widen the default loopback HTTP publication. It mints the disposable publisher +credential while the generated key is still owner-private, then makes the key +read-only for the non-root service container. `deploy/compose/appsettings.Production.json` is a local/private-bridge smoke profile, not an Internet template: TCP is published only on host loopback, the diff --git a/tests/FinalFactory.Rendezvous.Tests/Release/ReleaseCompatibilityTests.cs b/tests/FinalFactory.Rendezvous.Tests/Release/ReleaseCompatibilityTests.cs index db5b0e9..31c6012 100644 --- a/tests/FinalFactory.Rendezvous.Tests/Release/ReleaseCompatibilityTests.cs +++ b/tests/FinalFactory.Rendezvous.Tests/Release/ReleaseCompatibilityTests.cs @@ -179,10 +179,13 @@ public sealed class ReleaseCompatibilityTests string workflow = File.ReadAllText(Path.Combine(root, ".gitea/workflows", workflowName)); Assert.Contains("GITHUB_RUN_ID", workflow, StringComparison.Ordinal); Assert.Contains("compose_network", workflow, StringComparison.Ordinal); - Assert.Contains("docker network connect", workflow, StringComparison.Ordinal); - Assert.Contains("docker network disconnect", workflow, StringComparison.Ordinal); - Assert.Contains("\"$compose_network\" \"$HOSTNAME\"", workflow, StringComparison.Ordinal); + Assert.Contains("--network \"$compose_network\"", workflow, StringComparison.Ordinal); + Assert.Contains("publisher_credential", workflow, StringComparison.Ordinal); + Assert.Contains("RENDEZVOUS_PUBLISHER_CREDENTIAL", workflow, StringComparison.Ordinal); + Assert.Contains("for attempt in {1..300}", workflow, StringComparison.Ordinal); Assert.Contains("service_ip", workflow, StringComparison.Ordinal); + Assert.Contains("http://${service_ip}:8080/", workflow, StringComparison.Ordinal); + Assert.Contains("${service_ip}:9050", workflow, StringComparison.Ordinal); Assert.Contains("RENDEZVOUS_HTTP_HOST_PORT", workflow, StringComparison.Ordinal); Assert.Contains("RENDEZVOUS_UDP_HOST_PORT", workflow, StringComparison.Ordinal); Assert.Contains("RENDEZVOUS_SMOKE_HTTP_URL", workflow, StringComparison.Ordinal);