diff --git a/.github/workflows/baseimage.yml b/.github/workflows/baseimage.yml index f0eb7d3..32ece71 100644 --- a/.github/workflows/baseimage.yml +++ b/.github/workflows/baseimage.yml @@ -26,6 +26,7 @@ on: paths: - docker/Dockerfile.ci - docker/Dockerfile.ci-nightly + - docker/Dockerfile.mongo-rs - .github/workflows/baseimage.yml push: branches: @@ -33,6 +34,7 @@ on: paths: - docker/Dockerfile.ci - docker/Dockerfile.ci-nightly + - docker/Dockerfile.mongo-rs - .github/workflows/baseimage.yml workflow_dispatch: @@ -106,6 +108,60 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + # + # The mongo service image, and it is a service image rather than a base: + # nothing builds inside it. It is mongo:8.0 with --replSet on the command + # line, because a workflow `services:` block cannot pass a command and the + # HA cache sync needs an oplog to watch. It rides along here because this + # is where a Dockerfile in this repo gets published, not because it has + # anything to do with the build environment. + # + # Same immutable - tag as the other two, from the same run, so + # adopting a set of images is one bump rather than three. + # + - name: Build and push the mongo replica-set image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.mongo-rs + push: ${{ github.event_name != 'pull_request' }} + tags: quay.io/seamware/mongo-rs:${{ steps.tags.outputs.date_sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # + # The property every consumer depends on, asserted here so a consumer does + # not discover it: the image comes up, the set INITIATES, and isMaster then + # reports a setName - which is the exact probe coraine's harness uses to + # decide whether the HA tests are in the run set. + # + - name: Verify the mongo image is an initiable replica set + if: github.event_name != 'pull_request' + run: | + set -eu + img=quay.io/seamware/mongo-rs:${{ steps.tags.outputs.date_sha }} + docker run -d --name mongo-rs-probe "$img" + trap 'docker rm -f mongo-rs-probe >/dev/null 2>&1 || true' EXIT + + for i in $(seq 1 30); do + docker exec mongo-rs-probe mongosh --quiet --eval 'db.runCommand({ping:1}).ok' >/dev/null 2>&1 && break + [ "$i" = 30 ] && { echo "mongod never answered"; exit 1; } + sleep 2 + done + + docker exec mongo-rs-probe mongosh --quiet --eval \ + 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "localhost:27017"}]})' + + for i in $(seq 1 30); do + [ "$(docker exec mongo-rs-probe mongosh --quiet --eval 'db.adminCommand({isMaster:1}).ismaster')" = true ] && break + [ "$i" = 30 ] && { echo "the set never elected a primary"; exit 1; } + sleep 2 + done + + name=$(docker exec mongo-rs-probe mongosh --quiet --eval 'db.adminCommand({isMaster:1}).setName') + [ "$name" = rs0 ] || { echo "setName is '$name', expected rs0"; exit 1; } + echo "single-node replica set rs0 is PRIMARY" + # # The Dockerfile's last layer already asserts the environment is complete. # This repeats it against the built image so the check also covers a @@ -132,6 +188,7 @@ jobs: echo "Published:" echo "- \`quay.io/seamware/${{ env.IMAGE_NAME }}:${{ steps.tags.outputs.date_sha }}\`" echo "- \`quay.io/seamware/${{ env.IMAGE_NAME }}-nightly:${{ steps.tags.outputs.date_sha }}\`" + echo "- \`quay.io/seamware/mongo-rs:${{ steps.tags.outputs.date_sha }}\`" echo echo "To adopt, in coraine: set the image tag in .github/workflows/{ci,nightly}.yml to \`${{ steps.tags.outputs.date_sha }}\`" else diff --git a/docker/Dockerfile.mongo-rs b/docker/Dockerfile.mongo-rs new file mode 100644 index 0000000..c471c4d --- /dev/null +++ b/docker/Dockerfile.mongo-rs @@ -0,0 +1,40 @@ +# Dockerfile.mongo-rs - mongo:8.0, started as a single-node REPLICA SET. +# +# WHAT THIS IS: the stock upstream image with one thing changed - the command. +# No packages, no configuration, no data. If it ever needs a second line of +# content, it is the wrong shape and should be reconsidered. +# +# WHY IT EXISTS: the HA cache sync (`--high-availability mongo`) rides on a +# mongo CHANGE STREAM, and a change stream reads the oplog - which a standalone +# mongod does not have. The harness detects that (corTestParams.sh probes +# isMaster.setName) and leaves ha_cache_sync.test out of the run set on a +# standalone, silently: nothing in the log says the HA paths went unexercised. +# +# Every CI job ran a standalone, so nothing ever entered them - 108 lines and +# 10 functions of coraine, which the published coverage figure then reported as +# untested CODE rather than as an environment we had not stood up. +# +# WHY IT CANNOT BE DONE IN THE WORKFLOW: a GitHub Actions `services:` block +# passes docker-create OPTIONS but not a COMMAND, so `--replSet` cannot be +# appended to mongo:8.0 from a workflow, and the job container has mongosh but +# no mongod of its own to start instead. Two lines here, and every consumer +# keeps using an ordinary service container. +# +# WHAT THIS IMAGE DELIBERATELY DOES NOT DO: initiate the set. A mongod started +# with --replSet and never initiated answers every write with +# NotYetInitialized, so somebody must call rs.initiate() once it is up - but the +# member host has to be the name the CLIENTS use to reach it, and this image +# cannot know that name. It is one mongosh call in the consumer, beside the +# wait-for-database it already does. +# +# ONE MEMBER is the whole point. This is not a test of replication; it is the +# smallest thing that has an oplog. +# +FROM mongo:8.0 + +# +# The upstream entrypoint prepends `mongod` only when the command starts with a +# dash, so the binary is named explicitly. --bind_ip_all because a replica-set +# member is addressed by the name its clients use, not by localhost. +# +CMD ["mongod", "--replSet", "rs0", "--bind_ip_all"]