From 183af7604c97c6ef31ad2702b76175623c62bfec Mon Sep 17 00:00:00 2001 From: Stefan Peters Date: Wed, 17 Jan 2024 09:56:29 +0100 Subject: [PATCH] Initial commit with first working implementation --- .dockerignore | 19 ++++++++ .github/workflows/docker.yml | 36 ++++++++++++++ .gitignore | 15 ++++++ Dockerfile | 18 +++++++ README.md | 33 +++++++++++++ docker-compose.yml | 14 ++++++ docker-entrypoint.sh | 9 ++++ start.mjs | 94 ++++++++++++++++++++++++++++++++++++ 8 files changed, 238 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100755 docker-entrypoint.sh create mode 100755 start.mjs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e837f3a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.DS_Store +node_modules/ +/dist*/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +data/ + +# Temporary folder +temp/ + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..0ebc5eb --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,36 @@ +name: Publish Docker +on: + push: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Docker Meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + - + name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - + name: Build and Push + uses: docker/build-push-action@v4 + with: + push: true + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f5a873 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +node_modules/ +/dist*/ +/data*/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c18f241 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:22.04 +WORKDIR /root + +# Use bash as shell (instead of sh) +SHELL ["/bin/bash", "-c"] + +RUN apt update +# Install dependencies +RUN apt install -y curl git wget unzip jq +# Install fnm for managing Node.js versions +RUN curl -fsSL https://fnm.vercel.app/install | bash + +COPY . . + +# Directory where all Cocoda installations will live +RUN mkdir -p /www/cocoda + +CMD ./docker-entrypoint.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7414b6 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Cocoda Versions (Docker) + +Docker image to manage and serve multiple instances of [Cocoda Mapping Tool](https://github.com/gbv/cocoda). + +Note: Experimental. + +## Usage via Docker Compose + +```yml +version: "3" + +services: + cocoda: + image: ghcr.io/gbv/cocoda-versions + volumes: + - ./data/cocoda:/www/cocoda + ports: + - 8080:80 + environment: + - TAGS=0.2.0 1.0.0 dev + restart: unless-stopped +``` + +In the bind mount `./data/cocoda`, the static files of all Cocoda instances will be placed (to prevent rebuilding them every time). In that folder, you can also specify custom Cocoda configurations as `{instance-name}.json`. These will be built in addition to the defined `TAGS`. Custom configurations will use branch `master` by default; a different branch for a particular instance can be specific inside its configuration file as `_branch`. + +A special tag `all` can be used to build ALL existing Cocoda versions, plus branches `dev` and `master`. This can be used to provide a history of old Cocoda versions. + +The HTTP server serves the instances under the subpath `/cocoda/`, i.e. in the above example, branch `dev` will be availble at http://localhost:8080/cocoda/dev/. + +## To-Do +- [ ] Consider using other HTTP server instead of [http-server](https://github.com/http-party/http-server) +- [ ] Separate build step from container start (only start HTTP server on container start) +- [ ] Update specific versions/branches without restarting container. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dfcf3b4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3" + +services: + cocoda: + build: . + volumes: + - ./data/cocoda:/www/cocoda + - ./data/cocoda.git:/root/cocoda/ + - ./test.mjs:/root/test.mjs + ports: + - 8091:80 + environment: + - TAGS=dev + # restart: unless-stopped diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..03f0c3e --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Prepare Node environment via fnm +export PATH="/root/.local/share/fnm:$PATH" +eval "`fnm env --shell bash`" +fnm install 20 +npm i -g zx + +./start.mjs diff --git a/start.mjs b/start.mjs new file mode 100755 index 0000000..2bd0834 --- /dev/null +++ b/start.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env zx + +await $`node --version` +await $`npm --version` + +if (!await fs.pathExists("cocoda")) { + await $`mkdir cocoda` +} +await cd("cocoda") + +if (!await fs.pathExists(".git")) { + await $`git clone https://github.com/gbv/cocoda.git .` +} + +if (!await fs.pathExists("node_modules")) { + await $`npm ci` +} + +// Determine tags +const tags = new Set(process.env.TAGS ? process.env.TAGS.split(" ") : ["all"]) +if (tags.has("all")) { + tags.delete("all") + const allTags = await $`git tag` + ;`${allTags}`.split("\n").filter(Boolean).forEach(tag => tags.add(tag)) + tags.add("master") + tags.add("dev") +} + +const targetFolder = "/www/cocoda" + +// Target folder also contains custom configs +const customConfigs = await glob([`${targetFolder}/*.json`]) +const instances = [] +for (const configFile of customConfigs) { + const name = path.basename(configFile, ".json") + if (tags.has(name)) { + tags.delete(name) + } + // Read config file + const instance = { name, configFile } + try { + const { _branch } = await fs.readJson(configFile) + instance.branch = _branch || "master" + instances.push(instance) + } catch (error) { + console.error(`Error: Skipping custom instance ${name} because JSON could not be parsed.`) + } +} + +// Add tags to instances +for (const tag of tags) { + instances.push({ name: tag, branch: tag }) +} + +const updatedBranches = new Set() + +for (const { name, configFile, branch } of instances) { + console.log(`Cocoda Instance: ${name} (branch/tag: ${branch}, config: ${configFile ?? "none"})`) + if (await fs.pathExists(`${targetFolder}/${name}`)) { + // No updates for non-branches + if (branch.match(/^\d/)) { + console.log("- instance already built and no updates necessary") + continue + } + + // Skip if there are no changes with origin + await $`git fetch`.quiet() + const diff = await $`git diff ${branch} origin/${branch}`.quiet() + if (!`${diff}`.trim() && !updatedBranches.has(branch)) { + console.log("- instance already built and no updates necessary") + continue + } + updatedBranches.add(branch) + console.log(`- There's an update to branch ${branch}. Pulling changes and rebuilding Cocoda...`) + await $`git pull origin ${branch}`.quiet() + } + // Build version via build-all.sh script + await $`./build/build-all.sh ${branch}` + // Move files to target folder + if (await fs.pathExists(`${targetFolder}/${name}`)) { + await $`rm -r ${targetFolder}/${name}` + } + await $`mv releases/${branch} ${targetFolder}/${name}` + // Link config file if needed + if (configFile) { + await $`rm ${targetFolder}/${name}/cocoda.json` + await $`ln -s ${configFile} ${targetFolder}/${name}/cocoda.json` + } + console.log(`- Successfully built instance ${name}!`) +} + +const port = process.env.PORT ?? "8091" +console.log(`Starting HTTP server on port ${port}...`) +await $`npx http-server -s -d false -p ${port} /www/`