diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..d8c8777 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,91 @@ +name: Build and Publish Docker Images + +on: + workflow_dispatch: + push: + branches: + - '**' + tags: + - 'v*.*.*' + pull_request: + +jobs: + build_and_publish: + name: Build and Publish Docker images + runs-on: ubuntu-latest + strategy: + matrix: + version: + - '3.6.6' + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} + password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} + + # GS64 base image + - name: Gather docker meta data for base image + id: docker_meta_runtime + uses: crazy-max/ghaction-docker-meta@v4 + with: + images: ghcr.io/${{ github.repository_owner }}/gs64 + + - name: Docker build and push base image + uses: docker/build-push-action@v4 + with: + context: ./source + file: ./source/Dockerfile + build-args: GS_VERSION=${{ matrix.version }} + target: docker-gs64-base + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.docker_meta_runtime.outputs.tags }} + labels: ${{ steps.docker_meta_runtime.outputs.labels }} + secrets: GIT_AUTH_TOKEN=${{ secrets.DOCKER_REGISTRY_TOKEN }} + + # GS64 base image + rowan extent + - name: Gather docker meta data for rowan image + id: docker_meta_runtime_rowan + uses: crazy-max/ghaction-docker-meta@v4 + with: + images: ghcr.io/${{ github.repository_owner }}/gs64-rowan + + - name: Docker build and push rowan image + uses: docker/build-push-action@v4 + with: + context: ./source + file: ./source/Dockerfile + build-args: GS_VERSION=${{ matrix.version }} + target: docker-gs64-rowan + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.docker_meta_runtime_rowan.outputs.tags }} + labels: ${{ steps.docker_meta_runtime_rowan.outputs.labels }} + secrets: GIT_AUTH_TOKEN=${{ secrets.DOCKER_REGISTRY_TOKEN }} + + # GS64 base image + rowan extent + git + - name: Gather docker meta data for rowan-loader image + id: docker_meta_runtime_rowan_loader + uses: crazy-max/ghaction-docker-meta@v4 + with: + images: ghcr.io/${{ github.repository_owner }}/gs64-rowan-loader + + - name: Docker build and push rowan-loader image + uses: docker/build-push-action@v4 + with: + context: ./source + file: ./source/Dockerfile + build-args: GS_VERSION=${{ matrix.version }} + target: docker-gs64-rowan-loader + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.docker_meta_runtime_rowan_loader.outputs.tags }} + labels: ${{ steps.docker_meta_runtime_rowan_loader.outputs.labels }} + secrets: GIT_AUTH_TOKEN=${{ secrets.DOCKER_REGISTRY_TOKEN }} diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml new file mode 100644 index 0000000..fbb50fa --- /dev/null +++ b/.github/workflows/markdown-lint.yml @@ -0,0 +1,14 @@ +name: Markdown Lint +on: [push,pull_request,workflow_dispatch] +jobs: + remark-lint: + name: runner / markdownlint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: markdownlint + uses: reviewdog/action-markdownlint@v0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + fail_on_error: true + reporter: github-pr-review diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 0000000..dabaf41 --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,13 @@ +name: 'Shellcheck' + +on: [push,pull_request] + +jobs: + shellcheck: + runs-on: 'ubuntu-latest' + steps: + - uses: actions/checkout@v3 + - name: 'Run Shellcheck' + uses: reviewdog/action-shellcheck@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f6d744 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +data/* +locks/* +logs/* +conf/* +projects/* diff --git a/README.md b/README.md index d59f49b..4818b23 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,17 @@ license terms. Users with access to a custom key file, can mount and use it inst - [Report a defect](https://github.com/ba-st/Docker-GemStone-64/issues/new?labels=Type%3A+Defect) - [Request a feature](https://github.com/ba-st/Docker-GemStone-64/issues/new?labels=Type%3A+Feature) +## Quick start + +```bash +cd examples +docker compose up -d stone +``` + ## License - The code is licensed under [MIT](LICENSE). -- The documentation is licensed under [CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/). +- The documentation is licensed under [CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/) ## Contributing diff --git a/docs/README.md b/docs/README.md index 2e6d565..b1ba5e1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,3 +2,29 @@ **Unofficial** docker images for GemStone/S 64 bits server and gem sessions. This is a community project not endorsed by [GemTalk](https://gemtalksystems.com). + +## Configuration + +You can configure some things with environment variables: + +- `NETLDI_SERVICE_NAME` Netldi service name. Defaults to `gs64ldi` +- `NETLDI_PORT` NetLDI service port. Defaults to `50384` +- `STONE_SERVICE_NAME` Stone service name. Defaults to `gs64stone` +- `STONE_PORT` Stone service port. Defaults to `50385` +- `GEMSTONE_NRS_ALL` Defaults to `#netldi:gs64ldi#dir:/opt/gemstone/logs/#log:/opt/gemstone/logs/%N_%P.log` +- `GS_FORCE_CLEAN_LOG_FILE_DELETE` Defaults to `true` +- `DATA_CURATOR_PASSWORD` Password of the `DataCurator` user, used for stopping + the services. Defaults to `swordfish`. +- `STOPSTONE_TIMEOUT_SECONDS` time-out in seconds to wait for `stopstone` + command to finish. Defaults to 8 + +## Important directories and files + +- `/opt/gemstone/conf/${STONE_SERVICE_NAME}.conf` Stone configuration, created + if missing. +- `/opt/gemstone/conf/system.conf` GemStone system configuration, created if missing. +- `/opt/gemstone/data` Extent and transaction log location +- `/opt/gemstone/locks` Lock files +- `/opt/gemstone/logs` Log files +- `/opt/gemstone/product/sys/gemstone.key` License key, by default `community.starter.key` + is used. diff --git a/docs/reference/DockerImages.md b/docs/reference/DockerImages.md new file mode 100644 index 0000000..03aac40 --- /dev/null +++ b/docs/reference/DockerImages.md @@ -0,0 +1,19 @@ +# Docker images reference + +## `gs64` base docker image + +This container image contains the GemStone/S 64 bits runtime support but doesn't +provide an `extent0.dbf`. Users will need to map a volume against `/opt/gemstone/data/` +containing the relevant data files (extents and transaction logs). + +## `gs64-rowan` docker image + +This container image builds on top of the base image providing the `extent0.rowan.dbf`. +It's a useful target for a CI system. + +## `gs64-rowan-loader` docker image + +This container image builds on top of the rowan image installing also git. +In order to use Rowan for cloning and manipulating remote repositories, users +will need to map a volume against `/home/gemstone/.ssh/` containing the +relevant keys. diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..f103b36 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,5 @@ +conf/* +data/* +logs/* +locks/* +projects/* diff --git a/examples/docker-compose.build.yml b/examples/docker-compose.build.yml new file mode 100644 index 0000000..f552542 --- /dev/null +++ b/examples/docker-compose.build.yml @@ -0,0 +1,19 @@ +version: "3" + +services: + stone: + init: true + network_mode: host + build: + context: ../source + target: docker-gs64-rowan-loader + args: + GS_VERSION: 3.6.6 + environment: + TZ: America/Argentina/Buenos_Aires + volumes: + - ./conf/:/opt/gemstone/conf/ + - ./locks/:/opt/gemstone/locks/ + - ./logs/:/opt/gemstone/logs/ + - ./projects/:/opt/gemstone/projects/ + - ~/.ssh/:/home/gemstone/.ssh/:ro diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml new file mode 100644 index 0000000..fa34c37 --- /dev/null +++ b/examples/docker-compose.yml @@ -0,0 +1,15 @@ +version: "3" + +services: + stone: + init: true + network_mode: host + image: ghcr.io/ba-st/gs64-rowan-loader:v3.6.6 + environment: + TZ: America/Argentina/Buenos_Aires + volumes: + - ./conf/:/opt/gemstone/conf/ + - ./locks/:/opt/gemstone/locks/ + - ./logs/:/opt/gemstone/logs/ + - ./projects/:/opt/gemstone/projects/ + - ~/.ssh/:/home/gemstone/.ssh/:ro diff --git a/source/Dockerfile b/source/Dockerfile new file mode 100644 index 0000000..75fd0bc --- /dev/null +++ b/source/Dockerfile @@ -0,0 +1,146 @@ +# Runs on a multi-stage build: https://docs.docker.com/develop/develop-images/multistage-build/ +# - Prepare a base image with all dependencies installed +# - Download and prepare environment +# - Copy prepared environment onto the final image + +FROM debian:12-slim AS base + +ENV SHELL=/bin/bash +ENV LC_ALL=en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US.UTF-8 + +ENV GS_USER=gemstone +ENV GS_UID=1001 +ENV GS_GID=100 + +RUN apt-get update \ + && apt-get install --assume-yes --no-install-recommends \ + ca-certificates \ + gosu \ + locales \ + && echo "en_US.UTF-8 UTF-8" > /etc/locale.gen \ + && echo "en_US.ISO-8859-15 ISO-8859-15" >> /etc/locale.gen \ + && locale-gen \ + && apt-get clean \ + && rm --recursive --force /var/lib/apt/lists/* /tmp/* /var/tmp/* \ + && useradd --uid ${GS_UID} --gid ${GS_GID} --no-create-home --no-user-group ${GS_USER} \ + ; + +## Donwload GemStone 64 bits release +FROM alpine:3.18 as download + +ARG GS_VERSION +ENV GEMSTONE_GLOBAL_DIR=/opt/gemstone +ENV GEMSTONE=${GEMSTONE_GLOBAL_DIR}/product +ENV DOWNLOAD_LOCATION=/tmp + +# Download official release +RUN wget https://downloads.gemtalksystems.com/pub/GemStone64/${GS_VERSION}/GemStone64Bit${GS_VERSION}-x86_64.Linux.zip \ + -O ${DOWNLOAD_LOCATION}/GemStone.zip + +#Extract it +RUN mkdir - ${GEMSTONE_GLOBAL_DIR} \ + && unzip -qq ${DOWNLOAD_LOCATION}/GemStone.zip -d ${DOWNLOAD_LOCATION} \ + && mv ${DOWNLOAD_LOCATION}/GemStone64Bit${GS_VERSION}-x86_64.Linux ${GEMSTONE} + +# Create required directories +RUN mkdir -p \ + ${GEMSTONE_GLOBAL_DIR}/conf/ \ + ${GEMSTONE_GLOBAL_DIR}/data/ \ + ${GEMSTONE_GLOBAL_DIR}/locks/ \ + ${GEMSTONE_GLOBAL_DIR}/logs/ \ + ; + +# Remove non-essential files to keep the final image size small +RUN rm -rf \ + ${GEMSTONE}/bin32/ \ + ${GEMSTONE}/examples/ \ + ${GEMSTONE}/include/ \ + ${GEMSTONE}/install/ \ + ${GEMSTONE}/lib32/ \ + ${GEMSTONE}/rowan/ \ + ${GEMSTONE}/seaside/ \ + ${GEMSTONE}/upgrade/ \ + ${GEMSTONE}/bin/vsd* \ + ${GEMSTONE}/lib/tcl* \ + ${GEMSTONE}/lib/tk* \ + ${GEMSTONE}/lib/Tix* \ + ${GEMSTONE}/lib/pkgconfig/tcl* \ + ${GEMSTONE}/lib/pkgconfig/tk* \ + ${GEMSTONE}/lib/rbc* \ + ${GEMSTONE}/doc/man1/vsd.1 \ +&& mv ${GEMSTONE}/bin/extent0*.dbf ${DOWNLOAD_LOCATION} \ +; + +# Replace `data` location in the default system.conf +RUN sed -ri 's|\$GEMSTONE/data|\$GEMSTONE_GLOBAL_DIR/data|g' ${GEMSTONE}/data/system.conf + +# Copy startup scripts +COPY start-gemstone.sh ${GEMSTONE_GLOBAL_DIR}/start-gemstone.sh +COPY entrypoint.sh ${GEMSTONE_GLOBAL_DIR}/entrypoint.sh + +## Prepare server image +FROM base as docker-gs64-server +LABEL maintainer="Buenos Aires Smalltalk " + +ENV GEMSTONE_GLOBAL_DIR=/opt/gemstone +ENV GEMSTONE=${GEMSTONE_GLOBAL_DIR}/product +ENV GEMSTONE_EXE_CONF=${GEMSTONE_GLOBAL_DIR}/conf +ENV GEMSTONE_SYS_CONF=${GEMSTONE_GLOBAL_DIR}/conf +ENV GEMSTONE_LOG_DIR=${GEMSTONE_GLOBAL_DIR}/logs + +ENV PATH=$GEMSTONE/bin:$PATH +# Used to determine what shell to use for an exec, such as by System class>>performOnServer:. +ENV SHELL=/usr/bin/bash + +ENV NETLDI_SERVICE_NAME=gs64ldi +ENV NETLDI_PORT=50384 +ENV STONE_SERVICE_NAME=gs64stone +ENV STONE_PORT=50385 + +ENV DATA_CURATOR_PASSWORD=swordfish +# By default `docker stop` will wait 10 seconds for the container to handle +# SIGTERM signals, after the grace period docker will send a SIGKILL. +# By default we wait 8 seconds to allow a clean shutdown of the Stone. +# If your installation needs a greater timeout change the docker wait time +# and this timet-out in consequence +ENV STOPSTONE_TIMEOUT_SECONDS=8 + +ENV GS_FORCE_CLEAN_LOG_FILE_DELETE=true +ENV GEMSTONE_NRS_ALL=#netldi:$NETLDI_SERVICE_NAME#dir:$GEMSTONE_LOG_DIR/#log:$GEMSTONE_LOG_DIR/%N_%P.log + +COPY --from=download --chown=${GS_USER}:users ${GEMSTONE_GLOBAL_DIR} ${GEMSTONE_GLOBAL_DIR} +RUN ln -s ${GEMSTONE}/bin/gemsetup.sh /etc/profile.d/gemstone.sh + + +WORKDIR ${GEMSTONE_GLOBAL_DIR} +VOLUME ${GEMSTONE_GLOBAL_DIR}/data/ +CMD ["./entrypoint.sh"] + +## Prepare server base image +FROM docker-gs64-server as docker-gs64-base + +COPY --from=download --chown=${GS_USER}:users /tmp/extent0.dbf ${GEMSTONE_GLOBAL_DIR}/data/extent0.dbf +CMD ["./entrypoint.sh"] + +## Prepare server rowan image +FROM docker-gs64-server as docker-gs64-rowan + +ENV ROWAN_PROJECTS_HOME=${GEMSTONE_GLOBAL_DIR}/projects + +RUN mkdir ${ROWAN_PROJECTS_HOME} +COPY --from=download --chown=${GS_USER}:users /tmp/extent0.rowan.dbf ${GEMSTONE_GLOBAL_DIR}/data/extent0.dbf +CMD ["./entrypoint.sh"] + +## Prepare server rowan loader image +FROM docker-gs64-rowan as docker-gs64-rowan-loader + +# Install git +RUN apt-get update \ + && apt-get install --assume-yes --no-install-recommends git \ + && apt-get clean \ + && rm --recursive --force /var/lib/apt/lists/* /tmp/* /var/tmp/* \ + ; + +CMD ["./entrypoint.sh"] diff --git a/source/entrypoint.sh b/source/entrypoint.sh new file mode 100755 index 0000000..d2d994c --- /dev/null +++ b/source/entrypoint.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -eu + +echo "${NETLDI_SERVICE_NAME} ${NETLDI_PORT}/tcp # GemStone - Netldi" >> /etc/services +echo "${STONE_SERVICE_NAME} ${STONE_PORT}/tcp # GemStone - Stone" >> /etc/services + +# Copy default system config if missing +if [ ! -f "${GEMSTONE_SYS_CONF}/system.conf" ]; then + cp -p "${GEMSTONE}/data/system.conf" "${GEMSTONE_SYS_CONF}/system.conf" +fi + +# Create (empty) stone config if missing +if [ ! -f "${GEMSTONE_EXE_CONF}/${STONE_SERVICE_NAME}.conf" ]; then + touch "${GEMSTONE_EXE_CONF}/${STONE_SERVICE_NAME}.conf" +fi + +# Ensure write permissions +NEED_WRITE_PERMISSION=( + "${GEMSTONE_SYS_CONF}/system.conf" + "${GEMSTONE_EXE_CONF}/${STONE_SERVICE_NAME}.conf" + "${GEMSTONE_GLOBAL_DIR}/data/" + "${GEMSTONE_GLOBAL_DIR}/data/extent0.dbf" + "${GEMSTONE_GLOBAL_DIR}/locks/" + "${GEMSTONE_LOG_DIR}/" +) + +for path in "${NEED_WRITE_PERMISSION[@]}"; do + if ! gosu "${GS_USER}" test -w "$path"; then + chown "${GS_UID}:${GS_GID}" "$path" + chmod ug+w "$path" + fi +done + +# Run gemstone as GS_USER +exec gosu "${GS_USER}" "${GEMSTONE_GLOBAL_DIR}/start-gemstone.sh" diff --git a/source/start-gemstone.sh b/source/start-gemstone.sh new file mode 100755 index 0000000..bee78bb --- /dev/null +++ b/source/start-gemstone.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# based on https://medium.com/@gchudnov/trapping-signals-in-docker-containers-7a57fdda7d86 +# see also https://docs.docker.com/engine/reference/builder/#exec-form-entrypoint-example +set -eu + +# SIGTERM-handler +sigterm_handler() { + echo 'Got SIGTERM, stopping GemStone/S 64 server' + stopnetldi + stopstone \ + -i \ + -t "${STOPSTONE_TIMEOUT_SECONDS}" \ + "$STONE_SERVICE_NAME" \ + DataCurator \ + "${DATA_CURATOR_PASSWORD}" + exit 143; # 128 + 15 -- SIGTERM +} + +# setup handlers +# on callback, kill the last background process, +# which is `tail -f /dev/null` and execute the specified handler +trap 'kill ${!}; sigterm_handler' SIGTERM + +# start GemStone services +# shellcheck disable=SC2086 +startnetldi \ + -g \ + -a "${GS_USER}" \ + -n \ + -P "${NETLDI_PORT}" \ + -l "${GEMSTONE_LOG_DIR}/${NETLDI_SERVICE_NAME}.log" \ + ${NETLDI_ARGS:-} \ + "${NETLDI_SERVICE_NAME}" + +# shellcheck disable=SC2086 +startstone \ + -e "${GEMSTONE_EXE_CONF}" \ + -z "${GEMSTONE_SYS_CONF}" \ + -l "${GEMSTONE_LOG_DIR}/${STONE_SERVICE_NAME}.log" \ + ${STONE_ARGS:-} \ + ${STONE_SERVICE_NAME} + +# list GemStone servers +gslist -cvl + +# wait forever, (loop to handle multiple signals if needed) +while true +do + tail -f /dev/null & wait ${!} +done