Files
homelab-frontend/k8s/deployment.yaml
T
Admin Bot 1a122b0046
CI / CI (pull_request) Successful in 2m11s
feat(webhook): Forgejo package webhook \u2192 ArgoCD image promotion (#54)
Replaces ArgoCD Image Updater with a webhook-driven approach.

When Forgejo fires X-Gitea-Event: package (action: created, type: container):
1. Extract image owner+name+version from payload
2. Look up in static promotion table (10 images \u2192 ArgoCD apps)
3. Fetch digest from Forgejo registry v2 API (Basic auth, REGISTRY_PAT)
4. Read-modify-write ArgoCD Application CR via k8s API (in-cluster SA token)
   - kustomize: JSON Patch /spec/sources[N]/kustomize/images
   - helm:      JSON Patch /spec/source/helm/parameters (runner.image.*)
5. Send Gotify notification with promotion result

Key design:
- Raw http.Client only \u2014 no client-go dependency
- context.Context propagated through all outbound calls
- Retry up to 3x on 409 Conflict (concurrent poimen-toolkit promotions)
- Request ID per invocation for log correlation
- Promotion table covers: api-gw, portfolio, poimen (3 sources), poimen-toolkit (2 images), forgejo-runner-{golang,node,rust}

RBAC: ClusterRole image-promoter (get+patch applications.argoproj.io) bound to api-gateway SA
NetworkPolicy: egress to cicd:3000 (registry) + default:443 (k8s API)
Deployment: REGISTRY_PAT from forgejo-registry-pat secret
13 tests in promoter_test.go, all pass

Closes #54
2026-09-28 10:41:53 +08:00

158 lines
4.7 KiB
YAML

apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
namespace: api
labels:
app: api-gateway
component: gateway
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: api-gateway
template:
metadata:
labels:
app: api-gateway
component: gateway
# Required by the llm-serving-default-deny NetworkPolicy, which admits
# only pods labelled llm-client=true (from any namespace) on port 8080.
# Without it every upstream dial times out and dispatch returns 502.
llm-client: "true"
annotations:
reloader.stakater.com/auto: "true"
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: api-gateway
# The registry at forgejo.riotpiao.com requires auth — /v2/ answers 401.
# This Secret must exist in the api namespace before the first rollout.
imagePullSecrets:
- name: forgejo-registry
securityContext:
runAsNonRoot: true
# 65532 is distroless's nonroot user, matching USER in the Dockerfile.
runAsUser: 65532
fsGroup: 65532
containers:
- name: gateway
# Tag is pinned in kustomization.yaml so there is exactly one place to
# bump it. Never :latest — Argo cannot make a deterministic rollout
# decision from a mutable tag, and 6.1 requires SHA tags.
image: forgejo.riotpiao.com/rock/api-gateway:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: LISTEN_ADDR
value: "0.0.0.0:8080"
- name: CONFIG_PATH
value: "/etc/gateway/config.yaml"
- name: AUTH_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: api-gw-client-secret
key: client-secret
optional: true
- name: SHUTDOWN_TIMEOUT
value: "5m"
- name: LOG_LEVEL
value: "info"
# OpenTelemetry tracing configuration
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "otel-collector.tracing.svc.cluster.local:4317"
- name: OTEL_SERVICE_NAME
value: "api-gateway"
- name: OTEL_SERVICE_VERSION
value: "1.0.0"
- name: OTEL_ENVIRONMENT
value: "production"
# Gotify integration — Forgejo webhook → push notifications
- name: GOTIFY_URL
valueFrom:
secretKeyRef:
name: gotify-webhook-secret
key: gotify-url
optional: true
- name: GOTIFY_APP_TOKEN
valueFrom:
secretKeyRef:
name: gotify-webhook-secret
key: gotify-app-token
optional: true
- name: FORGEJO_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: gotify-webhook-secret
key: forgejo-webhook-secret
optional: true
- name: REGISTRY_PAT
valueFrom:
secretKeyRef:
name: forgejo-registry-pat
key: pat
optional: true
volumeMounts:
- name: config
mountPath: /etc/gateway
readOnly: true
livenessProbe:
httpGet:
path: /healthz
port: http
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: http
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65532
capabilities:
drop:
- ALL
volumes:
- name: config
secret:
secretName: api-gateway-config
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- api-gateway
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 300