diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c6a3b24..a8afedd 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: - { name: portal, dir: apps/portal } - { name: booking, dir: apps/booking } - { name: website, dir: apps/website } + - { name: operator, dir: apps/operator } defaults: run: working-directory: ${{ matrix.target.dir }} @@ -65,6 +66,7 @@ jobs: - { name: portal, dir: apps/portal } - { name: booking, dir: apps/booking } - { name: platform-api, dir: services/platform-api } + - { name: operator, dir: apps/operator } steps: - uses: actions/checkout@v4 - name: Registry login @@ -72,6 +74,47 @@ jobs: - name: Build + push run: | IMG=git.lastcloud.io/ronnibaslund/dezky/${{ matrix.app.name }} - docker build -t "$IMG:latest" -t "$IMG:${{ github.sha }}" "${{ matrix.app.dir }}" + # The commit SHA tag is what the deploy job pins the cluster to; + # ':latest' is kept for humans / manual pulls only. + docker build \ + -t "$IMG:latest" \ + -t "$IMG:${{ github.sha }}" \ + "${{ matrix.app.dir }}" docker push "$IMG:latest" docker push "$IMG:${{ github.sha }}" + + # Deploy the freshly built images to the k3s cluster the runner already runs + # in. No GitOps controller in between: kustomize pins each Deployment to this + # commit's SHA tag and kubectl applies the manifests, so "push to main" IS + # the release. Auth is the KUBECONFIG_B64 repo secret — a kubeconfig for the + # ci-deployer ServiceAccount (see infrastructure/production/fleet/ci/ + # ci-deployer.yaml), reaching the API server on the in-cluster service IP. + deploy: + runs-on: ubuntu-latest + needs: [build] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + + - name: Install kubectl + kustomize + run: | + curl -fsSLo /usr/local/bin/kubectl https://dl.k8s.io/release/v1.33.4/bin/linux/amd64/kubectl + chmod +x /usr/local/bin/kubectl + curl -fsSL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.6.0/kustomize_v5.6.0_linux_amd64.tar.gz \ + | tar -xz -C /usr/local/bin kustomize + + - name: Deploy to k3s + env: + KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }} + run: | + export KUBECONFIG=/tmp/kubeconfig + echo "$KUBECONFIG_B64" | base64 -d > "$KUBECONFIG" + cd infrastructure/production/fleet/apps + for app in platform-api portal booking operator; do + kustomize edit set image \ + "git.lastcloud.io/ronnibaslund/dezky/$app=git.lastcloud.io/ronnibaslund/dezky/$app:${{ github.sha }}" + done + kubectl apply -k . + for app in platform-api portal booking operator; do + kubectl -n dezky-apps rollout status "deploy/$app" --timeout=180s + done diff --git a/infrastructure/production/fleet/README.md b/infrastructure/production/fleet/README.md index 8be6269..dbc1397 100644 --- a/infrastructure/production/fleet/README.md +++ b/infrastructure/production/fleet/README.md @@ -2,15 +2,16 @@ k3s manifests for the dezky **application tier** that runs in-cluster on the Hetzner AX41 node (see `../host/README.md` for the host layer). This layer -deploys the three first-party apps: +deploys the first-party apps: | App | Image | Public host | Internal Service | |-----|-------|-------------|------------------| | platform-api | `git.lastcloud.io/ronnibaslund/dezky/platform-api` | `api.dezky.eu` | `platform-api.dezky-apps:3001` | | portal | `git.lastcloud.io/ronnibaslund/dezky/portal` | `app.dezky.eu` | `portal.dezky-apps:3000` | | booking | `git.lastcloud.io/ronnibaslund/dezky/booking` | `booking.dezky.eu` | `booking.dezky-apps:3000` | +| operator | `git.lastcloud.io/ronnibaslund/dezky/operator` | `operator.dezky.eu` | `operator.dezky-apps:3000` | -All three live in the `dezky-apps` namespace. The data tier (Postgres/Mongo/ +All of them live in the `dezky-apps` namespace. The data tier (Postgres/Mongo/ Redis), Authentik and OCIS are added by other parts of the fleet layer and live in their own namespaces; these manifests reference them by cluster DNS only. @@ -20,11 +21,16 @@ in their own namespaces; these manifests reference them by cluster DNS only. apps/ ├── kustomization.yaml # bundles the non-secret resources ├── namespace.yaml # dezky-apps namespace +├── redirect-middleware.yaml # per-router HTTP→HTTPS redirect (ACME-safe) ├── platform-api.yaml # Deployment + Service + Ingress (api.dezky.eu) ├── platform-api-config.yaml # non-secret ConfigMap (Stalwart URL, toggles) ├── portal.yaml # Deployment + Service + Ingress (app.dezky.eu) ├── booking.yaml # Deployment + Service + Ingress (booking.dezky.eu) +├── operator.yaml # Deployment + Service + Ingress (operator.dezky.eu) └── secrets.example.yaml # SECRET TEMPLATE — never commit real values +ci/ +├── gitea-runner.yaml # in-cluster Gitea Actions runner (+ dind) +└── ci-deployer.yaml # ServiceAccount/RBAC the CI deploy job uses ``` ## Prerequisites (other fleet layers) @@ -54,26 +60,58 @@ booking.dezky.eu → ## Deploy +**Push to main = release.** CI (`.gitea/workflows/ci.yml`, runner in-cluster — +see `ci/gitea-runner.yaml`) typechecks + tests, builds each app image tagged +`:latest` and the commit SHA, pushes to the Gitea registry, then the deploy job +pins the kustomization to that SHA (`kustomize edit set image`), runs +`kubectl apply -k apps/` and waits for the rollouts. No GitOps controller, no +bot commits — the pipeline that built the image deploys it. + +One-time bootstrap for the deploy job's cluster access: + ```bash -# 1) Apply real Secrets out-of-band (NOT from git). Copy the template, -# fill in values, apply — or render SealedSecrets from it. +# 1) ServiceAccount + RBAC (admin scoped to dezky-apps). +kubectl apply -f ci/ci-deployer.yaml + +# 2) Mint a kubeconfig from its token and store it as the Gitea repo secret +# KUBECONFIG_B64 (repo Settings → Actions → Secrets). API server address is +# the in-cluster service IP — the runner's jobs run inside the cluster. +TOKEN=$(kubectl -n dezky-apps get secret ci-deployer-token -o jsonpath='{.data.token}' | base64 -d) +CA=$(kubectl -n dezky-apps get secret ci-deployer-token -o jsonpath='{.data.ca\.crt}') +cat <