commit 9063241ee279a5e08369edb06d283a578e0a1d44
parent ed7f70629285f64901ab6d2080f5193ac6d10307
Author: Florian Dold <dold@taler.net>
Date: Sat, 22 Aug 2026 18:04:58 +0200
sandcastle: expose deployment status endpoint
Diffstat:
5 files changed, 104 insertions(+), 2 deletions(-)
diff --git a/Dockerfile b/Dockerfile
@@ -131,3 +131,9 @@ RUN sed -i /etc/postgresql/17/main/postgresql.conf -e 's/^port[ ]*=.*$/port = 54
# and thus fail, clobbering the systemd status.
RUN systemctl mask systemd-modules-load.service
RUN systemctl disable proc-sys-fs-binfmt_misc.automount
+
+# Keep the Sandcastle revision in its own final layer so revision-only changes
+# do not invalidate package installation and image configuration.
+ARG SANDCASTLE_VERSION=unknown
+RUN install -d /usr/share/sandcastle && \
+ printf '%s\n' "$SANDCASTLE_VERSION" >/usr/share/sandcastle/version
diff --git a/README.md b/README.md
@@ -142,6 +142,8 @@ Wait until everything has been set up:
While provisioning is running, this follows the output of
``setup-sandcastle.service`` inside the container. It exits successfully once
provisioning has completed, or with a non-zero status if provisioning fails.
+After a successful deployment, the landing host serves the Sandcastle version,
+component versions, and deployment time at ``/metrics/sandcastle-status``.
Note that ``./sandcastle-run`` is just a wrapper around ``podman run``.
If required, you can pass addtional arguments to ``./sandcastle-run``.
@@ -172,6 +174,13 @@ the services will be available as
This gives a nice environment for integration testing.
+# Running the Unit Tests
+
+Run the Python unit-test suite from the repository root:
+
+ python3 -m unittest discover -s tests -v
+
+
# Stopping the deployment
For stopping the deployment simply run
diff --git a/sandcastle-build b/sandcastle-build
@@ -38,6 +38,11 @@ if [[ ! -r $containerfile ]]; then
exit 1
fi
+sandcastle_version=unknown
+if git_version=$(git describe --tags --always --dirty 2>/dev/null); then
+ sandcastle_version=$git_version
+fi
+
builder_image=localhost/taler-sandcastle-package-builder:latest
cache_dir=$SCRIPT_DIR/cache
mkdir -p \
@@ -67,6 +72,7 @@ python3 buildscripts/sandcastle_build_packages.py \
echo "Building final sandcastle image"
exec "${build_args[@]}" "${no_cache_args[@]}" \
+ --build-arg "SANDCASTLE_VERSION=$sandcastle_version" \
--target taler-final \
--tag taler-base-all \
.
diff --git a/scripts/demo/setup-sandcastle.sh b/scripts/demo/setup-sandcastle.sh
@@ -40,7 +40,57 @@ update_config_block() {
fi
}
+SANDCASTLE_STATUS_DIR=/var/www/sandcastle/metrics
+SANDCASTLE_STATUS_FILE=$SANDCASTLE_STATUS_DIR/sandcastle-status
+
+# Atomically publish the version information for the status endpoint after a
+# successful deployment.
+write_sandcastle_status() {
+ local temporary_file
+ local sandcastle_version=unknown
+ local architecture
+ local component_dir
+ local component
+ local package
+ local versions
+ local turnstile_version
+
+ if [[ -s /usr/share/sandcastle/version ]]; then
+ sandcastle_version=$(</usr/share/sandcastle/version)
+ fi
+ architecture=$(dpkg --print-architecture)
+ mkdir -p "$SANDCASTLE_STATUS_DIR"
+ temporary_file=$(mktemp "$SANDCASTLE_STATUS_FILE.XXXXXX")
+
+ {
+ printf 'sandcastle: %s\n' "$sandcastle_version"
+ printf 'deployed-at: %s\n' "$(date --utc +%Y-%m-%dT%H:%M:%SZ)"
+ printf 'components:\n'
+ for component_dir in "/packages/$architecture"/*; do
+ [[ -d $component_dir ]] || continue
+ component=${component_dir##*/}
+ versions=$(
+ for package in "$component_dir"/*.deb; do
+ [[ -f $package ]] || continue
+ dpkg-deb --field "$package" Version
+ done | sort --unique | paste --serial --delimiters=,
+ )
+ [[ -n $versions ]] || continue
+ printf ' %s: %s\n' "$component" "$versions"
+ done
+ if turnstile_version=$(
+ git -C /opt/turnstile describe --tags --always --dirty 2>/dev/null
+ ); then
+ printf ' turnstile: %s\n' "$turnstile_version"
+ fi
+ } >"$temporary_file"
+
+ chmod 644 "$temporary_file"
+ mv "$temporary_file" "$SANDCASTLE_STATUS_FILE"
+}
+
echo "Provisioning sandcastle"
+rm -f "$SANDCASTLE_STATUS_FILE" "$SANDCASTLE_STATUS_FILE".*
# General configuration.
# Might eventually be moved to an external file.
@@ -132,6 +182,7 @@ PORT_INTERNAL_EXCHANGE=8201
PORT_INTERNAL_MERCHANT=8301
PORT_INTERNAL_LIBEUFIN_BANK=8080
PORT_INTERNAL_LANDING=8501
+PORT_INTERNAL_LANDING_SERVICE=8511
PORT_INTERNAL_BLOG=8502
PORT_INTERNAL_DONATIONS=8503
PORT_INTERNAL_BANK_SPA=8505
@@ -481,7 +532,7 @@ CURRENCY = $CURRENCY
[frontend-demo-landing]
SERVE = http
-HTTP_PORT = $PORT_INTERNAL_LANDING
+HTTP_PORT = $PORT_INTERNAL_LANDING_SERVICE
[frontend-demo-blog]
SERVE = http
@@ -658,6 +709,17 @@ cat <<EOF >/etc/caddy/Caddyfile
}
}
+(sandcastle_landing) {
+ handle /metrics/sandcastle-status {
+ root * /var/www/sandcastle
+ header Content-Type "text/plain; charset=utf-8"
+ file_server
+ }
+ handle {
+ reverse_proxy :$PORT_INTERNAL_LANDING_SERVICE
+ }
+}
+
# Services that only listen on unix domain sockets
# are reverse-proxied to serve on a TCP port.
@@ -678,6 +740,10 @@ cat <<EOF >/etc/caddy/Caddyfile
}
}
+:$PORT_INTERNAL_LANDING {
+ import sandcastle_landing
+}
+
:$PORT_INTERNAL_BANK_SPA {
root * /usr/share/libeufin/spa
root /settings.json /etc/libeufin/
@@ -734,6 +800,11 @@ https://$BANK_DOMAIN {
}
}
+https://$LANDING_DOMAIN {
+ tls internal
+ import sandcastle_landing
+}
+
https://$EXCHANGE_DOMAIN {
tls internal
reverse_proxy unix//run/taler-exchange/httpd/exchange-http.sock
@@ -805,7 +876,7 @@ http://$CHALLENGER_DOMAIN$PORT_SUFFIX {
}
http://$LANDING_DOMAIN$PORT_SUFFIX {
- reverse_proxy :$PORT_INTERNAL_LANDING
+ import sandcastle_landing
}
http://$BLOG_DOMAIN$PORT_SUFFIX {
@@ -1443,5 +1514,7 @@ fi
cd /
+write_sandcastle_status
+
# FIXME: Maybe do some taler-wallet-cli test?
# FIXME: How do we report errors occurring during the setup script?
diff --git a/tests/test_sandcastle_build_packages.py b/tests/test_sandcastle_build_packages.py
@@ -254,6 +254,11 @@ class BuildOrchestrationTests(unittest.TestCase):
executable = root / "bin" / command
executable.write_text(fake_command, encoding="utf-8")
executable.chmod(0o755)
+ git = root / "bin" / "git"
+ git.write_text(
+ "#!/bin/sh\nprintf 'sandcastle-version\\n'\n", encoding="utf-8"
+ )
+ git.chmod(0o755)
environment = os.environ.copy()
environment["PATH"] = f"{root / 'bin'}:{environment['PATH']}"
@@ -274,6 +279,9 @@ class BuildOrchestrationTests(unittest.TestCase):
self.assertIn("<buildscripts/sandcastle_build_packages.py>", calls[1])
self.assertIn("<--force>", calls[1])
self.assertIn("<--target> <taler-final>", calls[2])
+ self.assertIn(
+ "<--build-arg> <SANDCASTLE_VERSION=sandcastle-version>", calls[2]
+ )
self.assertIn("<--no-cache>", calls[2])
log.write_text("", encoding="utf-8")