feat: add feature branch deployments with per-branch isolation
Some checks failed
CI / build (push) Successful in 1m8s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 42s
CI / deploy (push) Failing after 5s
CI / deploy-feature (push) Has been skipped

Enable deploying feature branches into isolated environments on the same
k3s cluster. Each branch gets its own namespace (cam-<slug>), PostgreSQL
schema, and OpenSearch index prefix for data isolation while sharing the
underlying infrastructure.

- Make OpenSearch index prefix and DB schema configurable via env vars
  (defaults preserve existing behavior)
- Restructure deploy/ into Kustomize base + overlays (main/feature)
- Extend CI to build Docker images for all branches, not just main
- Add deploy-feature job with namespace creation, secret copying,
  Traefik Ingress routing (<slug>-api/ui.cameleer.siegeln.net)
- Add cleanup-branch job to remove namespace, PG schema, OS indices
  on branch deletion
- Install required tools (git, jq, curl) in CI deploy containers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-17 11:35:07 +01:00
parent 672544660f
commit 15f20d22ad
10 changed files with 573 additions and 21 deletions

View File

@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- server.yaml
- ui.yaml

124
deploy/base/server.yaml Normal file
View File

@@ -0,0 +1,124 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: cameleer3-server
spec:
replicas: 1
selector:
matchLabels:
app: cameleer3-server
template:
metadata:
labels:
app: cameleer3-server
spec:
imagePullSecrets:
- name: gitea-registry
containers:
- name: server
image: gitea.siegeln.net/cameleer/cameleer3-server:latest
ports:
- containerPort: 8081
env:
- name: SPRING_DATASOURCE_URL
value: "jdbc:postgresql://postgres.cameleer.svc.cluster.local:5432/cameleer3?currentSchema=$(CAMELEER_DB_SCHEMA)"
- name: CAMELEER_DB_SCHEMA
value: "public"
- name: SPRING_DATASOURCE_USERNAME
valueFrom:
secretKeyRef:
name: postgres-credentials
key: POSTGRES_USER
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: POSTGRES_PASSWORD
- name: OPENSEARCH_URL
value: "http://opensearch.cameleer.svc.cluster.local:9200"
- name: CAMELEER_OPENSEARCH_INDEX_PREFIX
value: "executions-"
- name: CAMELEER_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: cameleer-auth
key: CAMELEER_AUTH_TOKEN
- name: CAMELEER_UI_USER
valueFrom:
secretKeyRef:
name: cameleer-auth
key: CAMELEER_UI_USER
optional: true
- name: CAMELEER_UI_PASSWORD
valueFrom:
secretKeyRef:
name: cameleer-auth
key: CAMELEER_UI_PASSWORD
optional: true
- name: CAMELEER_UI_ORIGIN
value: "http://localhost:5173"
- name: CAMELEER_JWT_SECRET
valueFrom:
secretKeyRef:
name: cameleer-auth
key: CAMELEER_JWT_SECRET
optional: true
- name: CAMELEER_OIDC_ENABLED
valueFrom:
secretKeyRef:
name: cameleer-oidc
key: CAMELEER_OIDC_ENABLED
optional: true
- name: CAMELEER_OIDC_ISSUER
valueFrom:
secretKeyRef:
name: cameleer-oidc
key: CAMELEER_OIDC_ISSUER
optional: true
- name: CAMELEER_OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
name: cameleer-oidc
key: CAMELEER_OIDC_CLIENT_ID
optional: true
- name: CAMELEER_OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: cameleer-oidc
key: CAMELEER_OIDC_CLIENT_SECRET
optional: true
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/v1/health
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/v1/health
port: 8081
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: cameleer3-server
spec:
type: ClusterIP
selector:
app: cameleer3-server
ports:
- port: 8081
targetPort: 8081

71
deploy/base/ui.yaml Normal file
View File

