From 8d00a041872bf0259f0335210f3a454ce0578432 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 13 Feb 2026 19:07:54 -0600 Subject: [PATCH] feat: add FluxCD image automation, fix Preseem duplicate AP crash Switch deployment pipeline from GitLab Agent kubectl to FluxCD image automation. CI now only builds and pushes tagged images to Forgejo registry; FluxCD detects new tags and updates the deployment manifest. Fix Ecto.MultipleResultsError on /dashboard when a device has multiple Preseem access point records by using limit(1) instead of Repo.get_by. --- .forgejo/workflows/build-deploy.yml | 49 ++++++----------------------- .pre-commit-config.yaml | 2 +- flux/git-repository.yaml | 12 +++++++ flux/image-policy.yaml | 14 +++++++++ flux/image-repository.yaml | 10 ++++++ flux/image-update-automation.yaml | 24 ++++++++++++++ flux/kustomization.yaml | 13 ++++++++ k8s/deployment.yaml | 8 ++--- lib/towerops/preseem.ex | 5 ++- test/towerops/preseem_test.exs | 19 +++++++++++ 10 files changed, 109 insertions(+), 47 deletions(-) create mode 100644 flux/git-repository.yaml create mode 100644 flux/image-policy.yaml create mode 100644 flux/image-repository.yaml create mode 100644 flux/image-update-automation.yaml create mode 100644 flux/kustomization.yaml diff --git a/.forgejo/workflows/build-deploy.yml b/.forgejo/workflows/build-deploy.yml index ea22a9ba..c44c6ca9 100644 --- a/.forgejo/workflows/build-deploy.yml +++ b/.forgejo/workflows/build-deploy.yml @@ -1,4 +1,4 @@ -name: Build and Deploy +name: Build on: push: @@ -33,10 +33,13 @@ jobs: - name: Extract metadata id: meta run: | - echo "image=${{ secrets.REGISTRY_URL }}/${{ github.repository }}" >> $GITHUB_OUTPUT - echo "sha_tag=${{ secrets.REGISTRY_URL }}/${{ github.repository }}:${{ github.sha }}" >> $GITHUB_OUTPUT - echo "latest_tag=${{ secrets.REGISTRY_URL }}/${{ github.repository }}:latest" >> $GITHUB_OUTPUT - echo "cache_tag=${{ secrets.REGISTRY_URL }}/${{ github.repository }}:buildcache" >> $GITHUB_OUTPUT + TIMESTAMP=$(date +%s) + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) + IMAGE=${{ secrets.REGISTRY_URL }}/${{ github.repository }} + echo "image=${IMAGE}" >> $GITHUB_OUTPUT + echo "tag=${IMAGE}:main-${TIMESTAMP}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "latest_tag=${IMAGE}:latest" >> $GITHUB_OUTPUT + echo "cache_tag=${IMAGE}:buildcache" >> $GITHUB_OUTPUT - name: Build and push Docker image uses: docker/build-push-action@v6 @@ -45,41 +48,7 @@ jobs: file: k8s/Dockerfile push: true tags: | - ${{ steps.meta.outputs.sha_tag }} + ${{ steps.meta.outputs.tag }} ${{ steps.meta.outputs.latest_tag }} cache-from: type=registry,ref=${{ steps.meta.outputs.cache_tag }} cache-to: type=registry,ref=${{ steps.meta.outputs.cache_tag }},mode=max - - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: production - steps: - - name: Set up kubectl - run: | - # Verify kubectl is available - kubectl version --client - - - name: Configure kubectl - run: | - # Configure kubectl context - kubectl config get-contexts - kubectl config use-context towerops/towerops:home-cluster-agent - - - name: Deploy to Kubernetes - run: | - # Set deployment timestamp (ISO 8601 format in UTC) - DEPLOY_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - echo "Deploying at $DEPLOY_TIMESTAMP" - - # Deploy new version (migrations run on app start) - kubectl set image deployment/towerops \ - towerops=${{ secrets.REGISTRY_URL }}/${{ github.repository }}:${{ github.sha }} \ - -n towerops - - kubectl set env deployment/towerops \ - DEPLOY_TIMESTAMP=$DEPLOY_TIMESTAMP \ - -n towerops - - echo "Deployment initiated. Kubernetes will handle rollout asynchronously." diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87d896df..173daa95 120000 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1 +1 @@ -/nix/store/wvfi88hcjfw2fwbvdmcf1ahm8bfr6awr-pre-commit-config.json \ No newline at end of file +/nix/store/zx636v118rzzqzwafgrw2aybh7l0szrl-pre-commit-config.json \ No newline at end of file diff --git a/flux/git-repository.yaml b/flux/git-repository.yaml new file mode 100644 index 00000000..090d6d26 --- /dev/null +++ b/flux/git-repository.yaml @@ -0,0 +1,12 @@ +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: towerops-web + namespace: flux-system +spec: + interval: 1m + url: https://git.mcintire.me/graham/towerops-web.git + ref: + branch: main + secretRef: + name: forgejo-git-credentials diff --git a/flux/image-policy.yaml b/flux/image-policy.yaml new file mode 100644 index 00000000..44ff2d85 --- /dev/null +++ b/flux/image-policy.yaml @@ -0,0 +1,14 @@ +apiVersion: image.toolkit.fluxcd.io/v1 +kind: ImagePolicy +metadata: + name: towerops-web + namespace: flux-system +spec: + imageRepositoryRef: + name: towerops-web + filterTags: + pattern: '^main-(?P[0-9]+)-[a-f0-9]+$' + extract: '$ts' + policy: + numerical: + order: asc diff --git a/flux/image-repository.yaml b/flux/image-repository.yaml new file mode 100644 index 00000000..fcfd99ef --- /dev/null +++ b/flux/image-repository.yaml @@ -0,0 +1,10 @@ +apiVersion: image.toolkit.fluxcd.io/v1 +kind: ImageRepository +metadata: + name: towerops-web + namespace: flux-system +spec: + image: git.mcintire.me/graham/towerops-web + interval: 1m + secretRef: + name: forgejo-registry diff --git a/flux/image-update-automation.yaml b/flux/image-update-automation.yaml new file mode 100644 index 00000000..3824f6f8 --- /dev/null +++ b/flux/image-update-automation.yaml @@ -0,0 +1,24 @@ +apiVersion: image.toolkit.fluxcd.io/v1 +kind: ImageUpdateAutomation +metadata: + name: towerops-web + namespace: flux-system +spec: + interval: 1m + sourceRef: + kind: GitRepository + name: towerops-web + git: + checkout: + ref: + branch: main + commit: + author: + name: FluxCD + email: fluxcd@towerops.net + messageTemplate: 'chore: update towerops image to {{range .Changed.Changes}}{{.NewValue}}{{end}}' + push: + branch: main + update: + path: ./k8s + strategy: Setters diff --git a/flux/kustomization.yaml b/flux/kustomization.yaml new file mode 100644 index 00000000..c310815d --- /dev/null +++ b/flux/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: towerops-app + namespace: flux-system +spec: + interval: 5m + sourceRef: + kind: GitRepository + name: towerops-web + path: ./k8s + prune: true + targetNamespace: towerops diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index c1883775..62adfe13 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -24,7 +24,7 @@ spec: priorityClassName: system-cluster-critical terminationGracePeriodSeconds: 90 # Allow 90s for graceful shutdown and connection draining imagePullSecrets: - - name: gitlab-registry + - name: forgejo-registry securityContext: runAsNonRoot: true runAsUser: 65534 @@ -33,8 +33,8 @@ spec: type: RuntimeDefault containers: - name: towerops - image: registry.gitlab.com/towerops/towerops:latest - imagePullPolicy: Always + image: git.mcintire.me/graham/towerops-web:latest # {"$imagepolicy": "flux-system:towerops-web"} + imagePullPolicy: IfNotPresent securityContext: allowPrivilegeEscalation: false runAsNonRoot: true @@ -75,8 +75,6 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - - name: DEPLOY_TIMESTAMP - value: "1970-01-01T00:00:00Z" # Placeholder, updated by GitLab CI on deploy - name: ETCD_ENABLED value: "true" - name: RELEASE_DISTRIBUTION diff --git a/lib/towerops/preseem.ex b/lib/towerops/preseem.ex index efe0c8d7..79067d50 100644 --- a/lib/towerops/preseem.ex +++ b/lib/towerops/preseem.ex @@ -12,7 +12,10 @@ defmodule Towerops.Preseem do @doc "Get the Preseem AP linked to a specific device, or nil." def get_access_point_for_device(device_id) do - Repo.get_by(AccessPoint, device_id: device_id) + AccessPoint + |> where(device_id: ^device_id) + |> limit(1) + |> Repo.one() end @doc "List all Preseem APs for an organization." diff --git a/test/towerops/preseem_test.exs b/test/towerops/preseem_test.exs index 491effea..3598c36b 100644 --- a/test/towerops/preseem_test.exs +++ b/test/towerops/preseem_test.exs @@ -61,6 +61,25 @@ defmodule Towerops.PreseemTest do result = Preseem.get_access_point_for_device(device.id) assert result.id == ap.id end + + test "returns one AP when multiple are linked to same device", %{organization: org} do + device = device_fixture(%{organization_id: org.id}) + + _ap1 = + insert_access_point!(org, %{ + device_id: device.id, + match_confidence: "auto_ip" + }) + + _ap2 = + insert_access_point!(org, %{ + device_id: device.id, + match_confidence: "auto_mac" + }) + + result = Preseem.get_access_point_for_device(device.id) + assert %AccessPoint{} = result + end end describe "list_access_points/1" do