feat(actions): bootstrap sol runner
All checks were successful
runner-smoke / smoke (push) Successful in 36s

This commit is contained in:
mpabi
2026-04-12 16:39:26 +02:00
commit b94cc46b78
11 changed files with 376 additions and 0 deletions

View File

@@ -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

13
README.md Normal file
View File

@@ -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`.

View File

@@ -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`.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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}"

View File

@@ -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 <<EOF | base64 -w0
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: ${CA_DATA}
server: ${KUBE_API_SERVER}
name: sol
contexts:
- context:
cluster: sol
namespace: default
user: ${DEPLOY_SERVICE_ACCOUNT}
name: sol
current-context: sol
users:
- name: ${DEPLOY_SERVICE_ACCOUNT}
user:
token: ${SA_TOKEN}
EOF
)"
PAYLOAD="$(jq -nc --arg data "$KUBECONFIG_B64" --arg description "k3s deploy kubeconfig for trade-next on sol" '{data:$data,description:$description}')"
curl -fsS \
-X PUT \
-H "Authorization: token ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"${GITEA_URL}/api/v1/orgs/${ORG}/actions/secrets/${SECRET_NAME}" \
>/dev/null
echo "Organization secret ${ORG}/${SECRET_NAME} updated"