Repository navigation
packaging: add in-tree NS8 module (packaging/ns8) #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| name: NS8 module publish | ||
|
|
||
| # Builds the ns8-calnode module image from packaging/ns8. The module is a thin | ||
| # wrapper: this image carries configuration scripts and the settings UI, while | ||
| # the Calnode app itself is pulled from its own registry at install time. | ||
| # * PR → validate + build only. Nothing is pushed. | ||
| # * push to dev → push `:dev`, running app `:dev`. | ||
| # * push to main → push `:edge`, running app `:edge` (`:latest` stays | ||
| # pinned to releases, mirroring docker-publish.yml). | ||
| # * push a `vX.Y.Z` tag → push `:X.Y.Z`, `:X.Y` and `:latest`, running app | ||
| # `:X.Y.Z`. Module and app tags move together, so a | ||
| # module release can never point at the wrong app. | ||
| on: | ||
| push: | ||
| branches: [main, dev] | ||
| tags: ['v*.*.*'] | ||
| pull_request: | ||
|
|
||
| concurrency: | ||
| group: ns8-module-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| validate: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Validate action scripts and schemas | ||
| working-directory: packaging/ns8 | ||
| run: | | ||
| python3 -m py_compile imageroot/actions/configure-module/20configure imageroot/actions/get-configuration/20read | ||
| 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 | ||
|
|
||
| build: | ||
| needs: validate | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| packages: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| # Mirror the app tagging in docker-publish.yml: release tags pin both | ||
| # images to the same version, branches track the moving app tags. | ||
| - name: Resolve tags | ||
| id: tags | ||
| run: | | ||
| if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then | ||
| version="${GITHUB_REF_NAME#v}" | ||
| echo "module_tags=${version} ${version%.*} latest" >> "$GITHUB_OUTPUT" | ||
| echo "app_image=ghcr.io/calnode/calnode:${version}" >> "$GITHUB_OUTPUT" | ||
| elif [[ "${GITHUB_REF}" == refs/heads/main ]]; then | ||
| echo "module_tags=edge" >> "$GITHUB_OUTPUT" | ||
| echo "app_image=ghcr.io/calnode/calnode:edge" >> "$GITHUB_OUTPUT" | ||
| elif [[ "${GITHUB_REF}" == refs/heads/dev ]]; then | ||
| echo "module_tags=dev" >> "$GITHUB_OUTPUT" | ||
| echo "app_image=ghcr.io/calnode/calnode:dev" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "module_tags=build" >> "$GITHUB_OUTPUT" | ||
| echo "app_image=ghcr.io/calnode/calnode:edge" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| # buildah is preinstalled on the ubuntu runners, same as the upstream | ||
| # NS8 publish workflow assumes. | ||
| - name: Build (push on branch pushes and on tags) | ||
| working-directory: packaging/ns8 | ||
| env: | ||
| REPOBASE: ghcr.io/${{ github.repository_owner }} | ||
| APP_IMAGE: ${{ steps.tags.outputs.app_image }} | ||
| IMAGETAGS: ${{ steps.tags.outputs.module_tags }} | ||
| run: | | ||
| push=0 | ||
| if [ "${{ github.event_name }}" != "pull_request" ]; then | ||
| push=1 | ||
| buildah login -u "${{ github.actor }}" --password-stdin ghcr.io <<<"${{ secrets.GITHUB_TOKEN }}" | ||
| fi | ||
| PUSH="$push" bash build-images.sh |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| # ns8-calnode | ||
|
|
||
| NethServer 8 community module wrapping [Calnode](https://github.com/calnode/calnode), | ||
| the self-hostable scheduling and booking platform. Install it on your cluster | ||
| with one command and get Calnode on your own domain. No Calnode docs required. | ||
|
|
||
| ## How it works | ||
|
|
||
| This module is a thin wrapper around the published Calnode container image | ||
| (`ghcr.io/calnode/calnode:latest`). Nothing is ported or rebuilt: the module | ||
| image only carries configuration scripts and the settings UI. At install time | ||
| the cluster pulls the Calnode image and runs it as a rootless container. | ||
|
|
||
| Key facts: | ||
|
|
||
| - App listens on port 3000 inside the container, published on 127.0.0.1 only. | ||
| There is no inbound port beyond the app itself. | ||
| - TLS is handled by the cluster Traefik proxy, not the module. Set the | ||
| hostname and the module registers a host-based route with Let's Encrypt. | ||
| - The whole database is one SQLite file on the `calnode-data` named volume. | ||
| The volume survives module updates. Never delete it unless you mean to | ||
| wipe all bookings, users and settings. | ||
| - First configuration generates a `CALNODE_ENCRYPTION_KEY` that unlocks the | ||
| database key vault. It is stored in the module state and covered by cluster | ||
| backup. If you lose both the volume and this key, bookings become | ||
| unreadable, so keep backups working. | ||
|
|
||
| ## Install | ||
|
|
||
| On the cluster leader, as root: | ||
|
|
||
| ``` | ||
| add-module ghcr.io/calnode/ns8-calnode:latest 1 | ||
| ``` | ||
|
|
||
| Replace `calnode` with the actual registry namespace if you built your own | ||
| image (see Development). The command prints the instance name, usually | ||
| `ns8-calnode1`. | ||
|
|
||
| Then configure it with your public hostname: | ||
|
|
||
| ``` | ||
| api-cli run module/ns8-calnode1/configure-module --data '{"host": "book.example.com", "lets_encrypt": true, "http2https": true}' | ||
| ``` | ||
|
|
||
| Or open the cluster admin UI, find the instance, and fill in the Settings page: | ||
| hostname, Let's Encrypt toggle, HTTP to HTTPS redirect toggle. | ||
|
|
||
| Calnode is then live at `https://book.example.com`. Sign up the first admin | ||
| account in the web UI. Mail delivery (invites, reminders) is configured | ||
| inside Calnode under Settings, using your SMTP smarthost values. | ||
|
|
||
| ## Update | ||
|
|
||
| Update from the Software Center like any other module. The container image is | ||
| swapped and the service restarts; the `calnode-data` volume is untouched, so | ||
| bookings, users and settings carry over. New module releases track new | ||
| Calnode app releases. | ||
|
|
||
| To move an existing install to a new hostname, just run `configure-module` | ||
| again (or save the Settings page) with the new host. The encryption key and | ||
| the database are preserved. | ||
|
|
||
| ## Backup and restore | ||
|
|
||
| Backups run through the standard cluster backup in the cluster admin UI. | ||
| Each backup of the instance contains: | ||
|
|
||
| - the `calnode-data` volume (the SQLite database file, the whole app state) | ||
| - the module state, including `calnode.env` with `CALNODE_ENCRYPTION_KEY` | ||
|
|
||
| Restore to the same or a replacement node from the cluster admin backup | ||
| section, then re-run `configure-module` with the same hostname if the route | ||
| needs recreating. Keep at least one tested backup: the encryption key and | ||
| the database are only useful together. | ||
|
|
||
| Adopting data from a non-NS8 install is CLI-only: import the SQLite file | ||
| into the `calnode-data` volume, then pass its key explicitly, e.g. | ||
| `api-cli run module/<id>/configure-module --data '{"host": "...", | ||
| "encryption_key": "<key from the old install>"}'`. The Settings page does | ||
| 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. | ||
|
|
||
| ## Uninstall | ||
|
|
||
| To uninstall the instance, keeping its data for a later reinstall: | ||
|
|
||
| ``` | ||
| remove-module --preserve ns8-calnode1 | ||
| ``` | ||
|
|
||
| To wipe everything including bookings: | ||
|
|
||
| ``` | ||
| remove-module --no-preserve ns8-calnode1 | ||
| ``` | ||
|
|
||
| ## Development | ||
|
|
||
| This module lives in the Calnode repo under `packaging/ns8/` and ships from | ||
| 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 `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 | ||
|
|
||
| To build locally on a machine with podman and buildah, from this directory: | ||
|
|
||
| ``` | ||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| ## UI translation | ||
|
|
||
| Translated with [Weblate](https://hosted.weblate.org/projects/ns8/). | ||
|
|
||
| To setup the translation process: | ||
|
|
||
| - add [GitHub Weblate app](https://docs.weblate.org/en/latest/admin/code-hosting.html#code-hosting-github-notifications) to your repository | ||
| - add your repository to [hosted.weblate.org](https://hosted.weblate.org) or ask a NethServer developer to add it to ns8 Weblate project | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| #!/bin/bash | ||
|
|
||
| # | ||
| # Copyright (C) 2023 Nethesis S.r.l. | ||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||
| # | ||
|
|
||
| # Build the ns8-calnode module image and optionally push it. | ||
| # | ||
| # Run from this directory (packaging/ns8). Environment: | ||
| # REPOBASE module image namespace, default ghcr.io/calnode | ||
| # 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. | ||
| # IMAGETAGS space-separated module tags to push, default "latest" | ||
| # PUSH=1 push after building. Default 0 prints the push commands. | ||
| # | ||
| # The module image only carries configuration scripts and the settings UI. | ||
| # The Calnode application itself is pulled from APP_IMAGE at install time. | ||
|
|
||
| # Terminate on error | ||
| set -e | ||
|
|
||
| # Must run from packaging/ns8 (uses relative imageroot and ui paths) | ||
| if [ ! -d imageroot ] || [ ! -d ui ]; then | ||
| echo "Run build-images.sh from the packaging/ns8 directory." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Prepare variables for later use. Registry namespaces must be lowercase | ||
| # (docker:// rejects uppercase); normalize up front so every use below is safe. | ||
| repobase="${REPOBASE:-ghcr.io/calnode}" | ||
| repobase="${repobase,,}" | ||
| 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 | ||
| container=$(buildah from scratch) | ||
|
|
||
| # Reuse existing nodebuilder-ns8-calnode container, to speed up builds | ||
| if ! buildah containers --format "{{.ContainerName}}" | grep -q nodebuilder-ns8-calnode; then | ||
| echo "Pulling NodeJS runtime..." | ||
| buildah from --name nodebuilder-ns8-calnode -v "${PWD}:/usr/src:Z" docker.io/library/node:24.21.0-slim | ||
| fi | ||
|
|
||
| echo "Build static UI files with node..." | ||
| buildah run \ | ||
| --workingdir=/usr/src/ui \ | ||
| --env="NODE_OPTIONS=--openssl-legacy-provider" \ | ||
| 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 | ||
| 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=/ \ | ||
| --label="org.nethserver.authorizations=traefik@node:routeadm" \ | ||
| --label="org.nethserver.tcp-ports-demand=1" \ | ||
| --label="org.nethserver.rootfull=0" \ | ||
| --label="org.nethserver.volumes=calnode-data" \ | ||
| --label="org.nethserver.images=${app_image}" \ | ||
| "${container}" | ||
| # Commit the image (repobase already lowercased at variable prep above). | ||
| buildah commit "${container}" "${repobase}/${reponame}:build" | ||
|
|
||
| if [ "${PUSH:-0}" = "1" ]; then | ||
| for imagetag in "${imagetags[@]}"; do | ||
| buildah tag "${repobase}/${reponame}:build" "${repobase}/${reponame}:${imagetag}" | ||
| buildah push "${repobase}/${reponame}:${imagetag}" "docker://${repobase}/${reponame}:${imagetag}" | ||
| done | ||
| else | ||
| printf "Built %s:%s (app %s).\nPublish with:\n\n" "${repobase}/${reponame}" "build" "${app_image}" | ||
| for imagetag in "${imagetags[@]}"; do | ||
| printf " buildah tag %s:build %s:%s\n" "${repobase}/${reponame}" "${repobase}/${reponame}" "${imagetag}" | ||
| printf " buildah push %s:%s docker://%s:%s\n" "${repobase}/${reponame}" "${imagetag}" "${repobase}/${reponame}" "${imagetag}" | ||
| done | ||
| printf "\nOr rerun with PUSH=1 IMAGETAGS=\"...\" to push directly.\n" | ||
| fi |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This bullet now contradicts the workflow.
ff47709changedmainto publish:edgeonly, and the workflow's own header comment says:lateststays pinned to releases — but the Development section still advertises a moving:latestonmain. Please dropand :latesthere so the docs matchns8-module.yml.