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.
This commit is contained in:
parent
c37ae76ceb
commit
8d00a04187
10 changed files with 109 additions and 47 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
name: Build and Deploy
|
name: Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
@ -33,10 +33,13 @@ jobs:
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
echo "image=${{ secrets.REGISTRY_URL }}/${{ github.repository }}" >> $GITHUB_OUTPUT
|
TIMESTAMP=$(date +%s)
|
||||||
echo "sha_tag=${{ secrets.REGISTRY_URL }}/${{ github.repository }}:${{ github.sha }}" >> $GITHUB_OUTPUT
|
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||||
echo "latest_tag=${{ secrets.REGISTRY_URL }}/${{ github.repository }}:latest" >> $GITHUB_OUTPUT
|
IMAGE=${{ secrets.REGISTRY_URL }}/${{ github.repository }}
|
||||||
echo "cache_tag=${{ secrets.REGISTRY_URL }}/${{ github.repository }}:buildcache" >> $GITHUB_OUTPUT
|
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
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
|
|
@ -45,41 +48,7 @@ jobs:
|
||||||
file: k8s/Dockerfile
|
file: k8s/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ steps.meta.outputs.sha_tag }}
|
${{ steps.meta.outputs.tag }}
|
||||||
${{ steps.meta.outputs.latest_tag }}
|
${{ steps.meta.outputs.latest_tag }}
|
||||||
cache-from: type=registry,ref=${{ steps.meta.outputs.cache_tag }}
|
cache-from: type=registry,ref=${{ steps.meta.outputs.cache_tag }}
|
||||||
cache-to: type=registry,ref=${{ steps.meta.outputs.cache_tag }},mode=max
|
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."
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
/nix/store/wvfi88hcjfw2fwbvdmcf1ahm8bfr6awr-pre-commit-config.json
|
/nix/store/zx636v118rzzqzwafgrw2aybh7l0szrl-pre-commit-config.json
|
||||||
12
flux/git-repository.yaml
Normal file
12
flux/git-repository.yaml
Normal file
|
|
@ -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
|
||||||
14
flux/image-policy.yaml
Normal file
14
flux/image-policy.yaml
Normal file
|
|
@ -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<ts>[0-9]+)-[a-f0-9]+$'
|
||||||
|
extract: '$ts'
|
||||||
|
policy:
|
||||||
|
numerical:
|
||||||
|
order: asc
|
||||||
10
flux/image-repository.yaml
Normal file
10
flux/image-repository.yaml
Normal file
|
|
@ -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
|
||||||
24
flux/image-update-automation.yaml
Normal file
24
flux/image-update-automation.yaml
Normal file
|
|
@ -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
|
||||||
13
flux/kustomization.yaml
Normal file
13
flux/kustomization.yaml
Normal file
|
|
@ -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
|
||||||
|
|
@ -24,7 +24,7 @@ spec:
|
||||||
priorityClassName: system-cluster-critical
|
priorityClassName: system-cluster-critical
|
||||||
terminationGracePeriodSeconds: 90 # Allow 90s for graceful shutdown and connection draining
|
terminationGracePeriodSeconds: 90 # Allow 90s for graceful shutdown and connection draining
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
- name: gitlab-registry
|
- name: forgejo-registry
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsNonRoot: true
|
runAsNonRoot: true
|
||||||
runAsUser: 65534
|
runAsUser: 65534
|
||||||
|
|
@ -33,8 +33,8 @@ spec:
|
||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- name: towerops
|
- name: towerops
|
||||||
image: registry.gitlab.com/towerops/towerops:latest
|
image: git.mcintire.me/graham/towerops-web:latest # {"$imagepolicy": "flux-system:towerops-web"}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: IfNotPresent
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
runAsNonRoot: true
|
runAsNonRoot: true
|
||||||
|
|
@ -75,8 +75,6 @@ spec:
|
||||||
valueFrom:
|
valueFrom:
|
||||||
fieldRef:
|
fieldRef:
|
||||||
fieldPath: metadata.namespace
|
fieldPath: metadata.namespace
|
||||||
- name: DEPLOY_TIMESTAMP
|
|
||||||
value: "1970-01-01T00:00:00Z" # Placeholder, updated by GitLab CI on deploy
|
|
||||||
- name: ETCD_ENABLED
|
- name: ETCD_ENABLED
|
||||||
value: "true"
|
value: "true"
|
||||||
- name: RELEASE_DISTRIBUTION
|
- name: RELEASE_DISTRIBUTION
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,10 @@ defmodule Towerops.Preseem do
|
||||||
|
|
||||||
@doc "Get the Preseem AP linked to a specific device, or nil."
|
@doc "Get the Preseem AP linked to a specific device, or nil."
|
||||||
def get_access_point_for_device(device_id) do
|
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
|
end
|
||||||
|
|
||||||
@doc "List all Preseem APs for an organization."
|
@doc "List all Preseem APs for an organization."
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,25 @@ defmodule Towerops.PreseemTest do
|
||||||
result = Preseem.get_access_point_for_device(device.id)
|
result = Preseem.get_access_point_for_device(device.id)
|
||||||
assert result.id == ap.id
|
assert result.id == ap.id
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "list_access_points/1" do
|
describe "list_access_points/1" do
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue