From b94cc46b783c9a1dc3837c704c79a9d9c780f0ff Mon Sep 17 00:00:00 2001 From: mpabi Date: Sun, 12 Apr 2026 16:39:26 +0200 Subject: [PATCH] feat(actions): bootstrap sol runner --- .gitea/workflows/runner-smoke.yaml | 45 ++++++++++ README.md | 13 +++ bootstrap/gitea-actions/README.md | 33 +++++++ bootstrap/gitea-actions/kustomization.yaml | 10 +++ bootstrap/gitea-actions/namespace.yaml | 7 ++ bootstrap/gitea-actions/rbac.yaml | 18 ++++ bootstrap/gitea-actions/runner-configmap.yaml | 18 ++++ .../gitea-actions/runner-deployment.yaml | 90 +++++++++++++++++++ .../gitea-actions/scripts/bootstrap-sol.sh | 28 ++++++ .../create-runner-registration-secret.sh | 44 +++++++++ .../scripts/sync-k3s-kubeconfig-org-secret.sh | 70 +++++++++++++++ 11 files changed, 376 insertions(+) create mode 100644 .gitea/workflows/runner-smoke.yaml create mode 100644 README.md create mode 100644 bootstrap/gitea-actions/README.md create mode 100644 bootstrap/gitea-actions/kustomization.yaml create mode 100644 bootstrap/gitea-actions/namespace.yaml create mode 100644 bootstrap/gitea-actions/rbac.yaml create mode 100644 bootstrap/gitea-actions/runner-configmap.yaml create mode 100644 bootstrap/gitea-actions/runner-deployment.yaml create mode 100755 bootstrap/gitea-actions/scripts/bootstrap-sol.sh create mode 100755 bootstrap/gitea-actions/scripts/create-runner-registration-secret.sh create mode 100755 bootstrap/gitea-actions/scripts/sync-k3s-kubeconfig-org-secret.sh diff --git a/.gitea/workflows/runner-smoke.yaml b/.gitea/workflows/runner-smoke.yaml new file mode 100644 index 0000000..3405fd4 --- /dev/null +++ b/.gitea/workflows/runner-smoke.yaml @@ -0,0 +1,45 @@ +name: runner-smoke + +on: + push: + branches: + - main + paths: + - .gitea/workflows/runner-smoke.yaml + - bootstrap/gitea-actions/** + workflow_dispatch: + +jobs: + smoke: + runs-on: k3s-deploy + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Materialize kubeconfig + env: + K3S_KUBECONFIG_B64: ${{ secrets.K3S_KUBECONFIG_B64 }} + run: | + test -n "$K3S_KUBECONFIG_B64" + printf '%s' "$K3S_KUBECONFIG_B64" | base64 -d >/tmp/kubeconfig + chmod 600 /tmp/kubeconfig + + - name: Verify repository checkout + run: | + pwd + ls -la + test -f bootstrap/gitea-actions/kustomization.yaml + + - name: Install kubectl + run: | + curl -fsSL -o /tmp/kubectl https://dl.k8s.io/release/v1.34.6/bin/linux/amd64/kubectl + install -m 0755 /tmp/kubectl /usr/local/bin/kubectl + kubectl version --client + + - name: Verify cluster access + env: + KUBECONFIG: /tmp/kubeconfig + run: | + kubectl get nodes -o wide + kubectl -n gitea-actions get deploy,pods + kubectl -n trade-infra get svc,endpointslices diff --git a/README.md b/README.md new file mode 100644 index 0000000..8afef3a --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# trade-gitops + +GitOps bootstrap and cluster runtime for the `trade-next` migration target on `mevnode_sol`. + +## Bootstrap Modules + +- `bootstrap/gitea-actions`: organization-scoped Gitea Actions runner for `trade-next`, including deployer RBAC and operator scripts. + +## Current Scope + +- Bootstrap a single-node `k3s` control plane on `sol`. +- Expose host-level `Postgres` and `Redis` into the cluster through `trade-infra`. +- Run an organization-scoped Gitea Actions runner that can execute deployment workflows against `sol`. diff --git a/bootstrap/gitea-actions/README.md b/bootstrap/gitea-actions/README.md new file mode 100644 index 0000000..17af3f7 --- /dev/null +++ b/bootstrap/gitea-actions/README.md @@ -0,0 +1,33 @@ +# Gitea Actions Runner Bootstrap + +This module bootstraps a single organization-scoped Gitea Actions runner for `trade-next` on the `sol` cluster. + +## Design + +- Runner scope: organization-level for `trade-next` +- Runtime: `docker.io/gitea/act_runner:latest` +- Job execution: `docker:27-dind` sidecar with a shared Unix socket +- Cluster access for workflows: dedicated `trade-gitops-deployer` service account, exported as the `K3S_KUBECONFIG_B64` org secret +- Storage model: small persistent `hostPath` only for runner registration state, ephemeral Docker layer cache +- Runner labels: `ubuntu-latest` and `k3s-deploy`, both starting from the standard Gitea runner image so deployment jobs can install the exact `kubectl` version they need + +## Operator Flow + +1. Prepare the org registration token secret in `gitea-actions`. +2. Apply the kustomize module on `sol`. +3. Create or refresh the deployer kubeconfig and sync it to the `trade-next` org secrets. +4. Push a workflow to `trade-gitops` and let the runner execute deployment jobs. + +## Bootstrap Commands + +From the repository root: + +```bash +./bootstrap/gitea-actions/scripts/bootstrap-sol.sh +``` + +## Notes + +- This runner is intentionally pinned to the `sol` node because the target cluster is currently single-node. +- The deployer binding is `cluster-admin` for the first bootstrap pass and should be narrowed once the GitOps surface is fully reconstructed. +- The runner exposes the labels `ubuntu-latest` and `k3s-deploy`. diff --git a/bootstrap/gitea-actions/kustomization.yaml b/bootstrap/gitea-actions/kustomization.yaml new file mode 100644 index 0000000..e349e1b --- /dev/null +++ b/bootstrap/gitea-actions/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: gitea-actions + +resources: + - namespace.yaml + - rbac.yaml + - runner-configmap.yaml + - runner-deployment.yaml diff --git a/bootstrap/gitea-actions/namespace.yaml b/bootstrap/gitea-actions/namespace.yaml new file mode 100644 index 0000000..1340786 --- /dev/null +++ b/bootstrap/gitea-actions/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: gitea-actions + labels: + app.kubernetes.io/name: gitea-actions + app.kubernetes.io/part-of: trade-gitops diff --git a/bootstrap/gitea-actions/rbac.yaml b/bootstrap/gitea-actions/rbac.yaml new file mode 100644 index 0000000..7ff6119 --- /dev/null +++ b/bootstrap/gitea-actions/rbac.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: trade-gitops-deployer + namespace: gitea-actions +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: trade-gitops-deployer-cluster-admin +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: trade-gitops-deployer + namespace: gitea-actions diff --git a/bootstrap/gitea-actions/runner-configmap.yaml b/bootstrap/gitea-actions/runner-configmap.yaml new file mode 100644 index 0000000..1c6f264 --- /dev/null +++ b/bootstrap/gitea-actions/runner-configmap.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: act-runner-config + namespace: gitea-actions +data: + config.yaml: | + log: + level: info + runner: + file: /data/.runner + capacity: 1 + timeout: 3h + cache: + enabled: false + container: + docker_host: unix:///var/run/docker.sock + force_pull: true diff --git a/bootstrap/gitea-actions/runner-deployment.yaml b/bootstrap/gitea-actions/runner-deployment.yaml new file mode 100644 index 0000000..08501f8 --- /dev/null +++ b/bootstrap/gitea-actions/runner-deployment.yaml @@ -0,0 +1,90 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: trade-next-act-runner + namespace: gitea-actions + labels: + app.kubernetes.io/name: trade-next-act-runner + app.kubernetes.io/part-of: trade-gitops +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: trade-next-act-runner + template: + metadata: + labels: + app.kubernetes.io/name: trade-next-act-runner + app.kubernetes.io/part-of: trade-gitops + spec: + nodeSelector: + kubernetes.io/hostname: sol + volumes: + - name: runner-data + hostPath: + path: /var/lib/trade-gitops/gitea-actions/runner-data + type: DirectoryOrCreate + - name: runner-config + configMap: + name: act-runner-config + - name: docker-sock + emptyDir: + sizeLimit: 1Gi + - name: dind-data + emptyDir: + sizeLimit: 20Gi + containers: + - name: dind + image: docker:27-dind + securityContext: + privileged: true + env: + - name: DOCKER_TLS_CERTDIR + value: "" + args: + - --host=unix:///var/run/docker.sock + - --group=1001 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + volumeMounts: + - name: docker-sock + mountPath: /var/run + - name: dind-data + mountPath: /var/lib/docker + - name: runner + image: docker.io/gitea/act_runner:latest + env: + - name: CONFIG_FILE + value: /config/config.yaml + - name: GITEA_INSTANCE_URL + value: https://gitea.mpabi.pl + - name: GITEA_RUNNER_NAME + value: trade-next-sol + - name: GITEA_RUNNER_LABELS + value: ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest,k3s-deploy:docker://docker.gitea.com/runner-images:ubuntu-latest + - name: GITEA_RUNNER_REGISTRATION_TOKEN + valueFrom: + secretKeyRef: + name: act-runner-registration-token + key: token + - name: DOCKER_HOST + value: unix:///var/run/docker.sock + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - name: runner-data + mountPath: /data + - name: runner-config + mountPath: /config + - name: docker-sock + mountPath: /var/run diff --git a/bootstrap/gitea-actions/scripts/bootstrap-sol.sh b/bootstrap/gitea-actions/scripts/bootstrap-sol.sh new file mode 100755 index 0000000..8377a03 --- /dev/null +++ b/bootstrap/gitea-actions/scripts/bootstrap-sol.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +MODULE_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)" +REPO_ROOT="$(cd -- "${MODULE_DIR}/../.." && pwd -P)" + +SOL_HOST="${SOL_HOST:-149.50.96.162}" +SOL_USER="${SOL_USER:-user}" +SOL_SSH_KEY="${SOL_SSH_KEY:-/home/user/dev/mcp/keys/mpabi/mevnode_mcp}" +REMOTE_STAGING_DIR="${REMOTE_STAGING_DIR:-/tmp/trade-gitops-bootstrap}" + +ssh_sol() { + ssh -i "$SOL_SSH_KEY" -o IdentitiesOnly=yes -o StrictHostKeyChecking=no "$SOL_USER@$SOL_HOST" "$@" +} + +"${SCRIPT_DIR}/create-runner-registration-secret.sh" + +tar -C "$REPO_ROOT" -cf - bootstrap/gitea-actions | \ + ssh_sol "rm -rf ${REMOTE_STAGING_DIR} && mkdir -p ${REMOTE_STAGING_DIR} && tar -C ${REMOTE_STAGING_DIR} -xf -" + +ssh_sol "sudo mkdir -p /var/lib/trade-gitops/gitea-actions/runner-data" +ssh_sol "sudo k3s kubectl apply -k ${REMOTE_STAGING_DIR}/bootstrap/gitea-actions" +ssh_sol "sudo k3s kubectl -n gitea-actions rollout status deploy/trade-next-act-runner --timeout=180s" + +"${SCRIPT_DIR}/sync-k3s-kubeconfig-org-secret.sh" + +ssh_sol "sudo k3s kubectl -n gitea-actions get pods -o wide" diff --git a/bootstrap/gitea-actions/scripts/create-runner-registration-secret.sh b/bootstrap/gitea-actions/scripts/create-runner-registration-secret.sh new file mode 100755 index 0000000..f3925f1 --- /dev/null +++ b/bootstrap/gitea-actions/scripts/create-runner-registration-secret.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +ORG="${ORG:-trade-next}" +GITEA_URL="${GITEA_URL:-https://gitea.mpabi.pl}" +GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-/home/user/dev/mcp/tools/tokens/gitea.token}" +SOL_HOST="${SOL_HOST:-149.50.96.162}" +SOL_USER="${SOL_USER:-user}" +SOL_SSH_KEY="${SOL_SSH_KEY:-/home/user/dev/mcp/keys/mpabi/mevnode_mcp}" +NAMESPACE="${NAMESPACE:-gitea-actions}" +SECRET_NAME="${SECRET_NAME:-act-runner-registration-token}" + +gitea_token() { + cut -d: -f2- "$GITEA_TOKEN_FILE" | head -n1 | tr -d '[:space:]' +} + +ssh_sol() { + ssh -i "$SOL_SSH_KEY" -o IdentitiesOnly=yes -o StrictHostKeyChecking=no "$SOL_USER@$SOL_HOST" "$@" +} + +API_TOKEN="$(gitea_token)" +if [ -z "$API_TOKEN" ]; then + echo "Gitea API token is empty" >&2 + exit 1 +fi + +REG_TOKEN="$( + curl -fsS \ + -X POST \ + -H "Authorization: token ${API_TOKEN}" \ + "${GITEA_URL}/api/v1/orgs/${ORG}/actions/runners/registration-token" \ + | jq -r '.token' +)" + +if [ -z "$REG_TOKEN" ] || [ "$REG_TOKEN" = "null" ]; then + echo "Failed to obtain runner registration token" >&2 + exit 1 +fi + +ssh_sol "sudo k3s kubectl get ns ${NAMESPACE} >/dev/null 2>&1 || sudo k3s kubectl create ns ${NAMESPACE} >/dev/null" + +printf '%s' "$REG_TOKEN" | ssh_sol "tmp=\$(mktemp); cat >\"\$tmp\"; sudo k3s kubectl -n ${NAMESPACE} create secret generic ${SECRET_NAME} --from-file=token=\"\$tmp\" --dry-run=client -o yaml | sudo k3s kubectl apply -f - >/dev/null; rm -f \"\$tmp\"" + +echo "Runner registration secret synced to ${SOL_HOST}:${NAMESPACE}/${SECRET_NAME}" diff --git a/bootstrap/gitea-actions/scripts/sync-k3s-kubeconfig-org-secret.sh b/bootstrap/gitea-actions/scripts/sync-k3s-kubeconfig-org-secret.sh new file mode 100755 index 0000000..2a554ca --- /dev/null +++ b/bootstrap/gitea-actions/scripts/sync-k3s-kubeconfig-org-secret.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +ORG="${ORG:-trade-next}" +SECRET_NAME="${SECRET_NAME:-K3S_KUBECONFIG_B64}" +GITEA_URL="${GITEA_URL:-https://gitea.mpabi.pl}" +GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-/home/user/dev/mcp/tools/tokens/gitea.token}" +SOL_HOST="${SOL_HOST:-149.50.96.162}" +SOL_USER="${SOL_USER:-user}" +SOL_SSH_KEY="${SOL_SSH_KEY:-/home/user/dev/mcp/keys/mpabi/mevnode_mcp}" +DEPLOY_NAMESPACE="${DEPLOY_NAMESPACE:-gitea-actions}" +DEPLOY_SERVICE_ACCOUNT="${DEPLOY_SERVICE_ACCOUNT:-trade-gitops-deployer}" +KUBE_API_SERVER="${KUBE_API_SERVER:-https://149.50.96.162:6443}" + +gitea_token() { + cut -d: -f2- "$GITEA_TOKEN_FILE" | head -n1 | tr -d '[:space:]' +} + +ssh_sol() { + ssh -i "$SOL_SSH_KEY" -o IdentitiesOnly=yes -o StrictHostKeyChecking=no "$SOL_USER@$SOL_HOST" "$@" +} + +API_TOKEN="$(gitea_token)" +if [ -z "$API_TOKEN" ]; then + echo "Gitea API token is empty" >&2 + exit 1 +fi + +CA_DATA="$(ssh_sol "sudo k3s kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}'")" +SA_TOKEN="$(ssh_sol "sudo k3s kubectl -n ${DEPLOY_NAMESPACE} create token ${DEPLOY_SERVICE_ACCOUNT} --duration=8760h")" + +if [ -z "$CA_DATA" ] || [ -z "$SA_TOKEN" ]; then + echo "Failed to generate deployer kubeconfig material" >&2 + exit 1 +fi + +KUBECONFIG_B64="$( + cat </dev/null + +echo "Organization secret ${ORG}/${SECRET_NAME} updated"