Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ns8-module.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ jobs:
python3 -c "import json, glob; [json.load(open(f)) for f in glob.glob('imageroot/actions/*/*.json') + glob.glob('ui/public/*.json') + ['ui/public/i18n/en/translation.json']]"
bash -n imageroot/actions/configure-module/80start_services
bash -n build-images.sh
grep -rn '__CALNODE_APP_IMAGE__' imageroot/systemd/user/calnode.service
# No build-time placeholders may remain: the unit and configure read
# the app ref from ${CALNODE_IMAGE} at install time.
! grep -rn '__CALNODE_APP_IMAGE__' imageroot/
# Action scripts must be committed executable: the agent silently
# skips non-executable files, and fileMode=false checkouts do not
# record chmod, so assert the indexed modes, not just the worktree.
git ls-files -s -- imageroot/actions | awk '$1=="100644" && $4 !~ /validate-(input|output)\.json$/ {print "not executable in index:", $4; bad=1} END{exit bad}'

build:
needs: validate
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,5 +246,6 @@ at an unconditional `"zoom"`.
- The NS8 module is a thin wrapper around the published app image, built by
`.github/workflows/ns8-module.yml` with the same tag scheme as
`docker-publish.yml`. Release tags pin module and app to the same version.
- The committed systemd unit keeps a `__CALNODE_APP_IMAGE__` placeholder;
`packaging/ns8/build-images.sh` stamps the real app ref at build time.
- The app image ref lives in the `org.nethserver.images` label; at install
the cluster exposes it as `${CALNODE_IMAGE}` and the unit + configure read
it from there. Never hardcode an app tag in `packaging/ns8/`.
26 changes: 16 additions & 10 deletions packaging/ns8/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,14 @@ into the `calnode-data` volume, then pass its key explicitly, e.g.
not expose the key field; cluster backup/restore never needs it (the key
travels inside the backed-up module state).

UNTESTED: no live NS8 node was reachable while writing this, so install,
configure, update, backup and restore steps above are documented from the
NS8 module layout but have not been run against a real cluster. The
automated test suite in `tests/` covers install, configure, HTTP check and
removal once a node is available.
Live-tested 2026-10-13 on a single-node Rocky 9 cluster against the
0.10.1-pinned build: install, configure, host-based route (root 302,
`/admin/` 200 through Traefik), SQLite on the `calnode-data` volume,
encryption key byte-identical across reconfigures, and `remove-module
--no-preserve` with automatic route cleanup. Still untested live: cluster
backup/restore (no backup destination was available on the test node) and
the automated `tests/` robot suite, which covers install, configure, HTTP
check and removal once a node is available.

## Uninstall

Expand All @@ -108,7 +111,7 @@ the same CI as the app. The root workflow `.github/workflows/ns8-module.yml`
builds the module image on every PR (no push) and publishes on branch and
tag pushes, mirroring the app tags in `docker-publish.yml`:

- push to `main` pushes module `:edge` and `:latest`, running app `:edge`
- push to `main` pushes module `:edge`, running app `:edge`
- push to `dev` pushes module `:dev`, running app `:dev`
- push a `vX.Y.Z` tag pushes module `:X.Y.Z`, `:X.Y` and `:latest`, running
app `:X.Y.Z`, so a module release always runs the matching app release
Expand All @@ -121,13 +124,16 @@ bash build-images.sh