@@ -0,0 +1,71 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: cameleer3-ui-config
data:
config.js: |
window.__CAMELEER_CONFIG__ = {
apiBaseUrl: 'http://localhost:8081/api/v1',
};
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cameleer3-ui
spec:
replicas: 1
selector:
matchLabels:
app: cameleer3-ui
template:
metadata:
labels:
app: cameleer3-ui
spec:
imagePullSecrets:
- name: gitea-registry
containers:
- name: ui
image: gitea.siegeln.net/cameleer/cameleer3-server-ui:latest
ports:
- containerPort: 80
env:
- name: CAMELEER_API_URL
value: "http://cameleer3-server:8081"
volumeMounts:
- name: config
mountPath: /usr/share/nginx/html/config.js
subPath: config.js
resources:
requests:
memory: "32Mi"
cpu: "10m"
limits:
memory: "64Mi"
cpu: "100m"
livenessProbe:
httpGet:
path: /healthz
port: 80
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 80
periodSeconds: 5
volumes:
- name: config
configMap:
name: cameleer3-ui-config
---
apiVersion: v1
kind: Service
metadata:
name: cameleer3-ui
spec:
type: ClusterIP
selector:
app: cameleer3-ui
ports:
- port: 80
targetPort: 80

View File

@@ -0,0 +1,26 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cameleer-branch-ingress
spec:
rules:
- host: BRANCH_SLUG-api.cameleer.siegeln.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cameleer3-server
port:
number: 8081
- host: BRANCH_SLUG.cameleer.siegeln.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cameleer3-ui
port:
number: 80

View File

@@ -0,0 +1,30 @@
apiVersion: batch/v1
kind: Job
metadata:
name: init-schema
spec:
template:
spec:
restartPolicy: Never
containers:
- name: init
image: postgres:16
command: ["sh", "-c"]
args:
- |
PGPASSWORD=$POSTGRES_PASSWORD psql \
-h postgres.cameleer.svc.cluster.local \
-U $POSTGRES_USER -d cameleer3 \
-c "CREATE SCHEMA IF NOT EXISTS BRANCH_SCHEMA"
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-credentials
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: POSTGRES_PASSWORD
backoffLimit: 3

View File

@@ -0,0 +1,44 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: BRANCH_NAMESPACE
resources:
- ../../base
- ingress.yaml
- init-job.yaml
images:
- name: gitea.siegeln.net/cameleer/cameleer3-server
newTag: BRANCH_SHA
- name: gitea.siegeln.net/cameleer/cameleer3-server-ui
newTag: BRANCH_SHA
patches:
# Server Deployment: branch-specific schema, index prefix, UI origin, OIDC disabled
- patch: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: cameleer3-server
spec:
template:
spec:
containers:
- name: server
env:
- name: CAMELEER_DB_SCHEMA
value: "BRANCH_SCHEMA"
- name: CAMELEER_OPENSEARCH_INDEX_PREFIX
value: "cam-BRANCH_SLUG-executions-"
- name: CAMELEER_UI_ORIGIN
value: "http://BRANCH_SLUG.cameleer.siegeln.net"
- name: CAMELEER_OIDC_ENABLED
value: "false"
# UI ConfigMap: branch-specific API URL
- target:
kind: ConfigMap
name: cameleer3-ui-config
patch: |
- op: replace
path: /data/config.js
value: |
window.__CAMELEER_CONFIG__ = {
apiBaseUrl: 'http://BRANCH_SLUG-api.cameleer.siegeln.net/api/v1',
};

View File

@@ -0,0 +1,57 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: cameleer
resources:
- ../../base
patches:
# Server Service: NodePort 30081
- target:
kind: Service
name: cameleer3-server
patch: |
- op: replace
path: /spec/type
value: NodePort
- op: add
path: /spec/ports/0/nodePort
value: 30081
# UI Service: NodePort 30090
- target:
kind: Service
name: cameleer3-ui
patch: |
- op: replace
path: /spec/type
value: NodePort
- op: add
path: /spec/ports/0/nodePort
value: 30090
# Server Deployment: same-namespace DNS + production UI origin
- patch: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: cameleer3-server
spec:
template:
spec:
containers:
- name: server
env:
- name: SPRING_DATASOURCE_URL
value: "jdbc:postgresql://postgres:5432/cameleer3?currentSchema=public"
- name: OPENSEARCH_URL
value: "http://opensearch:9200"
- name: CAMELEER_UI_ORIGIN
value: "http://192.168.50.86:30090"
# UI ConfigMap: production API URL
- target:
kind: ConfigMap
name: cameleer3-ui-config
patch: |
- op: replace
path: /data/config.js
value: |
window.__CAMELEER_CONFIG__ = {
apiBaseUrl: 'http://192.168.50.86:30081/api/v1',
};