From 0d32376f8d05dcae808f91ffc39406b4010ed843 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 17 Feb 2025 09:49:12 +0000 Subject: [PATCH 1/4] Add docker bake environment with support for building an Element Web docker image with build & runtime module loader with support for building docker images with a module pre-baked in --- packages/element-web-module-api/Dockerfile | 19 ++++++++++ packages/element-web-module-api/README.md | 22 +++++++++++ .../17-fetch-element-modules.sh | 38 +++++++++++++++++++ .../18-load-element-modules.sh | 33 ++++++++++++++++ .../nginx-templates/default.conf.template | 24 ++++++++++++ 5 files changed, 136 insertions(+) create mode 100644 packages/element-web-module-api/Dockerfile create mode 100755 packages/element-web-module-api/docker/docker-entrypoint.d/17-fetch-element-modules.sh create mode 100755 packages/element-web-module-api/docker/docker-entrypoint.d/18-load-element-modules.sh create mode 100644 packages/element-web-module-api/docker/nginx-templates/default.conf.template diff --git a/packages/element-web-module-api/Dockerfile b/packages/element-web-module-api/Dockerfile new file mode 100644 index 0000000000..70f589b8c5 --- /dev/null +++ b/packages/element-web-module-api/Dockerfile @@ -0,0 +1,19 @@ +ARG ELEMENT_VERSION=latest +FROM vectorim/element-web:${ELEMENT_VERSION} +ARG ELEMENT_WEB_MODULES="" + +# Override default nginx config. Templates in `/etc/nginx/templates` are passed +# through `envsubst` by the nginx docker image entry point. +COPY /docker/nginx-templates/* /etc/nginx/templates/ +COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/ + +# Create directories before we gain privileges +RUN mkdir -p /tmp/element-web-config /tmp/element-web-modules + +# Escalate privileges to install jq and moreutils and run the fetch modules script +USER root +RUN apk add jq moreutils +RUN ELEMENT_WEB_MODULES=${ELEMENT_WEB_MODULES} /docker-entrypoint.d/17-fetch-element-modules.sh + +# Drop privileges back to nginx +USER nginx \ No newline at end of file diff --git a/packages/element-web-module-api/README.md b/packages/element-web-module-api/README.md index b5cdac7d96..d1f3c94fc5 100644 --- a/packages/element-web-module-api/README.md +++ b/packages/element-web-module-api/README.md @@ -4,6 +4,28 @@ API surface for extending Element Web in a safe & predictable way. This project is still in early development but aims to replace matrix-react-sdk-module-api and Element Web deprecated customisations. +## Using the Docker image + +The docker image specified by the Dockerfile in this directory can be used in one of four ways. + +You can specify `ELEMENT_WEB_MODULES` as a build-arg or as a runtime environment variable to fetch & load a module +from a remote URL at runtime or to bundle it into the docker image for easier deployment respectively. +The format this variable should take is a comma-delimited list of URLs followed by the optional tarball hash after a `#` character, e.g. +`ELEMENT_WEB_MODULES=https://example.com/module.tgz#abc123,https://example.com/another-module.tgz#`. + +You can also use it as a base image and add your desired modules into `/tmp/element-web-modules` each in their own directory. +Finally, you can bind mount modules into the `/tmp/element-web-modules` directory at runtime. +The default entrypoint will be index.js in that directory but can be overriden if a package.json file is found with a `main` directive. + +The container expects a config.json file to be bind mounted or copied into the `/app/config.json` path. +The container runs an nginx web server in rootless mode on port 8080. +If you wish to use docker in read-only mode, you should follow the [upstream instructions](https://hub.docker.com/_/nginx#:~:text=Running%20nginx%20in%20read%2Donly%20mode) +but additionally include the following directories: + +- /tmp/element-web-modules/ +- /tmp/element-web-config/ +- /etc/nginx/conf.d/ + ## Using the API Modules are loaded by Element Web at runtime via a dynamic ecmascript import, but can be bundled into a webapp for deployment convenience. diff --git a/packages/element-web-module-api/docker/docker-entrypoint.d/17-fetch-element-modules.sh b/packages/element-web-module-api/docker/docker-entrypoint.d/17-fetch-element-modules.sh new file mode 100755 index 0000000000..ff57e90d53 --- /dev/null +++ b/packages/element-web-module-api/docker/docker-entrypoint.d/17-fetch-element-modules.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# Loads modules specified via env var ELEMENT_WEB_MODULES +# in a comma delimited format of `url#hash,url#hash,...` +# the URL should point to a gzipped tarball, e.g. https://registry.npmjs.org/level/-/level-9.0.0.tgz +# the hash should a sha256sum of the tgz/tar.gz archive, it can be omitted though +# Runs both during the build stage and the runtime entrypoint for added flexibility + +set -e + +entrypoint_log() { + if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo "$@" + fi +} + +if [ -z "${ELEMENT_WEB_MODULES}" ]; then + entrypoint_log "ELEMENT_WEB_MODULES is not set, skipping module loading" + exit 0 +fi + +for MODULE in ${ELEMENT_WEB_MODULES//,/ } +do + MODULE_URL=${MODULE%#*} + EXPECTED_HASH=${MODULE#*#} + + entrypoint_log "Fetching module from $MODULE_URL" + wget -O /tmp/element-web-modules/module.tar.gz "$MODULE_URL" + HASH=$(sha256sum /tmp/element-web-modules/module.tar.gz | awk '{ print $1 }') + if [ -n "$EXPECTED_HASH" ] && [ "$HASH" != "$EXPECTED_HASH" ]; then + echo "Hash mismatch for $MODULE_URL: expected $EXPECTED_HASH, got $HASH" + exit 1 + fi + + mkdir -p "/tmp/element-web-modules/$HASH" + tar xvf /tmp/element-web-modules/module.tar.gz -C "/tmp/element-web-modules/$HASH" --strip-components=1 + rm -Rf /tmp/element-web-modules/module.tar.gz +done \ No newline at end of file diff --git a/packages/element-web-module-api/docker/docker-entrypoint.d/18-load-element-modules.sh b/packages/element-web-module-api/docker/docker-entrypoint.d/18-load-element-modules.sh new file mode 100755 index 0000000000..fe08194de0 --- /dev/null +++ b/packages/element-web-module-api/docker/docker-entrypoint.d/18-load-element-modules.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# Loads modules from `/tmp/element-web-modules` into config.json's `modules` field + +set -e + +entrypoint_log() { + if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo "$@" + fi +} + +# If there are modules to be loaded +if [ -d "/tmp/element-web-modules" ]; then + cd /tmp/element-web-modules + + # Copy these config files as a base + cp /app/config*.json /tmp/element-web-config/ + + for MODULE in * + do + # If the module has a package.json, use its main field as the entrypoint + ENTRYPOINT="index.js" + if [ -f "/tmp/element-web-modules/$MODULE/package.json" ]; then + ENTRYPOINT=$(jq -r '.main' "/tmp/element-web-modules/$MODULE/package.json") + fi + + entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT" + + # Append the module to the config + jq ".modules += [\"/modules/$MODULE/$ENTRYPOINT\"]" /tmp/element-web-config/config.json | sponge /tmp/element-web-config/config.json + done +fi \ No newline at end of file diff --git a/packages/element-web-module-api/docker/nginx-templates/default.conf.template b/packages/element-web-module-api/docker/nginx-templates/default.conf.template new file mode 100644 index 0000000000..83813ddcd2 --- /dev/null +++ b/packages/element-web-module-api/docker/nginx-templates/default.conf.template @@ -0,0 +1,24 @@ +server { + listen 8080; + server_name localhost; + + root /app; + index index.html; + + location = /index.html { + add_header Cache-Control "no-cache"; + } + location = /version { + add_header Cache-Control "no-cache"; + } + # covers config.json and config.hostname.json requests as it is prefix. + location /config { + root /tmp/element-web-config; + add_header Cache-Control "no-cache"; + } + location /modules { + root /tmp/element-web-modules; + } + error_page 500 502 503 504 /50x.html; +} + From 3a729d048706022100c368c59980762333e9e87c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Feb 2025 15:29:56 +0000 Subject: [PATCH 2/4] Stash docker & bake --- packages/element-web-module-api/Dockerfile | 32 +++++++++------- packages/element-web-module-api/README.md | 26 ++----------- .../17-fetch-element-modules.sh | 38 ------------------- .../18-load-element-modules.sh | 33 ---------------- .../nginx-templates/default.conf.template | 24 ------------ 5 files changed, 22 insertions(+), 131 deletions(-) delete mode 100755 packages/element-web-module-api/docker/docker-entrypoint.d/17-fetch-element-modules.sh delete mode 100755 packages/element-web-module-api/docker/docker-entrypoint.d/18-load-element-modules.sh delete mode 100644 packages/element-web-module-api/docker/nginx-templates/default.conf.template diff --git a/packages/element-web-module-api/Dockerfile b/packages/element-web-module-api/Dockerfile index 70f589b8c5..a858dc6aed 100644 --- a/packages/element-web-module-api/Dockerfile +++ b/packages/element-web-module-api/Dockerfile @@ -1,19 +1,23 @@ ARG ELEMENT_VERSION=latest -FROM vectorim/element-web:${ELEMENT_VERSION} -ARG ELEMENT_WEB_MODULES="" -# Override default nginx config. Templates in `/etc/nginx/templates` are passed -# through `envsubst` by the nginx docker image entry point. -COPY /docker/nginx-templates/* /etc/nginx/templates/ -COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/ +FROM --platform=$BUILDPLATFORM node:lts-alpine AS builder -# Create directories before we gain privileges -RUN mkdir -p /tmp/element-web-config /tmp/element-web-modules +ARG BUILD_CONTEXT -# Escalate privileges to install jq and moreutils and run the fetch modules script -USER root -RUN apk add jq moreutils -RUN ELEMENT_WEB_MODULES=${ELEMENT_WEB_MODULES} /docker-entrypoint.d/17-fetch-element-modules.sh +RUN apk add --no-cache jq -# Drop privileges back to nginx -USER nginx \ No newline at end of file +WORKDIR /app +COPY package.json yarn.lock ./ +COPY ./$BUILD_CONTEXT/package.json ./$BUILD_CONTEXT/ +RUN yarn install --frozen-lockfile --ignore-scripts +COPY tsconfig.json ./ +COPY ./$BUILD_CONTEXT ./$BUILD_CONTEXT +RUN cd $BUILD_CONTEXT && yarn vite build +RUN mkdir /modules +RUN cp -r ./$BUILD_CONTEXT/lib/ /modules/$(jq -r '"\(.name)-v\(.version)"' ./$BUILD_CONTEXT/package.json) + +FROM ghcr.io/element-hq/element-web:${ELEMENT_VERSION} +ARG ELEMENT_VERSION=latest +ARG BUILD_CONTEXT + +COPY --from=builder /modules/* /tmp/element-web-modules/ \ No newline at end of file diff --git a/packages/element-web-module-api/README.md b/packages/element-web-module-api/README.md index d1f3c94fc5..93186ed438 100644 --- a/packages/element-web-module-api/README.md +++ b/packages/element-web-module-api/README.md @@ -4,28 +4,6 @@ API surface for extending Element Web in a safe & predictable way. This project is still in early development but aims to replace matrix-react-sdk-module-api and Element Web deprecated customisations. -## Using the Docker image - -The docker image specified by the Dockerfile in this directory can be used in one of four ways. - -You can specify `ELEMENT_WEB_MODULES` as a build-arg or as a runtime environment variable to fetch & load a module -from a remote URL at runtime or to bundle it into the docker image for easier deployment respectively. -The format this variable should take is a comma-delimited list of URLs followed by the optional tarball hash after a `#` character, e.g. -`ELEMENT_WEB_MODULES=https://example.com/module.tgz#abc123,https://example.com/another-module.tgz#`. - -You can also use it as a base image and add your desired modules into `/tmp/element-web-modules` each in their own directory. -Finally, you can bind mount modules into the `/tmp/element-web-modules` directory at runtime. -The default entrypoint will be index.js in that directory but can be overriden if a package.json file is found with a `main` directive. - -The container expects a config.json file to be bind mounted or copied into the `/app/config.json` path. -The container runs an nginx web server in rootless mode on port 8080. -If you wish to use docker in read-only mode, you should follow the [upstream instructions](https://hub.docker.com/_/nginx#:~:text=Running%20nginx%20in%20read%2Donly%20mode) -but additionally include the following directories: - -- /tmp/element-web-modules/ -- /tmp/element-web-config/ -- /etc/nginx/conf.d/ - ## Using the API Modules are loaded by Element Web at runtime via a dynamic ecmascript import, but can be bundled into a webapp for deployment convenience. @@ -74,6 +52,10 @@ class ExampleModule implements Module { // ... ``` +## Releases + +The API is versioned using semver, with the major version incremented for breaking changes. + ## Copyright & License Copyright (c) 2025 New Vector Ltd diff --git a/packages/element-web-module-api/docker/docker-entrypoint.d/17-fetch-element-modules.sh b/packages/element-web-module-api/docker/docker-entrypoint.d/17-fetch-element-modules.sh deleted file mode 100755 index ff57e90d53..0000000000 --- a/packages/element-web-module-api/docker/docker-entrypoint.d/17-fetch-element-modules.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh - -# Loads modules specified via env var ELEMENT_WEB_MODULES -# in a comma delimited format of `url#hash,url#hash,...` -# the URL should point to a gzipped tarball, e.g. https://registry.npmjs.org/level/-/level-9.0.0.tgz -# the hash should a sha256sum of the tgz/tar.gz archive, it can be omitted though -# Runs both during the build stage and the runtime entrypoint for added flexibility - -set -e - -entrypoint_log() { - if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo "$@" - fi -} - -if [ -z "${ELEMENT_WEB_MODULES}" ]; then - entrypoint_log "ELEMENT_WEB_MODULES is not set, skipping module loading" - exit 0 -fi - -for MODULE in ${ELEMENT_WEB_MODULES//,/ } -do - MODULE_URL=${MODULE%#*} - EXPECTED_HASH=${MODULE#*#} - - entrypoint_log "Fetching module from $MODULE_URL" - wget -O /tmp/element-web-modules/module.tar.gz "$MODULE_URL" - HASH=$(sha256sum /tmp/element-web-modules/module.tar.gz | awk '{ print $1 }') - if [ -n "$EXPECTED_HASH" ] && [ "$HASH" != "$EXPECTED_HASH" ]; then - echo "Hash mismatch for $MODULE_URL: expected $EXPECTED_HASH, got $HASH" - exit 1 - fi - - mkdir -p "/tmp/element-web-modules/$HASH" - tar xvf /tmp/element-web-modules/module.tar.gz -C "/tmp/element-web-modules/$HASH" --strip-components=1 - rm -Rf /tmp/element-web-modules/module.tar.gz -done \ No newline at end of file diff --git a/packages/element-web-module-api/docker/docker-entrypoint.d/18-load-element-modules.sh b/packages/element-web-module-api/docker/docker-entrypoint.d/18-load-element-modules.sh deleted file mode 100755 index fe08194de0..0000000000 --- a/packages/element-web-module-api/docker/docker-entrypoint.d/18-load-element-modules.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh - -# Loads modules from `/tmp/element-web-modules` into config.json's `modules` field - -set -e - -entrypoint_log() { - if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo "$@" - fi -} - -# If there are modules to be loaded -if [ -d "/tmp/element-web-modules" ]; then - cd /tmp/element-web-modules - - # Copy these config files as a base - cp /app/config*.json /tmp/element-web-config/ - - for MODULE in * - do - # If the module has a package.json, use its main field as the entrypoint - ENTRYPOINT="index.js" - if [ -f "/tmp/element-web-modules/$MODULE/package.json" ]; then - ENTRYPOINT=$(jq -r '.main' "/tmp/element-web-modules/$MODULE/package.json") - fi - - entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT" - - # Append the module to the config - jq ".modules += [\"/modules/$MODULE/$ENTRYPOINT\"]" /tmp/element-web-config/config.json | sponge /tmp/element-web-config/config.json - done -fi \ No newline at end of file diff --git a/packages/element-web-module-api/docker/nginx-templates/default.conf.template b/packages/element-web-module-api/docker/nginx-templates/default.conf.template deleted file mode 100644 index 83813ddcd2..0000000000 --- a/packages/element-web-module-api/docker/nginx-templates/default.conf.template +++ /dev/null @@ -1,24 +0,0 @@ -server { - listen 8080; - server_name localhost; - - root /app; - index index.html; - - location = /index.html { - add_header Cache-Control "no-cache"; - } - location = /version { - add_header Cache-Control "no-cache"; - } - # covers config.json and config.hostname.json requests as it is prefix. - location /config { - root /tmp/element-web-config; - add_header Cache-Control "no-cache"; - } - location /modules { - root /tmp/element-web-modules; - } - error_page 500 502 503 504 /50x.html; -} - From f004a93e152b9b18ad3e3990c4993d9a77ecd5b6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 3 Mar 2025 12:30:57 +0000 Subject: [PATCH 3/4] Iterate --- packages/element-web-module-api/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/element-web-module-api/Dockerfile b/packages/element-web-module-api/Dockerfile index a858dc6aed..918ef81283 100644 --- a/packages/element-web-module-api/Dockerfile +++ b/packages/element-web-module-api/Dockerfile @@ -20,4 +20,4 @@ FROM ghcr.io/element-hq/element-web:${ELEMENT_VERSION} ARG ELEMENT_VERSION=latest ARG BUILD_CONTEXT -COPY --from=builder /modules/* /tmp/element-web-modules/ \ No newline at end of file +COPY --from=builder /modules /tmp/element-web-modules/ \ No newline at end of file From ea906ed8a999886f704f2dfa2290e3c617ffb680 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 3 Mar 2025 13:09:10 +0000 Subject: [PATCH 4/4] Iterate --- packages/element-web-module-api/Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/element-web-module-api/Dockerfile b/packages/element-web-module-api/Dockerfile index 918ef81283..93d2e32ab4 100644 --- a/packages/element-web-module-api/Dockerfile +++ b/packages/element-web-module-api/Dockerfile @@ -17,7 +17,5 @@ RUN mkdir /modules RUN cp -r ./$BUILD_CONTEXT/lib/ /modules/$(jq -r '"\(.name)-v\(.version)"' ./$BUILD_CONTEXT/package.json) FROM ghcr.io/element-hq/element-web:${ELEMENT_VERSION} -ARG ELEMENT_VERSION=latest -ARG BUILD_CONTEXT COPY --from=builder /modules /tmp/element-web-modules/ \ No newline at end of file