diff --git a/Dockerfile b/Dockerfile index 6ec69d1..fc0910e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ FROM docker:stable COPY start-mongodb.sh /start-mongodb.sh -RUN chmod +x /start-mongodb.sh +COPY stop-mongodb.sh /stop-mongodb.sh +RUN chmod +x /start-mongodb.sh /stop-mongodb.sh ENTRYPOINT ["/start-mongodb.sh"] diff --git a/action-types.yml b/action-types.yml index b03fcf8..e567ea3 100644 --- a/action-types.yml +++ b/action-types.yml @@ -16,3 +16,13 @@ inputs: type: string mongodb-container-name: type: string + mongodb-key: + type: string + mongodb-authsource: + type: string + mongodb-replica-set-host: + type: string + docker-network: + type: string + docker-network-alias: + type: string diff --git a/action.yml b/action.yml index 2b237cb..bd0b05f 100644 --- a/action.yml +++ b/action.yml @@ -12,7 +12,7 @@ inputs: default: 'mongo' mongodb-version: - description: 'MongoDB version to use (default "latest")' + description: 'MongoDB version to use (default: "latest")' required: false default: 'latest' @@ -22,7 +22,7 @@ inputs: default: '' mongodb-port: - description: 'MongoDB port to use (default 27017)' + description: 'MongoDB port to use (default: 27017)' required: false default: 27017 @@ -46,6 +46,31 @@ inputs: required: false default: 'mongodb' + mongodb-key: + description: 'MongoDB key, required if replica set and auth are setup through username and password (no key set by default)' + required: false + default: '' + + mongodb-authsource: + description: 'MongoDB authenticationDatabase a.k.a authSource to use (default: "admin" based on https://github.com/docker-library/mongo/blob/master/8.0/docker-entrypoint.sh#L372C4-L372C20)' + required: false + default: 'admin' + + mongodb-replica-set-host: + description: 'MongoDB replica set host, must be accessible from both internal container and external usage (default: "localhost")' + required: false + default: 'localhost' + + docker-network: + description: 'Docker network to attach the MongoDB container to. If not provided, will try to use the default GitHub Actions network if available (github_network_).' + required: false + default: '' + + docker-network-alias: + description: 'Network alias for the MongoDB container when attaching to a Docker network. If not provided, will use mongodb-container-name input' + required: false + default: '' + runs: using: 'docker' image: 'Dockerfile' @@ -58,3 +83,9 @@ runs: - ${{ inputs.mongodb-username }} - ${{ inputs.mongodb-password }} - ${{ inputs.mongodb-container-name }} + - ${{ inputs.mongodb-key }} + - ${{ inputs.mongodb-authsource }} + - ${{ inputs.mongodb-replica-set-host }} + - ${{ inputs.docker-network }} + - ${{ inputs.docker-network-alias }} + post-entrypoint: /stop-mongodb.sh diff --git a/start-mongodb.sh b/start-mongodb.sh old mode 100644 new mode 100755 index 50f3f9a..b34b56b --- a/start-mongodb.sh +++ b/start-mongodb.sh @@ -9,6 +9,33 @@ MONGODB_DB=$5 MONGODB_USERNAME=$6 MONGODB_PASSWORD=$7 MONGODB_CONTAINER_NAME=$8 +MONGODB_KEY=$9 +MONGODB_AUTHSOURCE=${10} +MONGODB_REPLICA_SET_HOST=${11:-"localhost"} +DOCKER_NETWORK=${12} +DOCKER_NETWORK_ALIAS=${13:-$MONGODB_CONTAINER_NAME} + +# If DOCKER_NETWORK not provided, try to detect the default GitHub Actions network +if [ -z "$DOCKER_NETWORK" ]; then + DOCKER_NETWORK=$(docker network ls --no-trunc --format '{{.Name}}' | grep '^github_network') +fi + +# Build network args if a network is set +NETWORK_ARGS="" +if [ -n "$DOCKER_NETWORK" ]; then + NETWORK_ARGS="--network $DOCKER_NETWORK --network-alias $DOCKER_NETWORK_ALIAS" +fi + +# Echo selected network info for visibility +echo "::group::Selecting Docker network" +if [ -n "$DOCKER_NETWORK" ]; then + echo " - Docker network: [$DOCKER_NETWORK]" + echo " - Network alias: [$DOCKER_NETWORK_ALIAS]" +else + echo " - No Docker network provided; container will use default Docker network." +fi +echo "" +echo "::endgroup::" # `mongosh` is used starting from MongoDB 5.x MONGODB_CLIENT="mongosh --quiet" @@ -47,16 +74,8 @@ wait_for_mongodb () { TIMER=0 MONGODB_ARGS="" - - if [ -z "$MONGODB_REPLICA_SET" ] - then - if [ -z "$MONGODB_USERNAME" ] - then - MONGODB_ARGS="" - else - # no replica set, but username given: use them as args - MONGODB_ARGS="--username $MONGODB_USERNAME --password $MONGODB_PASSWORD" - fi + if [ -n "$MONGODB_USERNAME" ]; then + MONGODB_ARGS="--username $MONGODB_USERNAME --password $MONGODB_PASSWORD --authenticationDatabase $MONGODB_AUTHSOURCE" fi # until ${WAIT_FOR_MONGODB_COMMAND} @@ -66,7 +85,7 @@ wait_for_mongodb () { sleep 1 TIMER=$((TIMER + 1)) - if [[ $TIMER -eq 20 ]]; then + if [ "$TIMER" -eq 20 ]; then echo "MongoDB did not initialize within 20 seconds. Exiting." exit 2 fi @@ -82,7 +101,7 @@ wait_for_mongodb () { # docker rm -f $MONGODB_CONTAINER_NAME # fi - +# If no replica set specified, run single node if [ -z "$MONGODB_REPLICA_SET" ]; then echo "::group::Starting single-node instance, no replica set" echo " - port [$MONGODB_PORT]" @@ -92,7 +111,13 @@ if [ -z "$MONGODB_REPLICA_SET" ]; then echo " - container-name [$MONGODB_CONTAINER_NAME]" echo "" - docker run --name $MONGODB_CONTAINER_NAME --publish $MONGODB_PORT:$MONGODB_PORT -e MONGO_INITDB_DATABASE=$MONGODB_DB -e MONGO_INITDB_ROOT_USERNAME=$MONGODB_USERNAME -e MONGO_INITDB_ROOT_PASSWORD=$MONGODB_PASSWORD --detach $MONGODB_IMAGE:$MONGODB_VERSION --port $MONGODB_PORT + docker run --name $MONGODB_CONTAINER_NAME \ + $NETWORK_ARGS \ + --publish $MONGODB_PORT:$MONGODB_PORT \ + -e MONGO_INITDB_DATABASE=$MONGODB_DB \ + -e MONGO_INITDB_ROOT_USERNAME=$MONGODB_USERNAME \ + -e MONGO_INITDB_ROOT_PASSWORD=$MONGODB_PASSWORD \ + --detach $MONGODB_IMAGE:$MONGODB_VERSION --port $MONGODB_PORT if [ $? -ne 0 ]; then echo "Error starting MongoDB Docker container" @@ -101,36 +126,81 @@ if [ -z "$MONGODB_REPLICA_SET" ]; then echo "::endgroup::" wait_for_mongodb - exit 0 fi - echo "::group::Starting MongoDB as single-node replica set" echo " - port [$MONGODB_PORT]" echo " - version [$MONGODB_VERSION]" echo " - replica set [$MONGODB_REPLICA_SET]" +if [ -n "$MONGODB_KEY" ]; then + echo " - keyFile provided: yes" +else + echo " - keyFile provided: no (random)" +fi echo "" +# For replica set mode: +# If auth (username/password) is requested, ensure mongodb-key is provided, otherwise generate random +if { [ -n "$MONGODB_USERNAME" ] || [ -n "$MONGODB_PASSWORD" ]; } && [ -z "$MONGODB_KEY" ]; then + MONGODB_KEY=$(dd if=/dev/urandom bs=256 count=1 2>/dev/null | base64 | tr -d '\n') +fi + +MONGODB_CMD_ARGS="--port \"$MONGODB_PORT\"" -docker run --name $MONGODB_CONTAINER_NAME --publish $MONGODB_PORT:$MONGODB_PORT --detach $MONGODB_IMAGE:$MONGODB_VERSION --port $MONGODB_PORT --replSet $MONGODB_REPLICA_SET +if [ -n "$MONGODB_REPLICA_SET" ]; then + MONGODB_CMD_ARGS="$MONGODB_CMD_ARGS --replSet \"$MONGODB_REPLICA_SET\"" +fi + +if [ -n "$MONGODB_KEY" ]; then + # NOTE: MONGO_KEY_FILE is interpolated internally + MONGODB_CMD_ARGS="$MONGODB_CMD_ARGS --keyFile \"\$MONGO_KEY_FILE\"" +fi + + +# Start mongod in replica set mode, with optional auth and keyFile +# MONGO_INITDB_* envs will create the root user on first startup + +docker run --name $MONGODB_CONTAINER_NAME \ + $NETWORK_ARGS \ + --publish $MONGODB_PORT:$MONGODB_PORT \ + -e MONGO_INITDB_DATABASE=$MONGODB_DB \ + -e MONGO_INITDB_ROOT_USERNAME=$MONGODB_USERNAME \ + -e MONGO_INITDB_ROOT_PASSWORD=$MONGODB_PASSWORD \ + -e MONGO_KEY=$MONGODB_KEY \ + -e MONGO_KEY_FILE=/tmp/mongo-keyfile \ + --detach \ + --entrypoint bash \ + $MONGODB_IMAGE:$MONGODB_VERSION \ + -c '\ + echo "$MONGO_KEY" > "$MONGO_KEY_FILE" && chmod 400 "$MONGO_KEY_FILE" && chown mongodb:mongodb "$MONGO_KEY_FILE" && \ + exec docker-entrypoint.sh mongod '"$MONGODB_CMD_ARGS"' \ + ' if [ $? -ne 0 ]; then echo "Error starting MongoDB Docker container" exit 2 fi + echo "::endgroup::" wait_for_mongodb +# After mongod is up, initiate the replica set +# Use auth if credentials were supplied +MONGODB_ARGS="" +if [ -n "$MONGODB_USERNAME" ]; then + MONGODB_ARGS="--username $MONGODB_USERNAME --password $MONGODB_PASSWORD" +fi + echo "::group::Initiating replica set [$MONGODB_REPLICA_SET]" -docker exec --tty $MONGODB_CONTAINER_NAME $MONGODB_CLIENT --port $MONGODB_PORT --eval " +docker exec --tty $MONGODB_CONTAINER_NAME $MONGODB_CLIENT --port $MONGODB_PORT $MONGODB_ARGS --eval " rs.initiate({ \"_id\": \"$MONGODB_REPLICA_SET\", \"members\": [ { \"_id\": 0, - \"host\": \"localhost:$MONGODB_PORT\" + \"host\": \"$MONGODB_REPLICA_SET_HOST:$MONGODB_PORT\" } ] }) " @@ -140,5 +210,5 @@ echo "::endgroup::" echo "::group::Checking replica set status [$MONGODB_REPLICA_SET]" -docker exec --tty $MONGODB_CONTAINER_NAME $MONGODB_CLIENT --port $MONGODB_PORT --eval "rs.status()" +docker exec --tty $MONGODB_CONTAINER_NAME $MONGODB_CLIENT --port $MONGODB_PORT $MONGODB_ARGS --eval "rs.status()" echo "::endgroup::" diff --git a/stop-mongodb.sh b/stop-mongodb.sh new file mode 100755 index 0000000..ea2cf5d --- /dev/null +++ b/stop-mongodb.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# Keep argument positions aligned with action.yml "args" so we can reuse them in post-args +MONGODB_IMAGE=$1 +MONGODB_VERSION=$2 +MONGODB_REPLICA_SET=$3 +MONGODB_PORT=$4 +MONGODB_DB=$5 +MONGODB_USERNAME=$6 +MONGODB_PASSWORD=$7 +MONGODB_CONTAINER_NAME=$8 +MONGODB_KEY=$9 +MONGODB_AUTHSOURCE=${10} +MONGODB_REPLICA_SET_HOST=${11:-"localhost"} +DOCKER_NETWORK=${12} +DOCKER_NETWORK_ALIAS=${13:-$MONGODB_CONTAINER_NAME} + +# Best-effort cleanup, do not fail the job if cleanup fails +set +e + +echo "::group::Cleaning up MongoDB container [$MONGODB_CONTAINER_NAME]" + +if docker ps -a --format '{{.Names}}' | grep -Eq "^${MONGODB_CONTAINER_NAME}$"; then + docker rm -f "$MONGODB_CONTAINER_NAME" >/dev/null 2>&1 || true + echo "Removed container $MONGODB_CONTAINER_NAME" +else + echo "Container $MONGODB_CONTAINER_NAME not found; nothing to clean." +fi + +echo "::endgroup::" + +exit 0 diff --git a/test/single-instance/single-instance.js b/test/single-instance/single-instance.js index 430c66c..2b47ab3 100644 --- a/test/single-instance/single-instance.js +++ b/test/single-instance/single-instance.js @@ -4,14 +4,14 @@ const { test } = require('uvu') const { expect } = require('expect') const Mongoose = require('mongoose') -const { MONGODB_USERNAME, MONGODB_PASSWORD, MONGODB_DB } = process.env +const { MONGODB_USERNAME, MONGODB_PASSWORD, MONGODB_DB, MONGODB_AUTHSOURCE } = process.env test('connects to MongoDB', async () => { const connection = await Mongoose.createConnection('mongodb://localhost', { user: MONGODB_USERNAME, pass: MONGODB_PASSWORD, dbName: MONGODB_DB, - authSource: MONGODB_USERNAME && MONGODB_PASSWORD ? 'admin' : undefined + authSource: MONGODB_USERNAME && MONGODB_PASSWORD ? MONGODB_AUTHSOURCE : undefined }) await connection.close()