It prints the `buildah push` commands for your registry. Override the app
reference per build with e.g. `APP_IMAGE=ghcr.io/calnode/calnode:1.2.3
IMAGETAGS="1.2.3 latest" PUSH=1 bash build-images.sh`. The systemd unit in
the tree keeps a `__CALNODE_APP_IMAGE__` placeholder that the script stamps
at build time, so the committed files never name an app tag.
IMAGETAGS="1.2.3 latest" PUSH=1 bash build-images.sh`. `APP_IMAGE` only sets
the `org.nethserver.images` label: at install the cluster writes that same
reference into the module environment as `CALNODE_IMAGE`, which is what the
systemd unit and configure-module actually run. The label is the single
source of truth; nothing is stamped at build time.

Running the test suite needs an NS8 test cluster; see the
[NS8 testing docs](https://github.com/NethServer/ns8-github-actions/blob/v1/README.md#running-tests-locally).
UNTESTED here: no test cluster was available.
From this directory: `run-ns8-tests <NODE_ADDR> ghcr.io/calnode/ns8-calnode:latest`.
Not yet run in CI: no test cluster is wired up.

## UI translation

Expand Down
26 changes: 8 additions & 18 deletions packaging/ns8/build-images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
# APP_IMAGE Calnode app image the module runs, default
# ghcr.io/calnode/calnode:latest. Release CI passes the app
# tag matching the release, so module 1.2.3 always runs app
# 1.2.3 and the two can never drift apart.
# 1.2.3 and the two can never drift apart. APP_IMAGE is only
# used for the org.nethserver.images label: the systemd unit
# reads the exact reference from ${CALNODE_IMAGE}, which the
# cluster writes into the module environment at install time
# from that same label. One source of truth, no stamping.
# IMAGETAGS space-separated module tags to push, default "latest"
# PUSH=1 push after building. Default 0 prints the push commands.
#
Expand All @@ -36,22 +40,9 @@ reponame="ns8-calnode"
app_image="${APP_IMAGE:-ghcr.io/calnode/calnode:latest}"
read -r -a imagetags <<< "${IMAGETAGS:-latest}"

# Stage imageroot with the app image reference stamped in, so the committed
# tree keeps a placeholder and releases pin the exact app tag. Both the
# systemd unit and configure-module's warm-pull default are stamped.
stage=$(mktemp -d)
trap 'rm -rf "${stage}"' EXIT
cp -r imageroot "${stage}/imageroot"
sed -i "s#__CALNODE_APP_IMAGE__#${app_image}#g" \
"${stage}/imageroot/systemd/user/calnode.service" \
"${stage}/imageroot/actions/configure-module/20configure"
if grep -q "__CALNODE_APP_IMAGE__" -r "${stage}/imageroot"; then
echo "App image placeholder was not fully substituted." >&2
exit 1
fi

# Create a new empty container image
# Add imageroot directory to the container image
container=$(buildah from scratch)
buildah add "${container}" imageroot /imageroot

# Reuse existing nodebuilder-ns8-calnode container, to speed up builds
if ! buildah containers --format "{{.ContainerName}}" | grep -q nodebuilder-ns8-calnode; then
Expand All @@ -66,8 +57,7 @@ buildah run \
nodebuilder-ns8-calnode \
sh -c "corepack enable && yarn install && yarn build"

# Add the staged imageroot directory to the container image
buildah add "${container}" "${stage}/imageroot" /imageroot
# Add the UI files to the container image
buildah add "${container}" ui/dist /ui
# Setup the entrypoint, ask to reserve one TCP port with the label and set a rootless container
buildah config --entrypoint=/ \
Expand Down
15 changes: 10 additions & 5 deletions packaging/ns8/imageroot/actions/configure-module/20configure
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import agent.tasks
STATE_DIR = os.getcwd()
ENV_FILE = os.path.join(STATE_DIR, "calnode.env")
CONFIG_FILE = os.path.join(STATE_DIR, "config.json")
# Stamped at build time (build-images.sh) to the exact app ref this module
# release runs, so a pinned install warms the right image instead of :latest.
CALNODE_IMAGE = "__CALNODE_APP_IMAGE__"
# Written by the cluster into the module environment at install time from
# the org.nethserver.images label: the same reference the systemd unit runs.
CALNODE_IMAGE = os.environ.get("CALNODE_IMAGE", "ghcr.io/calnode/calnode:latest")


def read_env_file(path):
Expand Down Expand Up @@ -101,8 +101,13 @@ with open(CONFIG_FILE, "w", encoding="utf-8") as handle:
indent=2,
)

# Make sure the app image is present before the service starts.
agent.run_helper("podman", "pull", CALNODE_IMAGE).check_returncode()
# Make sure the app image is present before the service starts. The cluster
# normally pre-pulls it at install; tolerate a registry outage if it is
# already here so a transient network fault cannot block a reconfigure.
pulled = agent.run_helper("podman", "pull", CALNODE_IMAGE)
if pulled.returncode != 0:
present = agent.run_helper("podman", "image", "exists", CALNODE_IMAGE)
agent.assert_exp(present.returncode == 0)

# Register a host-based route. Since the "instance" attribute starts with
# MODULE_ID, the route is removed automatically with the module.
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"description": "Public fully qualified domain name Calnode is reached at",
"minLength": 1,
"maxLength": 253,
"pattern": "^(?=.{1,253}$)[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
"pattern": "^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
},
"lets_encrypt": {
"type": "boolean",
Expand Down
21 changes: 21 additions & 0 deletions packaging/ns8/imageroot/actions/create-module/validate-input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "create-module input",
"$id": "http://schema.nethserver.org/calnode/create-module-input.json",
"description": "Create calnode instance. Volumes and images arrive from the module labels; the cluster framework provisions the volume, pre-pulls the images and writes the *_IMAGE environment entries. No module script needed.",
"type": "object",
"properties": {
"images": {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
},
"volumes": {
"type": "object",
"description": "Optional mapping of application volume names to filesystem mount points."
}
},
"additionalProperties": false
}
Empty file modified packaging/ns8/imageroot/actions/get-configuration/20read
100644 → 100755
Empty file.
6 changes: 5 additions & 1 deletion packaging/ns8/imageroot/systemd/user/calnode.service
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
# users and settings are lost. TLS is terminated by the cluster Traefik
# instance, so the container only listens on 127.0.0.1.
#
# The app image reference comes from ${CALNODE_IMAGE}, which the cluster
# writes into the module environment at install time from the
# org.nethserver.images label. The label is the single source of truth.
#
[Unit]
Description=calnode server
After=network-online.target
Expand All @@ -33,7 +37,7 @@ ExecStart=/usr/bin/podman run \
--publish=127.0.0.1:${TCP_PORT}:3000 \
--volume=calnode-data:/data \
--env-file=calnode.env \
__CALNODE_APP_IMAGE__
${CALNODE_IMAGE}
# 45s grace: the app allows up to 30s HTTP drain plus worker shutdown, so the
# default 10s would SIGKILL updates mid-request. Data stays safe either way
# (jobs are crash-recoverable), this is about clean connection draining.
Expand Down
Loading