diff --git a/.forgejo/workflows/build-container-nix.yaml b/.forgejo/workflows/build-container-nix.yaml index f66691f..3c041a6 100644 --- a/.forgejo/workflows/build-container-nix.yaml +++ b/.forgejo/workflows/build-container-nix.yaml @@ -1,17 +1,17 @@ # Nix container build workflow -# Triggers on tags matching: -nix-v -# Builds from containers//default.nix using nix build -# Pushes to Zot registry via skopeo +# Triggers on tags matching: -v +# Builds from containers//default.nix if it exists, skips otherwise +# Pushes to Zot registry via skopeo with -nix image tag suffix # # Examples: -# nettest-nix-v1.0.0 -> builds containers/nettest/default.nix -# myapp-nix-v2.1.0 -> builds containers/myapp/default.nix +# nettest-v1.0.0 -> builds containers/nettest/default.nix, pushes :v1.0.0-nix +# devpi-v2.1.0 -> skips (no default.nix) name: Build Container (Nix) on: push: tags: - - '*-nix-v[0-9]*' + - '*-v[0-9]*' jobs: build: @@ -23,10 +23,10 @@ jobs: TAG="${GITHUB_REF_NAME}" echo "Tag: $TAG" - # Extract container name (everything before -nix-v) - # e.g., "nettest-nix-v1.0.0" -> "nettest" - CONTAINER="${TAG%-nix-v[0-9]*}" - VERSION="${TAG#"${CONTAINER}"-nix-}" + # Extract container name (everything before -v) + # e.g., "nettest-v1.0.0" -> "nettest", "my-app-v2.0.0" -> "my-app" + CONTAINER="${TAG%-v[0-9]*}" + VERSION="${TAG#"${CONTAINER}"-}" echo "container=$CONTAINER" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" @@ -46,33 +46,29 @@ jobs: echo "Found $CONTEXT/default.nix" echo "exists=true" >> "$GITHUB_OUTPUT" else - echo "No default.nix found at $CONTEXT/default.nix" + echo "No default.nix found at $CONTEXT/default.nix — skipping" echo "exists=false" >> "$GITHUB_OUTPUT" fi - - name: Skip if container not found - if: steps.check.outputs.exists != 'true' + - name: Resolve nixpkgs + if: steps.check.outputs.exists == 'true' + id: nixpkgs run: | - echo "========================================" - echo "Nix container not found: ${{ steps.parse.outputs.container }}" - echo "========================================" - echo "" - echo "Tag '${{ github.ref_name }}' does not match any nix container in containers/" - echo "" - echo "Available nix containers:" - for nix in containers/*/default.nix; do - [ -f "$nix" ] && echo " - $(basename "$(dirname "$nix")")" - done - echo "" - echo "Skipping build." + # Resolve nixpkgs from the flake registry for lookup + NIXPKGS_PATH=$(nix flake metadata nixpkgs --json | jq -r '.path') + echo "Resolved nixpkgs: $NIXPKGS_PATH" + echo "path=$NIXPKGS_PATH" >> "$GITHUB_OUTPUT" - name: Build with nix if: steps.check.outputs.exists == 'true' id: build + env: + NIX_PATH: "nixpkgs=${{ steps.nixpkgs.outputs.path }}" run: | CONTAINER="${{ steps.parse.outputs.container }}" echo "Building containers/$CONTAINER/default.nix" - nix build -f "containers/$CONTAINER/default.nix" -o result + echo "NIX_PATH=$NIX_PATH" + nix-build "containers/$CONTAINER/default.nix" -o result echo "Build complete: $(readlink result)" - name: Push to registry @@ -80,7 +76,7 @@ jobs: run: | CONTAINER="${{ steps.parse.outputs.container }}" VERSION="${{ steps.parse.outputs.version }}" - IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:$VERSION" + IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:$VERSION-nix" echo "Pushing to $IMAGE" skopeo copy \ diff --git a/.forgejo/workflows/build-container.yaml b/.forgejo/workflows/build-container.yaml index b76978f..98231cf 100644 --- a/.forgejo/workflows/build-container.yaml +++ b/.forgejo/workflows/build-container.yaml @@ -17,7 +17,6 @@ on: jobs: build: - if: "!contains(github.ref_name, '-nix-v')" runs-on: k8s steps: - name: Parse tag diff --git a/containers/nettest/default.nix b/containers/nettest/default.nix new file mode 100644 index 0000000..4520804 --- /dev/null +++ b/containers/nettest/default.nix @@ -0,0 +1,39 @@ +# Nix-built nettest container +# Equivalent to the Dockerfile: curl, jq, bind (nslookup), ca-certs, bash +# Built with dockerTools.buildLayeredImage for efficient layer caching +{ pkgs ? import { } }: + +let + testScript = ./test-connectivity.sh; + + tools = pkgs.buildEnv { + name = "nettest-tools"; + paths = [ + pkgs.curl + pkgs.jq + pkgs.dnsutils # provides nslookup, dig + pkgs.cacert + pkgs.coreutils + pkgs.hostname + pkgs.bashInteractive + ]; + }; +in +pkgs.dockerTools.buildLayeredImage { + name = "blumeops/nettest"; + tag = "latest"; + + contents = [ tools ]; + + extraCommands = '' + cp ${testScript} test-connectivity.sh + chmod +x test-connectivity.sh + ''; + + config = { + Entrypoint = [ "/bin/bash" "/test-connectivity.sh" ]; + Env = [ + "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" + ]; + }; +} diff --git a/docs/changelog.d/feature-nettest-nix-container.feature.md b/docs/changelog.d/feature-nettest-nix-container.feature.md new file mode 100644 index 0000000..b529de2 --- /dev/null +++ b/docs/changelog.d/feature-nettest-nix-container.feature.md @@ -0,0 +1 @@ +Added Nix container build for nettest, validating the full nix-container-builder pipeline on ringtail. One git tag now triggers both Dockerfile and Nix workflows — each skips if its build file is absent. Rewrote container-tag-and-release as a typer CLI with --dry-run support. Added container policy.json and registries.conf to ringtail for skopeo. diff --git a/docs/how-to/deployment/build-container-image.md b/docs/how-to/deployment/build-container-image.md index 3086b23..18233eb 100644 --- a/docs/how-to/deployment/build-container-image.md +++ b/docs/how-to/deployment/build-container-image.md @@ -1,6 +1,6 @@ --- title: Build Container Image -modified: 2026-02-15 +modified: 2026-02-19 last-reviewed: 2026-02-15 tags: - how-to @@ -14,30 +14,35 @@ How to create a custom container image in BlumeOps, build it locally, and releas ## Prerequisites -- [Dagger CLI](https://docs.dagger.io/install) installed locally -- A Dockerfile for the service you want to build +- [Dagger CLI](https://docs.dagger.io/install) installed locally (for Dockerfile builds) +- A `Dockerfile` and/or `default.nix` for the service ## 1. Create the container directory -Add a `Dockerfile` (and any supporting files) under `containers//`: +Add build files under `containers//`: ``` containers// -├── Dockerfile +├── Dockerfile (built by Dagger on the k8s runner) +├── default.nix (built by nix-build on the ringtail runner) └── (optional scripts, configs) ``` -The directory name becomes the image name: `registry.ops.eblu.me/blumeops/`. +A container can have one or both build files. The directory name becomes the image name: `registry.ops.eblu.me/blumeops/`. ## 2. Build locally -Test your image with Dagger: +**Dockerfile** — test with Dagger: ```bash dagger call build --src=. --container-name= ``` -This builds `containers//Dockerfile` using the Dagger `docker_build()` function. Fix any build errors before proceeding. +**Nix** — test with nix-build (requires nix, e.g. on [[ringtail]]): + +```bash +nix-build containers//default.nix -o result +``` ## 3. Release @@ -47,7 +52,14 @@ Once the image builds cleanly, create a tagged release: mise run container-tag-and-release v1.0.0 ``` -This creates a git tag `-v1.0.0` and pushes it. The `build-container` Forgejo workflow triggers on the tag, builds the image via Dagger, and publishes it to the registry as `registry.ops.eblu.me/blumeops/:v1.0.0`. +Use `--dry-run` to preview without creating tags. + +This creates a single git tag `-v1.0.0` and pushes it. Both Forgejo workflows trigger on the tag — each checks for its build file and skips if not present: + +| Build file | Workflow | Runner | Registry tag | +|------------|----------|--------|--------------| +| `Dockerfile` | `build-container.yaml` | `k8s` (indri) | `:v1.0.0` | +| `default.nix` | `build-container-nix.yaml` | `nix-container-builder` ([[ringtail]]) | `:v1.0.0-nix` | Check available images and tags with: @@ -76,6 +88,7 @@ Existing containers demonstrate several build approaches: | Multi-stage with Node + Go | [[#navidrome]] | Separate UI and backend build stages | | Multi-stage Elixir | [[#teslamate]] | Elixir release with Node assets | | Runtime tarball download | [[#kiwix-serve]] | Download pre-built binary with arch detection | +| Nix `dockerTools` | [[#nettest-nix]] | `buildLayeredImage` with nixpkgs tools | ### transmission @@ -97,6 +110,10 @@ Existing containers demonstrate several build approaches: `containers/kiwix-serve/Dockerfile` — Downloads a pre-built binary from upstream, with architecture detection for cross-platform support. +### nettest (nix) + +`containers/nettest/default.nix` — Uses `dockerTools.buildLayeredImage` with `buildEnv` to merge nixpkgs tools (curl, jq, dnsutils, bash). Runs alongside the existing Dockerfile; the nix variant is tagged `:version-nix` in the registry. + ## Related - [[deploy-k8s-service]] — Deploying the service that uses the image diff --git a/docs/reference/infrastructure/ringtail.md b/docs/reference/infrastructure/ringtail.md index 45bc757..f6e0cc3 100644 --- a/docs/reference/infrastructure/ringtail.md +++ b/docs/reference/infrastructure/ringtail.md @@ -1,6 +1,6 @@ --- title: Ringtail -modified: 2026-02-18 +modified: 2026-02-19 tags: - infrastructure - host @@ -81,7 +81,7 @@ argocd cluster add default --name k3s-ringtail ### Forgejo Actions Runner -A native Forgejo Actions runner (`ringtail-nix-builder`) runs as a systemd service via the NixOS `services.gitea-actions-runner` module. It builds containers using `nix build` and pushes them to Zot via `skopeo`. +A native Forgejo Actions runner (`ringtail-nix-builder`) runs as a systemd service via the NixOS `services.gitea-actions-runner` module. It builds containers using `nix-build` and pushes them to Zot via `skopeo`. | Property | Value | |----------|-------| @@ -89,6 +89,9 @@ A native Forgejo Actions runner (`ringtail-nix-builder`) runs as a systemd servi | **Execution** | Host (no containers) | | **Token** | `/etc/forgejo-runner/token.env` (provisioned by Ansible) | | **Service unit** | `gitea-runner-nix_container_builder.service` | +| **Host packages** | bash, coreutils, curl, gawk, git, gnused, jq, nodejs, wget, nix, skopeo | + +The runner resolves `` from the flake registry at build time. Container trust policy (`/etc/containers/policy.json`) and registry search order (`/etc/containers/registries.conf`) are configured minimally in `configuration.nix` for skopeo — no full `virtualisation.containers` module needed. ## Maintenance Notes diff --git a/docs/reference/services/forgejo.md b/docs/reference/services/forgejo.md index db84339..e7c0b10 100644 --- a/docs/reference/services/forgejo.md +++ b/docs/reference/services/forgejo.md @@ -1,6 +1,6 @@ --- title: Forgejo -modified: 2026-02-08 +modified: 2026-02-19 tags: - service - git @@ -31,15 +31,20 @@ Git forge and CI/CD platform. **Primary source of truth for blumeops** (mirrored ## CI/CD (Forgejo Actions) -**Runner:** Kubernetes pod with Docker-in-Docker sidecar -- Namespace: `forgejo-runner` -- Labels: `k8s` -- ArgoCD app: `forgejo-runner` +**Runners:** + +| Runner | Host | Labels | Purpose | +|--------|------|--------|---------| +| k8s DinD pod | [[indri]] (minikube) | `k8s` | Dockerfile builds via Dagger | +| ringtail-nix-builder | [[ringtail]] (native) | `nix-container-builder` | Nix builds via `nix-build` + `skopeo` | **Workflows:** `.forgejo/workflows/` -- `build-container.yaml` - Container image builds on tag +- `build-container.yaml` - Dockerfile builds on tag (runs on `k8s`) +- `build-container-nix.yaml` - Nix builds on tag (runs on `nix-container-builder`) - `build-blumeops.yaml` - Documentation builds and releases +Both container workflows trigger on the same tag pattern (`*-v[0-9]*`). Each checks for its build file (`Dockerfile` or `default.nix`) and skips if not present. See [[build-container-image]]. + ## Secrets (Forgejo Config) Server configuration secrets managed via 1Password → Ansible: diff --git a/mise-tasks/container-list b/mise-tasks/container-list index 0122e77..b785599 100755 --- a/mise-tasks/container-list +++ b/mise-tasks/container-list @@ -14,20 +14,26 @@ echo "" for dir in "$CONTAINER_DIR"/*/; do [[ -d "$dir" ]] || continue - # Determine build type - if [[ -f "$dir/default.nix" ]]; then - build_type="nix" - elif [[ -f "$dir/Dockerfile" ]]; then - build_type="dockerfile" - else - continue - fi + # Determine available build types + has_dockerfile=false + has_nix=false + [[ -f "$dir/Dockerfile" ]] && has_dockerfile=true + [[ -f "$dir/default.nix" ]] && has_nix=true + + # Skip directories with no build files + $has_dockerfile || $has_nix || continue + + # Build type label + types=() + $has_dockerfile && types+=("dockerfile") + $has_nix && types+=("nix") + label=$(IFS=+; echo "${types[*]}") # Extract container name from directory container=$(basename "$dir") image="blumeops/$container" - echo "[$build_type] $container" + echo "[$label] $container" echo " Image: $REGISTRY/$image" echo " Path: $dir" @@ -49,5 +55,7 @@ echo "---" echo "To release a new version:" echo " mise run container-tag-and-release " echo "" +echo "One tag triggers all applicable workflows (dockerfile and/or nix)." +echo "" echo "Example:" echo " mise run container-tag-and-release nettest v1.0.0" diff --git a/mise-tasks/container-tag-and-release b/mise-tasks/container-tag-and-release index 493f00f..d3795cc 100755 --- a/mise-tasks/container-tag-and-release +++ b/mise-tasks/container-tag-and-release @@ -1,77 +1,114 @@ -#!/usr/bin/env bash +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.12" +# dependencies = ["typer>=0.15.0"] +# /// #MISE description="Release a container image by creating a git tag" +#USAGE arg "" help="Container name (directory under containers/)" +#USAGE arg "" help="Version in vX.Y.Z format" +#USAGE flag "--dry-run" help="Show what would be done without creating tags" +"""Release a container image by creating a git tag that triggers CI builds. -set -euo pipefail +One tag triggers all applicable workflows: + - Dockerfile present -> Build Container workflow -> :v + - default.nix present -> Build Container (Nix) workflow -> :v-nix +""" -CONTAINER="${1:-}" -VERSION="${2:-}" +import re +import subprocess +import sys +from pathlib import Path -if [[ -z "$CONTAINER" || -z "$VERSION" ]]; then - echo "Usage: mise run container-tag-and-release " - echo "" - echo "Run 'mise run container-list' to see available containers and recent tags." - exit 1 -fi +import typer -# Validate version format -if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: Version must be in format vX.Y.Z (e.g. v1.0.0)" - exit 1 -fi +REGISTRY = "registry.ops.eblu.me" +FORGE_ACTIONS = "https://forge.ops.eblu.me/eblume/blumeops/actions" -# Determine build type: Nix or Dockerfile -CONTAINER_DIR="containers/${CONTAINER}" -if [[ -f "$CONTAINER_DIR/default.nix" ]]; then - BUILD_TYPE="nix" - TAG="${CONTAINER}-nix-${VERSION}" -elif [[ -f "$CONTAINER_DIR/Dockerfile" ]]; then - BUILD_TYPE="dockerfile" - TAG="${CONTAINER}-${VERSION}" -else - echo "Error: No Dockerfile or default.nix found in '$CONTAINER_DIR'" - echo "" - echo "Available containers:" - for dir in containers/*/; do - [[ -d "$dir" ]] || continue - name=$(basename "$dir") - if [[ -f "$dir/default.nix" ]]; then - echo " - $name (nix)" - elif [[ -f "$dir/Dockerfile" ]]; then - echo " - $name (dockerfile)" - fi - done - exit 1 -fi +app = typer.Typer(add_completion=False) -echo "Creating release tag: $TAG" -echo "Build type: $BUILD_TYPE" -echo "" -# Check if tag already exists -if git rev-parse "$TAG" >/dev/null 2>&1; then - echo "Error: Tag '$TAG' already exists" - echo "Existing tags for $CONTAINER:" - git tag -l "${CONTAINER}-*v*" | sort -V | tail -5 - exit 1 -fi +def git(*args: str) -> str: + result = subprocess.run( + ["git", *args], capture_output=True, text=True, check=True + ) + return result.stdout.strip() -# Image name follows convention: blumeops/ -IMAGE="blumeops/${CONTAINER}" -echo "Container: $CONTAINER" -echo "Directory: $CONTAINER_DIR" -echo "Image: registry.ops.eblu.me/$IMAGE:$VERSION" -echo "" +def git_tag_exists(tag: str) -> bool: + result = subprocess.run( + ["git", "rev-parse", tag], capture_output=True, text=True + ) + return result.returncode == 0 -# Create and push tag -git tag "$TAG" -git push origin "$TAG" -echo "" -echo "Tag '$TAG' created and pushed" -echo "" -echo "The workflow will now build and push:" -echo " registry.ops.eblu.me/$IMAGE:$VERSION" -echo "" -echo "Monitor the build at:" -echo " https://forge.ops.eblu.me/eblume/blumeops/actions" +def list_containers() -> None: + typer.echo("Available containers:") + for d in sorted(Path("containers").iterdir()): + if not d.is_dir(): + continue + types = [] + if (d / "Dockerfile").exists(): + types.append("dockerfile") + if (d / "default.nix").exists(): + types.append("nix") + if types: + typer.echo(f" - {d.name} ({', '.join(types)})") + + +@app.command() +def main( + container: str = typer.Argument(help="Container name (directory under containers/)"), + version: str = typer.Argument(help="Version in vX.Y.Z format"), + dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without creating tags"), +) -> None: + """Release a container image by creating a git tag that triggers CI builds.""" + if not re.match(r"^v\d+\.\d+\.\d+$", version): + typer.echo("Error: Version must be in format vX.Y.Z (e.g. v1.0.0)") + raise typer.Exit(1) + + container_dir = Path("containers") / container + has_dockerfile = (container_dir / "Dockerfile").exists() + has_nix = (container_dir / "default.nix").exists() + + if not has_dockerfile and not has_nix: + typer.echo(f"Error: No Dockerfile or default.nix found in '{container_dir}'") + typer.echo() + list_containers() + raise typer.Exit(1) + + image = f"blumeops/{container}" + tag = f"{container}-{version}" + + # Show what workflows will trigger + builds = [] + if has_dockerfile: + builds.append(f" dockerfile -> {REGISTRY}/{image}:{version}") + if has_nix: + builds.append(f" nix -> {REGISTRY}/{image}:{version}-nix") + + if dry_run: + typer.echo("[dry-run mode]") + typer.echo(f"Container: {container}") + typer.echo(f"Tag: {tag}") + typer.echo(f"Builds:") + for b in builds: + typer.echo(b) + typer.echo() + + if git_tag_exists(tag): + typer.echo(f"Error: Tag '{tag}' already exists") + raise typer.Exit(1) + + if dry_run: + typer.echo(f"[dry-run] Would create and push tag: {tag}") + else: + git("tag", tag) + git("push", "origin", tag) + typer.echo(f"Tag '{tag}' created and pushed") + + typer.echo() + typer.echo(f"Monitor builds at: {FORGE_ACTIONS}") + + +if __name__ == "__main__": + app() diff --git a/nixos/ringtail/configuration.nix b/nixos/ringtail/configuration.nix index fd93c39..6cb0581 100644 --- a/nixos/ringtail/configuration.nix +++ b/nixos/ringtail/configuration.nix @@ -446,6 +446,15 @@ in "d /mnt/storage2 0755 eblume users -" ]; + # Container config for skopeo (used by the forgejo runner to push images) + # and for unqualified image pulls via Zot pull-through cache + environment.etc."containers/policy.json".text = builtins.toJSON { + default = [{ type = "insecureAcceptAnything"; }]; + }; + environment.etc."containers/registries.conf".text = '' + unqualified-search-registries = ["registry.ops.eblu.me", "docker.io", "ghcr.io", "quay.io"] + ''; + # Forgejo Actions runner (nix container builder) services.gitea-actions-runner = { package = pkgs.forgejo-runner; @@ -456,7 +465,7 @@ in tokenFile = "/etc/forgejo-runner/token.env"; labels = [ "nix-container-builder:host" ]; hostPackages = with pkgs; [ - bash coreutils curl gawk gitMinimal gnused nodejs wget + bash coreutils curl gawk gitMinimal gnused jq nodejs wget nix skopeo ]; settings = {