From 961151ed30876332fc5bcd72fe7c827d51d74f40 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 18 Feb 2026 19:09:47 -0800 Subject: [PATCH 1/7] Add k3s cluster on ringtail with amd64 Forgejo runner Enable k3s single-node server on ringtail (NixOS) for native amd64 container builds. Includes ArgoCD Application and manifests for a Forgejo Actions runner with the `k8s-amd64` label, Ansible bootstrap tasks for k3s token and runner secret, and containerd registry mirrors pulling through Zot on indri. Co-Authored-By: Claude Opus 4.6 --- ansible/playbooks/ringtail.yml | 65 ++++++++++++++ argocd/apps/forgejo-runner-amd64.yaml | 17 ++++ .../forgejo-runner-amd64/configmap.yaml | 25 ++++++ .../forgejo-runner-amd64/deployment.yaml | 89 +++++++++++++++++++ .../forgejo-runner-amd64/kustomization.yaml | 6 ++ .../forgejo-runner-amd64/namespace.yaml | 4 + .../feature-k3s-ringtail-runner.feature.md | 1 + docs/reference/infrastructure/ringtail.md | 28 ++++++ nixos/ringtail/configuration.nix | 21 +++++ nixos/ringtail/k3s-registries.yaml | 13 +++ 10 files changed, 269 insertions(+) create mode 100644 argocd/apps/forgejo-runner-amd64.yaml create mode 100644 argocd/manifests/forgejo-runner-amd64/configmap.yaml create mode 100644 argocd/manifests/forgejo-runner-amd64/deployment.yaml create mode 100644 argocd/manifests/forgejo-runner-amd64/kustomization.yaml create mode 100644 argocd/manifests/forgejo-runner-amd64/namespace.yaml create mode 100644 docs/changelog.d/feature-k3s-ringtail-runner.feature.md create mode 100644 nixos/ringtail/k3s-registries.yaml diff --git a/ansible/playbooks/ringtail.yml b/ansible/playbooks/ringtail.yml index f7e085a..3cc1a0b 100644 --- a/ansible/playbooks/ringtail.yml +++ b/ansible/playbooks/ringtail.yml @@ -3,6 +3,28 @@ hosts: ringtail become: true + pre_tasks: + - name: Fetch Forgejo runner registration token from 1Password + ansible.builtin.command: + cmd: op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/Forgejo Secrets/runner_reg" + register: _runner_token + changed_when: false + delegate_to: localhost + become: false + + - name: Ensure /etc/k3s directory exists + ansible.builtin.file: + path: /etc/k3s + state: directory + mode: "0700" + + - name: Generate k3s token if not present + ansible.builtin.copy: + content: "{{ lookup('ansible.builtin.password', '/dev/null', chars=['hexdigits'], length=32) }}" + dest: /etc/k3s/token + mode: "0600" + force: false + tasks: - name: Ensure blumeops repo is present ansible.builtin.git: @@ -24,3 +46,46 @@ register: _ts_status changed_when: false failed_when: "'Running' not in _ts_status.stdout" + + post_tasks: + - name: Wait for k3s to be ready + ansible.builtin.command: k3s kubectl get nodes + register: _k3s_ready + changed_when: false + retries: 30 + delay: 5 + until: _k3s_ready.rc == 0 + + - name: Create forgejo-runner namespace + ansible.builtin.command: k3s kubectl create namespace forgejo-runner + register: _ns + changed_when: _ns.rc == 0 + failed_when: _ns.rc != 0 and 'AlreadyExists' not in _ns.stderr + + - name: Check if forgejo-runner-env secret exists + ansible.builtin.command: k3s kubectl get secret forgejo-runner-env -n forgejo-runner + register: _secret_exists + changed_when: false + failed_when: false + + - name: Create forgejo-runner-env secret + ansible.builtin.command: > + k3s kubectl create secret generic forgejo-runner-env + --namespace=forgejo-runner + --from-literal=RUNNER_TOKEN={{ _runner_token.stdout }} + changed_when: true + when: _secret_exists.rc != 0 + no_log: true + + - name: Update forgejo-runner-env secret + ansible.builtin.shell: + cmd: | + set -o pipefail + k3s kubectl create secret generic forgejo-runner-env \ + --namespace=forgejo-runner \ + --from-literal=RUNNER_TOKEN={{ _runner_token.stdout }} \ + --dry-run=client -o yaml | k3s kubectl apply -f - + executable: /bin/bash + when: _secret_exists.rc == 0 + changed_when: true + no_log: true diff --git a/argocd/apps/forgejo-runner-amd64.yaml b/argocd/apps/forgejo-runner-amd64.yaml new file mode 100644 index 0000000..1e82c79 --- /dev/null +++ b/argocd/apps/forgejo-runner-amd64.yaml @@ -0,0 +1,17 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: forgejo-runner-amd64 + namespace: argocd +spec: + project: default + source: + repoURL: https://forge.ops.eblu.me/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/forgejo-runner-amd64 + destination: + server: https://ringtail.tail8d86e.ts.net:6443 + namespace: forgejo-runner + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/manifests/forgejo-runner-amd64/configmap.yaml b/argocd/manifests/forgejo-runner-amd64/configmap.yaml new file mode 100644 index 0000000..bc14942 --- /dev/null +++ b/argocd/manifests/forgejo-runner-amd64/configmap.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: forgejo-runner-config + namespace: forgejo-runner +data: + config.yaml: | + log: + level: info + + runner: + file: /data/.runner + capacity: 2 + timeout: 3h + envs: + DOCKER_HOST: tcp://127.0.0.1:2375 + TZ: America/Los_Angeles + + container: + network: "host" + docker_host: tcp://127.0.0.1:2375 + daemon.json: | + { + "registry-mirrors": ["https://registry.ops.eblu.me"] + } diff --git a/argocd/manifests/forgejo-runner-amd64/deployment.yaml b/argocd/manifests/forgejo-runner-amd64/deployment.yaml new file mode 100644 index 0000000..ae426aa --- /dev/null +++ b/argocd/manifests/forgejo-runner-amd64/deployment.yaml @@ -0,0 +1,89 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: forgejo-runner-amd64 + namespace: forgejo-runner + labels: + app: forgejo-runner-amd64 +spec: + replicas: 1 + selector: + matchLabels: + app: forgejo-runner-amd64 + template: + metadata: + labels: + app: forgejo-runner-amd64 + spec: + containers: + # Forgejo runner daemon + - name: runner + image: code.forgejo.org/forgejo/runner:6.3.1 + env: + - name: TZ + value: America/Los_Angeles + - name: DOCKER_HOST + value: tcp://localhost:2375 + - name: FORGEJO_URL + value: "https://forge.ops.eblu.me" + - name: RUNNER_NAME + value: "k8s-amd64-runner" + - name: RUNNER_LABELS + value: "k8s-amd64:docker://registry.ops.eblu.me/blumeops/forgejo-runner:v3.2.0-amd64" + command: + - /bin/sh + - -c + - | + # Wait for DinD to be ready + echo "Waiting for Docker daemon..." + while ! wget -q -O /dev/null http://localhost:2375/_ping 2>/dev/null; do + sleep 1 + done + echo "Docker daemon ready" + + # Register if not already registered + if [ ! -f /data/.runner ]; then + echo "Registering runner..." + forgejo-runner register \ + --instance "$FORGEJO_URL" \ + --token "$RUNNER_TOKEN" \ + --name "$RUNNER_NAME" \ + --labels "$RUNNER_LABELS" \ + --no-interactive + fi + + # Start daemon + exec forgejo-runner daemon --config /config/config.yaml + envFrom: + - secretRef: + name: forgejo-runner-env + volumeMounts: + - name: data + mountPath: /data + - name: config + mountPath: /config + + # Docker-in-Docker sidecar + - name: dind + image: docker:27-dind + securityContext: + privileged: true + env: + - name: DOCKER_TLS_CERTDIR + value: "" + volumeMounts: + - name: dind-storage + mountPath: /var/lib/docker + - name: config + mountPath: /etc/docker/daemon.json + subPath: daemon.json + readOnly: true + + volumes: + - name: data + emptyDir: {} + - name: dind-storage + emptyDir: {} + - name: config + configMap: + name: forgejo-runner-config diff --git a/argocd/manifests/forgejo-runner-amd64/kustomization.yaml b/argocd/manifests/forgejo-runner-amd64/kustomization.yaml new file mode 100644 index 0000000..6f6f6e5 --- /dev/null +++ b/argocd/manifests/forgejo-runner-amd64/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - configmap.yaml + - deployment.yaml diff --git a/argocd/manifests/forgejo-runner-amd64/namespace.yaml b/argocd/manifests/forgejo-runner-amd64/namespace.yaml new file mode 100644 index 0000000..19441b1 --- /dev/null +++ b/argocd/manifests/forgejo-runner-amd64/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: forgejo-runner diff --git a/docs/changelog.d/feature-k3s-ringtail-runner.feature.md b/docs/changelog.d/feature-k3s-ringtail-runner.feature.md new file mode 100644 index 0000000..5d69fda --- /dev/null +++ b/docs/changelog.d/feature-k3s-ringtail-runner.feature.md @@ -0,0 +1 @@ +K3s cluster on ringtail with Forgejo Actions runner (`k8s-amd64` label) for native amd64 container builds, managed via ArgoCD multi-cluster. diff --git a/docs/reference/infrastructure/ringtail.md b/docs/reference/infrastructure/ringtail.md index 7906979..686c0eb 100644 --- a/docs/reference/infrastructure/ringtail.md +++ b/docs/reference/infrastructure/ringtail.md @@ -45,6 +45,34 @@ mise run provision-ringtail This updates `flake.lock` via Dagger, verifies the current commit is pushed to forge, then deploys the exact commit via ansible. If the lockfile changed, it stages the file and exits so you can commit and re-run. +## K3s Cluster + +Ringtail runs a single-node k3s cluster for native amd64 workloads, registered in [[argocd|ArgoCD]] on indri as `k3s-ringtail`. + +- **Disabled components:** Traefik, ServiceLB, metrics-server (minimal footprint) +- **TLS SAN:** `ringtail.tail8d86e.ts.net` (ArgoCD connects via Tailscale) +- **Registry mirrors:** Containerd pulls through Zot on indri (`registry.ops.eblu.me`) +- **Token:** `/etc/k3s/token` (generated on first provision) +- **Kubeconfig:** `/etc/rancher/k3s/k3s.yaml` (world-readable via `--write-kubeconfig-mode=644`) + +### Workloads + +| Workload | Namespace | Label | +|----------|-----------|-------| +| Forgejo Runner (amd64) | `forgejo-runner` | `k8s-amd64` | + +### Manual Cluster Registration + +After first provision, register the cluster in ArgoCD: + +```fish +ssh ringtail 'sudo cat /etc/rancher/k3s/k3s.yaml' | \ + sed 's|127.0.0.1|ringtail.tail8d86e.ts.net|' > /tmp/k3s-ringtail.yaml +set -x KUBECONFIG /tmp/k3s-ringtail.yaml +kubectl get nodes # verify access +argocd cluster add default --name k3s-ringtail +``` + ## Maintenance Notes **1Password:** Desktop app must be running for `op` CLI. Use `$mod+Shift+minus` to send to scratchpad. diff --git a/nixos/ringtail/configuration.nix b/nixos/ringtail/configuration.nix index 9e5ecec..79bcd79 100644 --- a/nixos/ringtail/configuration.nix +++ b/nixos/ringtail/configuration.nix @@ -96,12 +96,32 @@ in dedicatedServer.openFirewall = true; }; + # K3s single-node cluster + services.k3s = { + enable = true; + role = "server"; + tokenFile = "/etc/k3s/token"; + extraFlags = toString [ + "--disable=traefik" + "--disable=servicelb" + "--disable=metrics-server" + "--write-kubeconfig-mode=644" + "--tls-san=ringtail.tail8d86e.ts.net" + ]; + }; + + # K3s containerd registry mirrors (pull through Zot on indri) + environment.etc."rancher/k3s/registries.yaml".source = ./k3s-registries.yaml; + # Tailscale services.tailscale = { enable = true; extraUpFlags = [ "--accept-routes" "--ssh" ]; }; + # Trust Tailscale interface (ArgoCD on indri connects via tailnet) + networking.firewall.trustedInterfaces = [ "tailscale0" ]; + # SSH services.openssh = { enable = true; @@ -124,6 +144,7 @@ in # System packages environment.systemPackages = with pkgs; [ git + kubectl python3 # required for Ansible vim htop diff --git a/nixos/ringtail/k3s-registries.yaml b/nixos/ringtail/k3s-registries.yaml new file mode 100644 index 0000000..312362c --- /dev/null +++ b/nixos/ringtail/k3s-registries.yaml @@ -0,0 +1,13 @@ +mirrors: + docker.io: + endpoint: + - "https://registry.ops.eblu.me" + ghcr.io: + endpoint: + - "https://registry.ops.eblu.me" + quay.io: + endpoint: + - "https://registry.ops.eblu.me" + registry.ops.eblu.me: + endpoint: + - "https://registry.ops.eblu.me" -- 2.50.1 (Apple Git-155) From 0d3269e8d6c8bdf5e8b91907792cfb24cd593f15 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 18 Feb 2026 19:40:21 -0800 Subject: [PATCH 2/7] Add 1Password Connect + External Secrets to ringtail k3s Deploy the full ESO stack on ringtail, matching the indri pattern: - 4 ArgoCD apps (1password-connect, external-secrets-crds, external-secrets, external-secrets-config) targeting ringtail k3s cluster - ExternalSecret for forgejo-runner-amd64 token (replaces Ansible-managed secret) - Ansible playbook bootstraps 1Password Connect credentials instead of directly managing runner tokens Co-Authored-By: Claude Opus 4.6 --- ansible/playbooks/ringtail.yml | 54 ++++++++++--------- argocd/apps/1password-connect-ringtail.yaml | 32 +++++++++++ .../external-secrets-config-ringtail.yaml | 24 +++++++++ .../apps/external-secrets-crds-ringtail.yaml | 24 +++++++++ argocd/apps/external-secrets-ringtail.yaml | 32 +++++++++++ .../forgejo-runner-amd64/external-secret.yaml | 27 ++++++++++ .../forgejo-runner-amd64/kustomization.yaml | 1 + .../feature-k3s-ringtail-runner.feature.md | 2 +- docs/reference/infrastructure/ringtail.md | 6 +++ 9 files changed, 176 insertions(+), 26 deletions(-) create mode 100644 argocd/apps/1password-connect-ringtail.yaml create mode 100644 argocd/apps/external-secrets-config-ringtail.yaml create mode 100644 argocd/apps/external-secrets-crds-ringtail.yaml create mode 100644 argocd/apps/external-secrets-ringtail.yaml create mode 100644 argocd/manifests/forgejo-runner-amd64/external-secret.yaml diff --git a/ansible/playbooks/ringtail.yml b/ansible/playbooks/ringtail.yml index 3cc1a0b..de5826a 100644 --- a/ansible/playbooks/ringtail.yml +++ b/ansible/playbooks/ringtail.yml @@ -4,10 +4,18 @@ become: true pre_tasks: - - name: Fetch Forgejo runner registration token from 1Password + - name: Fetch 1Password Connect credentials from 1Password ansible.builtin.command: - cmd: op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/Forgejo Secrets/runner_reg" - register: _runner_token + cmd: op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/1Password Connect/credentials-file" + register: _op_credentials + changed_when: false + delegate_to: localhost + become: false + + - name: Fetch 1Password Connect token from 1Password + ansible.builtin.command: + cmd: op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/1Password Connect/token" + register: _op_token changed_when: false delegate_to: localhost become: false @@ -56,36 +64,32 @@ delay: 5 until: _k3s_ready.rc == 0 - - name: Create forgejo-runner namespace - ansible.builtin.command: k3s kubectl create namespace forgejo-runner + - name: Create 1password namespace + ansible.builtin.command: k3s kubectl create namespace 1password register: _ns changed_when: _ns.rc == 0 failed_when: _ns.rc != 0 and 'AlreadyExists' not in _ns.stderr - - name: Check if forgejo-runner-env secret exists - ansible.builtin.command: k3s kubectl get secret forgejo-runner-env -n forgejo-runner - register: _secret_exists - changed_when: false - failed_when: false - - - name: Create forgejo-runner-env secret - ansible.builtin.command: > - k3s kubectl create secret generic forgejo-runner-env - --namespace=forgejo-runner - --from-literal=RUNNER_TOKEN={{ _runner_token.stdout }} - changed_when: true - when: _secret_exists.rc != 0 - no_log: true - - - name: Update forgejo-runner-env secret + - name: Create or update op-credentials secret ansible.builtin.shell: cmd: | set -o pipefail - k3s kubectl create secret generic forgejo-runner-env \ - --namespace=forgejo-runner \ - --from-literal=RUNNER_TOKEN={{ _runner_token.stdout }} \ + k3s kubectl create secret generic op-credentials \ + --namespace=1password \ + --from-literal=1password-credentials.json='{{ _op_credentials.stdout }}' \ + --dry-run=client -o yaml | k3s kubectl apply -f - + executable: /bin/bash + changed_when: true + no_log: true + + - name: Create or update onepassword-token secret + ansible.builtin.shell: + cmd: | + set -o pipefail + k3s kubectl create secret generic onepassword-token \ + --namespace=1password \ + --from-literal=token={{ _op_token.stdout }} \ --dry-run=client -o yaml | k3s kubectl apply -f - executable: /bin/bash - when: _secret_exists.rc == 0 changed_when: true no_log: true diff --git a/argocd/apps/1password-connect-ringtail.yaml b/argocd/apps/1password-connect-ringtail.yaml new file mode 100644 index 0000000..408eb23 --- /dev/null +++ b/argocd/apps/1password-connect-ringtail.yaml @@ -0,0 +1,32 @@ +# 1Password Connect for ringtail k3s cluster +# Same chart/values as indri, different destination +# +# Prerequisites: +# 1. Bootstrap secrets via ansible (provision-ringtail creates 1password namespace, +# op-credentials and onepassword-token secrets) +# 2. Sync BEFORE external-secrets-ringtail +# +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: 1password-connect-ringtail + namespace: argocd +spec: + project: default + sources: + - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/connect-helm-charts.git + targetRevision: connect-2.3.0 + path: charts/connect + helm: + releaseName: onepassword-connect + valueFiles: + - $values/argocd/manifests/1password-connect/values.yaml + - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + ref: values + destination: + server: https://ringtail.tail8d86e.ts.net:6443 + namespace: 1password + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/apps/external-secrets-config-ringtail.yaml b/argocd/apps/external-secrets-config-ringtail.yaml new file mode 100644 index 0000000..d3f9e58 --- /dev/null +++ b/argocd/apps/external-secrets-config-ringtail.yaml @@ -0,0 +1,24 @@ +# External Secrets Configuration for ringtail k3s cluster +# Same ClusterSecretStore manifests as indri, different destination +# +# Prerequisites: +# - 1password-connect-ringtail is deployed and healthy +# - external-secrets-ringtail operator is deployed and CRDs are installed +# +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: external-secrets-config-ringtail + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/external-secrets + destination: + server: https://ringtail.tail8d86e.ts.net:6443 + namespace: external-secrets + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/apps/external-secrets-crds-ringtail.yaml b/argocd/apps/external-secrets-crds-ringtail.yaml new file mode 100644 index 0000000..a23eae3 --- /dev/null +++ b/argocd/apps/external-secrets-crds-ringtail.yaml @@ -0,0 +1,24 @@ +# External Secrets Operator CRDs for ringtail k3s cluster +# Same CRDs source as indri, different destination +# +# Must be synced BEFORE external-secrets-ringtail operator app. +# +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: external-secrets-crds-ringtail + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/external-secrets.git + targetRevision: helm-chart-2.0.0 + path: config/crds/bases + directory: + exclude: 'kustomization.yaml' + destination: + server: https://ringtail.tail8d86e.ts.net:6443 + syncPolicy: + syncOptions: + - ServerSideApply=true + - CreateNamespace=false diff --git a/argocd/apps/external-secrets-ringtail.yaml b/argocd/apps/external-secrets-ringtail.yaml new file mode 100644 index 0000000..c54c51b --- /dev/null +++ b/argocd/apps/external-secrets-ringtail.yaml @@ -0,0 +1,32 @@ +# External Secrets Operator for ringtail k3s cluster +# Same chart/values as indri, different destination +# +# Prerequisites: +# - 1password-connect-ringtail must be deployed and healthy +# - external-secrets-crds-ringtail must be synced first +# +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: external-secrets-ringtail + namespace: argocd +spec: + project: default + sources: + - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/external-secrets.git + targetRevision: helm-chart-2.0.0 + path: deploy/charts/external-secrets + helm: + releaseName: external-secrets + valueFiles: + - $values/argocd/manifests/external-secrets/values.yaml + - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + ref: values + destination: + server: https://ringtail.tail8d86e.ts.net:6443 + namespace: external-secrets + syncPolicy: + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd/manifests/forgejo-runner-amd64/external-secret.yaml b/argocd/manifests/forgejo-runner-amd64/external-secret.yaml new file mode 100644 index 0000000..3d0ac98 --- /dev/null +++ b/argocd/manifests/forgejo-runner-amd64/external-secret.yaml @@ -0,0 +1,27 @@ +# ExternalSecret for Forgejo Runner token (amd64) +# +# 1Password item: "Forgejo Secrets" in blumeops vault +# Field: runner_reg (runner registration token) +# +# Non-secret env vars (FORGEJO_URL, RUNNER_NAME, RUNNER_LABELS) live in the +# deployment spec so that changes (e.g. image version bumps) trigger a rollout +# automatically. +# +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: forgejo-runner-env + namespace: forgejo-runner +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: forgejo-runner-env + creationPolicy: Owner + data: + - secretKey: RUNNER_TOKEN + remoteRef: + key: Forgejo Secrets + property: runner_reg diff --git a/argocd/manifests/forgejo-runner-amd64/kustomization.yaml b/argocd/manifests/forgejo-runner-amd64/kustomization.yaml index 6f6f6e5..1793b51 100644 --- a/argocd/manifests/forgejo-runner-amd64/kustomization.yaml +++ b/argocd/manifests/forgejo-runner-amd64/kustomization.yaml @@ -4,3 +4,4 @@ resources: - namespace.yaml - configmap.yaml - deployment.yaml + - external-secret.yaml diff --git a/docs/changelog.d/feature-k3s-ringtail-runner.feature.md b/docs/changelog.d/feature-k3s-ringtail-runner.feature.md index 5d69fda..9dcb385 100644 --- a/docs/changelog.d/feature-k3s-ringtail-runner.feature.md +++ b/docs/changelog.d/feature-k3s-ringtail-runner.feature.md @@ -1 +1 @@ -K3s cluster on ringtail with Forgejo Actions runner (`k8s-amd64` label) for native amd64 container builds, managed via ArgoCD multi-cluster. +K3s cluster on ringtail with Forgejo Actions runner (`k8s-amd64` label) for native amd64 container builds, managed via ArgoCD multi-cluster. Includes 1Password Connect + External Secrets Operator for automated secret management, matching the indri pattern. diff --git a/docs/reference/infrastructure/ringtail.md b/docs/reference/infrastructure/ringtail.md index 686c0eb..ee5a6da 100644 --- a/docs/reference/infrastructure/ringtail.md +++ b/docs/reference/infrastructure/ringtail.md @@ -55,6 +55,12 @@ Ringtail runs a single-node k3s cluster for native amd64 workloads, registered i - **Token:** `/etc/k3s/token` (generated on first provision) - **Kubeconfig:** `/etc/rancher/k3s/k3s.yaml` (world-readable via `--write-kubeconfig-mode=644`) +### Secrets Management + +1Password Connect + External Secrets Operator syncs secrets from 1Password to k8s, matching the [[1password|indri pattern]]. Bootstrap credentials (`op-credentials`, `onepassword-token`) are provisioned by Ansible; ArgoCD manages the operator stack. + +Sync order: `1password-connect-ringtail` -> `external-secrets-crds-ringtail` -> `external-secrets-ringtail` -> `external-secrets-config-ringtail` + ### Workloads | Workload | Namespace | Label | -- 2.50.1 (Apple Git-155) From c098199f8b824c69b2d9993fd898edbafb4d7c13 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 18 Feb 2026 20:21:39 -0800 Subject: [PATCH 3/7] Replace k8s Forgejo runner with systemd nix-container-builder Remove the DinD-based k8s runner and add a native systemd Forgejo Actions runner on ringtail for building containers with nix build and pushing via skopeo. The runner uses the NixOS services.gitea-actions-runner module with host execution (no containers), and Ansible provisions the registration token from 1Password. Adds a new build-container-nix workflow for -nix- tags and updates mise tasks to support both Dockerfile and Nix builds. Co-Authored-By: Claude Opus 4.6 --- .forgejo/workflows/build-container-nix.yaml | 90 +++++++++++++++++++ .forgejo/workflows/build-container.yaml | 1 + .github/actionlint.yaml | 1 + ansible/playbooks/ringtail.yml | 21 +++++ argocd/apps/forgejo-runner-amd64.yaml | 17 ---- .../forgejo-runner-amd64/configmap.yaml | 25 ------ .../forgejo-runner-amd64/deployment.yaml | 89 ------------------ .../forgejo-runner-amd64/external-secret.yaml | 27 ------ .../forgejo-runner-amd64/kustomization.yaml | 7 -- .../forgejo-runner-amd64/namespace.yaml | 4 - .../feature-k3s-ringtail-runner.feature.md | 2 +- docs/reference/infrastructure/ringtail.md | 17 +++- mise-tasks/container-list | 14 ++- mise-tasks/container-tag-and-release | 39 +++++--- nixos/ringtail/configuration.nix | 26 ++++++ 15 files changed, 190 insertions(+), 190 deletions(-) create mode 100644 .forgejo/workflows/build-container-nix.yaml delete mode 100644 argocd/apps/forgejo-runner-amd64.yaml delete mode 100644 argocd/manifests/forgejo-runner-amd64/configmap.yaml delete mode 100644 argocd/manifests/forgejo-runner-amd64/deployment.yaml delete mode 100644 argocd/manifests/forgejo-runner-amd64/external-secret.yaml delete mode 100644 argocd/manifests/forgejo-runner-amd64/kustomization.yaml delete mode 100644 argocd/manifests/forgejo-runner-amd64/namespace.yaml diff --git a/.forgejo/workflows/build-container-nix.yaml b/.forgejo/workflows/build-container-nix.yaml new file mode 100644 index 0000000..f7a75b6 --- /dev/null +++ b/.forgejo/workflows/build-container-nix.yaml @@ -0,0 +1,90 @@ +# Nix container build workflow +# Triggers on tags matching: -nix-v +# Builds from containers//default.nix using nix build +# Pushes to Zot registry via skopeo +# +# Examples: +# nettest-nix-v1.0.0 -> builds containers/nettest/default.nix +# myapp-nix-v2.1.0 -> builds containers/myapp/default.nix +name: Build Container (Nix) + +on: + push: + tags: + - '*-nix-v[0-9]*' + +jobs: + build: + runs-on: nix-container-builder + steps: + - name: Parse tag + id: parse + run: | + 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-}" + + echo "container=$CONTAINER" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Container: $CONTAINER" + echo "Version: $VERSION" + + - name: Checkout + uses: actions/checkout@v4 + + - name: Check if nix container exists + id: check + run: | + CONTAINER="${{ steps.parse.outputs.container }}" + CONTEXT="containers/$CONTAINER" + + if [ -f "$CONTEXT/default.nix" ]; then + echo "Found $CONTEXT/default.nix" + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "No default.nix found at $CONTEXT/default.nix" + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Skip if container not found + if: steps.check.outputs.exists != 'true' + 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." + + - name: Build with nix + if: steps.check.outputs.exists == 'true' + id: build + run: | + CONTAINER="${{ steps.parse.outputs.container }}" + echo "Building containers/$CONTAINER/default.nix" + nix build -f "containers/$CONTAINER/default.nix" -o result + echo "Build complete: $(readlink result)" + + - name: Push to registry + if: steps.check.outputs.exists == 'true' + run: | + CONTAINER="${{ steps.parse.outputs.container }}" + VERSION="${{ steps.parse.outputs.version }}" + IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:$VERSION" + + echo "Pushing to $IMAGE" + skopeo copy \ + --dest-tls-verify=false \ + "docker-archive:result" \ + "docker://$IMAGE" + echo "Push complete: $IMAGE" diff --git a/.forgejo/workflows/build-container.yaml b/.forgejo/workflows/build-container.yaml index 98231cf..b76978f 100644 --- a/.forgejo/workflows/build-container.yaml +++ b/.forgejo/workflows/build-container.yaml @@ -17,6 +17,7 @@ on: jobs: build: + if: "!contains(github.ref_name, '-nix-v')" runs-on: k8s steps: - name: Parse tag diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 20281ec..ffc2bdf 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -1,3 +1,4 @@ self-hosted-runner: labels: - k8s + - nix-container-builder diff --git a/ansible/playbooks/ringtail.yml b/ansible/playbooks/ringtail.yml index de5826a..444bd1b 100644 --- a/ansible/playbooks/ringtail.yml +++ b/ansible/playbooks/ringtail.yml @@ -20,6 +20,27 @@ delegate_to: localhost become: false + - name: Fetch Forgejo runner registration token from 1Password + ansible.builtin.command: + cmd: op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/Forgejo Secrets/runner_reg" + register: _runner_reg + changed_when: false + delegate_to: localhost + become: false + + - name: Ensure /etc/forgejo-runner directory exists + ansible.builtin.file: + path: /etc/forgejo-runner + state: directory + mode: "0700" + + - name: Write Forgejo runner token file + ansible.builtin.copy: + content: "TOKEN={{ _runner_reg.stdout }}" + dest: /etc/forgejo-runner/token.env + mode: "0600" + no_log: true + - name: Ensure /etc/k3s directory exists ansible.builtin.file: path: /etc/k3s diff --git a/argocd/apps/forgejo-runner-amd64.yaml b/argocd/apps/forgejo-runner-amd64.yaml deleted file mode 100644 index 1e82c79..0000000 --- a/argocd/apps/forgejo-runner-amd64.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: forgejo-runner-amd64 - namespace: argocd -spec: - project: default - source: - repoURL: https://forge.ops.eblu.me/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/forgejo-runner-amd64 - destination: - server: https://ringtail.tail8d86e.ts.net:6443 - namespace: forgejo-runner - syncPolicy: - syncOptions: - - CreateNamespace=true diff --git a/argocd/manifests/forgejo-runner-amd64/configmap.yaml b/argocd/manifests/forgejo-runner-amd64/configmap.yaml deleted file mode 100644 index bc14942..0000000 --- a/argocd/manifests/forgejo-runner-amd64/configmap.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: forgejo-runner-config - namespace: forgejo-runner -data: - config.yaml: | - log: - level: info - - runner: - file: /data/.runner - capacity: 2 - timeout: 3h - envs: - DOCKER_HOST: tcp://127.0.0.1:2375 - TZ: America/Los_Angeles - - container: - network: "host" - docker_host: tcp://127.0.0.1:2375 - daemon.json: | - { - "registry-mirrors": ["https://registry.ops.eblu.me"] - } diff --git a/argocd/manifests/forgejo-runner-amd64/deployment.yaml b/argocd/manifests/forgejo-runner-amd64/deployment.yaml deleted file mode 100644 index ae426aa..0000000 --- a/argocd/manifests/forgejo-runner-amd64/deployment.yaml +++ /dev/null @@ -1,89 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: forgejo-runner-amd64 - namespace: forgejo-runner - labels: - app: forgejo-runner-amd64 -spec: - replicas: 1 - selector: - matchLabels: - app: forgejo-runner-amd64 - template: - metadata: - labels: - app: forgejo-runner-amd64 - spec: - containers: - # Forgejo runner daemon - - name: runner - image: code.forgejo.org/forgejo/runner:6.3.1 - env: - - name: TZ - value: America/Los_Angeles - - name: DOCKER_HOST - value: tcp://localhost:2375 - - name: FORGEJO_URL - value: "https://forge.ops.eblu.me" - - name: RUNNER_NAME - value: "k8s-amd64-runner" - - name: RUNNER_LABELS - value: "k8s-amd64:docker://registry.ops.eblu.me/blumeops/forgejo-runner:v3.2.0-amd64" - command: - - /bin/sh - - -c - - | - # Wait for DinD to be ready - echo "Waiting for Docker daemon..." - while ! wget -q -O /dev/null http://localhost:2375/_ping 2>/dev/null; do - sleep 1 - done - echo "Docker daemon ready" - - # Register if not already registered - if [ ! -f /data/.runner ]; then - echo "Registering runner..." - forgejo-runner register \ - --instance "$FORGEJO_URL" \ - --token "$RUNNER_TOKEN" \ - --name "$RUNNER_NAME" \ - --labels "$RUNNER_LABELS" \ - --no-interactive - fi - - # Start daemon - exec forgejo-runner daemon --config /config/config.yaml - envFrom: - - secretRef: - name: forgejo-runner-env - volumeMounts: - - name: data - mountPath: /data - - name: config - mountPath: /config - - # Docker-in-Docker sidecar - - name: dind - image: docker:27-dind - securityContext: - privileged: true - env: - - name: DOCKER_TLS_CERTDIR - value: "" - volumeMounts: - - name: dind-storage - mountPath: /var/lib/docker - - name: config - mountPath: /etc/docker/daemon.json - subPath: daemon.json - readOnly: true - - volumes: - - name: data - emptyDir: {} - - name: dind-storage - emptyDir: {} - - name: config - configMap: - name: forgejo-runner-config diff --git a/argocd/manifests/forgejo-runner-amd64/external-secret.yaml b/argocd/manifests/forgejo-runner-amd64/external-secret.yaml deleted file mode 100644 index 3d0ac98..0000000 --- a/argocd/manifests/forgejo-runner-amd64/external-secret.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# ExternalSecret for Forgejo Runner token (amd64) -# -# 1Password item: "Forgejo Secrets" in blumeops vault -# Field: runner_reg (runner registration token) -# -# Non-secret env vars (FORGEJO_URL, RUNNER_NAME, RUNNER_LABELS) live in the -# deployment spec so that changes (e.g. image version bumps) trigger a rollout -# automatically. -# -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: forgejo-runner-env - namespace: forgejo-runner -spec: - refreshInterval: 1h - secretStoreRef: - kind: ClusterSecretStore - name: onepassword-blumeops - target: - name: forgejo-runner-env - creationPolicy: Owner - data: - - secretKey: RUNNER_TOKEN - remoteRef: - key: Forgejo Secrets - property: runner_reg diff --git a/argocd/manifests/forgejo-runner-amd64/kustomization.yaml b/argocd/manifests/forgejo-runner-amd64/kustomization.yaml deleted file mode 100644 index 1793b51..0000000 --- a/argocd/manifests/forgejo-runner-amd64/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - namespace.yaml - - configmap.yaml - - deployment.yaml - - external-secret.yaml diff --git a/argocd/manifests/forgejo-runner-amd64/namespace.yaml b/argocd/manifests/forgejo-runner-amd64/namespace.yaml deleted file mode 100644 index 19441b1..0000000 --- a/argocd/manifests/forgejo-runner-amd64/namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: forgejo-runner diff --git a/docs/changelog.d/feature-k3s-ringtail-runner.feature.md b/docs/changelog.d/feature-k3s-ringtail-runner.feature.md index 9dcb385..e51251d 100644 --- a/docs/changelog.d/feature-k3s-ringtail-runner.feature.md +++ b/docs/changelog.d/feature-k3s-ringtail-runner.feature.md @@ -1 +1 @@ -K3s cluster on ringtail with Forgejo Actions runner (`k8s-amd64` label) for native amd64 container builds, managed via ArgoCD multi-cluster. Includes 1Password Connect + External Secrets Operator for automated secret management, matching the indri pattern. +Systemd Forgejo Actions runner on ringtail (`nix-container-builder` label) for building containers with `nix build` and pushing via `skopeo`. K3s cluster retained for future workloads. 1Password Connect + External Secrets Operator available for k8s secret management. diff --git a/docs/reference/infrastructure/ringtail.md b/docs/reference/infrastructure/ringtail.md index ee5a6da..c3fafac 100644 --- a/docs/reference/infrastructure/ringtail.md +++ b/docs/reference/infrastructure/ringtail.md @@ -63,9 +63,7 @@ Sync order: `1password-connect-ringtail` -> `external-secrets-crds-ringtail` -> ### Workloads -| Workload | Namespace | Label | -|----------|-----------|-------| -| Forgejo Runner (amd64) | `forgejo-runner` | `k8s-amd64` | +No k8s workloads currently deployed. K3s is available for future workloads (e.g. Frigate, running nix-built containers). ### Manual Cluster Registration @@ -79,6 +77,19 @@ kubectl get nodes # verify access argocd cluster add default --name k3s-ringtail ``` +## Systemd Services + +### 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`. + +| Property | Value | +|----------|-------| +| **Label** | `nix-container-builder` | +| **Execution** | Host (no containers) | +| **Token** | `/etc/forgejo-runner/token.env` (provisioned by Ansible) | +| **Service unit** | `gitea-runner-nix-container-builder.service` | + ## Maintenance Notes **1Password:** Desktop app must be running for `op` CLI. Use `$mod+Shift+minus` to send to scratchpad. diff --git a/mise-tasks/container-list b/mise-tasks/container-list index 4a168ea..0122e77 100755 --- a/mise-tasks/container-list +++ b/mise-tasks/container-list @@ -10,16 +10,24 @@ echo "Container Images" echo "================" echo "" -# Find all container directories with Dockerfiles +# Find all container directories with Dockerfiles or default.nix for dir in "$CONTAINER_DIR"/*/; do [[ -d "$dir" ]] || continue - [[ -f "$dir/Dockerfile" ]] || continue + + # Determine build type + if [[ -f "$dir/default.nix" ]]; then + build_type="nix" + elif [[ -f "$dir/Dockerfile" ]]; then + build_type="dockerfile" + else + continue + fi # Extract container name from directory container=$(basename "$dir") image="blumeops/$container" - echo "📦 $container" + echo "[$build_type] $container" echo " Image: $REGISTRY/$image" echo " Path: $dir" diff --git a/mise-tasks/container-tag-and-release b/mise-tasks/container-tag-and-release index 068b164..493f00f 100755 --- a/mise-tasks/container-tag-and-release +++ b/mise-tasks/container-tag-and-release @@ -19,28 +19,39 @@ if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then exit 1 fi -TAG="${CONTAINER}-${VERSION}" +# 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 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 - -# Check if container directory exists -CONTAINER_DIR="containers/${CONTAINER}" -if [[ ! -f "$CONTAINER_DIR/Dockerfile" ]]; then - echo "Error: No Dockerfile found at '$CONTAINER_DIR/Dockerfile'" - echo "" - echo "Available containers:" - for dir in containers/*/; do - [[ -d "$dir" ]] && echo " - $(basename "$dir")" - done + git tag -l "${CONTAINER}-*v*" | sort -V | tail -5 exit 1 fi diff --git a/nixos/ringtail/configuration.nix b/nixos/ringtail/configuration.nix index 79bcd79..c00bd1d 100644 --- a/nixos/ringtail/configuration.nix +++ b/nixos/ringtail/configuration.nix @@ -390,9 +390,35 @@ in "d /mnt/storage2 0755 eblume users -" ]; + # Forgejo Actions runner (nix container builder) + services.gitea-actions-runner = { + package = pkgs.forgejo-runner; + instances.nix-container-builder = { + enable = true; + name = "ringtail-nix-builder"; + url = "https://forge.ops.eblu.me"; + tokenFile = "/etc/forgejo-runner/token.env"; + labels = [ "nix-container-builder:host" ]; + hostPackages = with pkgs; [ + bash coreutils curl gawk gitMinimal gnused nodejs wget + nix skopeo + ]; + settings = { + log.level = "info"; + runner = { + capacity = 1; + timeout = "3h"; + }; + }; + }; + }; + # Enable nix flakes nix.settings.experimental-features = [ "nix-command" "flakes" ]; + # Allow the runner's dynamic user to access the nix daemon + nix.settings.trusted-users = [ "gitea-runner" ]; + # NixOS release system.stateVersion = "25.11"; } -- 2.50.1 (Apple Git-155) From c2ce60c8c91e84e209af13f9c209e65b585dd61c Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 18 Feb 2026 20:29:02 -0800 Subject: [PATCH 4/7] Remove unnecessary --dest-tls-verify=false from skopeo push Caddy provides valid TLS for registry.ops.eblu.me. Co-Authored-By: Claude Opus 4.6 --- .forgejo/workflows/build-container-nix.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.forgejo/workflows/build-container-nix.yaml b/.forgejo/workflows/build-container-nix.yaml index f7a75b6..f66691f 100644 --- a/.forgejo/workflows/build-container-nix.yaml +++ b/.forgejo/workflows/build-container-nix.yaml @@ -84,7 +84,6 @@ jobs: echo "Pushing to $IMAGE" skopeo copy \ - --dest-tls-verify=false \ "docker-archive:result" \ "docker://$IMAGE" echo "Push complete: $IMAGE" -- 2.50.1 (Apple Git-155) From 753fe90b49dc0cd0525af0b2f040ed59b2460e76 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 18 Feb 2026 20:44:18 -0800 Subject: [PATCH 5/7] Fix bash path for NixOS in ringtail playbook NixOS doesn't have /bin/bash. Use /run/current-system/sw/bin/bash which is the stable PATH-resolved location on NixOS. Co-Authored-By: Claude Opus 4.6 --- ansible/playbooks/ringtail.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/playbooks/ringtail.yml b/ansible/playbooks/ringtail.yml index 444bd1b..74c4f54 100644 --- a/ansible/playbooks/ringtail.yml +++ b/ansible/playbooks/ringtail.yml @@ -99,7 +99,7 @@ --namespace=1password \ --from-literal=1password-credentials.json='{{ _op_credentials.stdout }}' \ --dry-run=client -o yaml | k3s kubectl apply -f - - executable: /bin/bash + executable: /run/current-system/sw/bin/bash changed_when: true no_log: true @@ -111,6 +111,6 @@ --namespace=1password \ --from-literal=token={{ _op_token.stdout }} \ --dry-run=client -o yaml | k3s kubectl apply -f - - executable: /bin/bash + executable: /run/current-system/sw/bin/bash changed_when: true no_log: true -- 2.50.1 (Apple Git-155) From 8aa85ca116f7167fd0f3bf2c12b3023dcc97023c Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 18 Feb 2026 20:46:07 -0800 Subject: [PATCH 6/7] Rename runner instance to avoid systemd hyphen escaping instances.nix-container-builder becomes nix_container_builder so the service unit is gitea-runner-nix_container_builder.service instead of gitea-runner-nix\x2dcontainer\x2dbuilder.service. Co-Authored-By: Claude Opus 4.6 --- docs/reference/infrastructure/ringtail.md | 2 +- nixos/ringtail/configuration.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/infrastructure/ringtail.md b/docs/reference/infrastructure/ringtail.md index c3fafac..45bc757 100644 --- a/docs/reference/infrastructure/ringtail.md +++ b/docs/reference/infrastructure/ringtail.md @@ -88,7 +88,7 @@ A native Forgejo Actions runner (`ringtail-nix-builder`) runs as a systemd servi | **Label** | `nix-container-builder` | | **Execution** | Host (no containers) | | **Token** | `/etc/forgejo-runner/token.env` (provisioned by Ansible) | -| **Service unit** | `gitea-runner-nix-container-builder.service` | +| **Service unit** | `gitea-runner-nix_container_builder.service` | ## Maintenance Notes diff --git a/nixos/ringtail/configuration.nix b/nixos/ringtail/configuration.nix index c00bd1d..d765a23 100644 --- a/nixos/ringtail/configuration.nix +++ b/nixos/ringtail/configuration.nix @@ -393,7 +393,7 @@ in # Forgejo Actions runner (nix container builder) services.gitea-actions-runner = { package = pkgs.forgejo-runner; - instances.nix-container-builder = { + instances.nix_container_builder = { enable = true; name = "ringtail-nix-builder"; url = "https://forge.ops.eblu.me"; -- 2.50.1 (Apple Git-155) From 382dcd1e71e2b7e57279005c20596c1faddec18d Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 18 Feb 2026 20:58:31 -0800 Subject: [PATCH 7/7] Add k3s-ringtail kubectl config task and services-check entries New mise task ensure-k3s-ringtail-kubectl-config fetches certs from ringtail and writes a kubeconfig to ~/.kube/k3s-ringtail/config.yml. services-check now verifies k3s, k3s API reachability, and the forgejo-runner systemd service on ringtail. Co-Authored-By: Claude Opus 4.6 --- mise-tasks/ensure-k3s-ringtail-kubectl-config | 61 +++++++++++++++++++ mise-tasks/services-check | 3 + 2 files changed, 64 insertions(+) create mode 100755 mise-tasks/ensure-k3s-ringtail-kubectl-config diff --git a/mise-tasks/ensure-k3s-ringtail-kubectl-config b/mise-tasks/ensure-k3s-ringtail-kubectl-config new file mode 100755 index 0000000..d8a1b80 --- /dev/null +++ b/mise-tasks/ensure-k3s-ringtail-kubectl-config @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +#MISE description="Ensure kubectl config for k3s-ringtail is set up on this workstation" + +set -euo pipefail + +CONFIG_DIR="$HOME/.kube/k3s-ringtail" +CONFIG_FILE="$CONFIG_DIR/config.yml" + +echo "Ensuring k3s-ringtail kubectl config..." + +# Create directory if needed +mkdir -p "$CONFIG_DIR" + +# Fetch kubeconfig from ringtail and extract the CA cert +echo "Fetching kubeconfig from ringtail..." +RAW_CONFIG=$(ssh ringtail 'sudo cat /etc/rancher/k3s/k3s.yaml') + +# Extract and decode the CA certificate +echo "$RAW_CONFIG" | grep certificate-authority-data | awk '{print $2}' | base64 -d > "$CONFIG_DIR/ca.crt" + +# Extract and decode the client certificate +echo "$RAW_CONFIG" | grep client-certificate-data | awk '{print $2}' | base64 -d > "$CONFIG_DIR/client.crt" + +# Extract and decode the client key +echo "$RAW_CONFIG" | grep client-key-data | awk '{print $2}' | base64 -d > "$CONFIG_DIR/client.key" +chmod 600 "$CONFIG_DIR/client.key" + +# Write kubeconfig with file-based certs and tailscale hostname +cat > "$CONFIG_FILE" << EOF +apiVersion: v1 +kind: Config +clusters: +- cluster: + certificate-authority: $CONFIG_DIR/ca.crt + server: https://ringtail.tail8d86e.ts.net:6443 + name: k3s-ringtail +contexts: +- context: + cluster: k3s-ringtail + user: k3s-ringtail + name: k3s-ringtail +current-context: k3s-ringtail +users: +- name: k3s-ringtail + user: + client-certificate: $CONFIG_DIR/client.crt + client-key: $CONFIG_DIR/client.key +EOF + +echo "Config written to $CONFIG_FILE" + +# Warn if KUBECONFIG doesn't include this file +if [[ -z "${KUBECONFIG:-}" ]] || [[ ":$KUBECONFIG:" != *":$CONFIG_FILE:"* ]]; then + echo "" + echo "WARNING: KUBECONFIG does not include $CONFIG_FILE" + echo "Add this to your shell config:" + echo " export KUBECONFIG=\"\$KUBECONFIG:$CONFIG_FILE\"" +fi + +echo "" +echo "Test with: kubectl --context=k3s-ringtail get nodes" diff --git a/mise-tasks/services-check b/mise-tasks/services-check index 31c8cc5..a77c53d 100755 --- a/mise-tasks/services-check +++ b/mise-tasks/services-check @@ -87,6 +87,9 @@ echo "" echo "Ringtail (NixOS):" check_service "ssh" "ssh -o ConnectTimeout=5 ringtail true" check_service "tailscale" "ssh ringtail 'tailscale status --self --json' | jq -e '.Self.Online' > /dev/null" +check_service "k3s" "ssh ringtail 'k3s kubectl get nodes --no-headers | grep -q Ready'" +check_service "k3s-apiserver (remote)" "kubectl --context=k3s-ringtail get --raw /healthz" +check_service "forgejo-runner" "ssh ringtail 'systemctl is-active gitea-runner-nix_container_builder.service'" echo "" echo "Public services (via Fly.io):" -- 2.50.1 (Apple Git-155)