From 15f20d22adeff4ce88a4ee008d9cf69fd743d51b Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:35:07 +0100 Subject: [PATCH] feat: add feature branch deployments with per-branch isolation Enable deploying feature branches into isolated environments on the same k3s cluster. Each branch gets its own namespace (cam-), 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 (-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) --- .gitea/workflows/ci.yml | 209 +++++++++++++++++- .../server/app/search/OpenSearchIndex.java | 23 +- .../src/main/resources/application.yml | 5 +- deploy/base/kustomization.yaml | 5 + deploy/base/server.yaml | 124 +++++++++++ deploy/base/ui.yaml | 71 ++++++ deploy/overlays/feature/ingress.yaml | 26 +++ deploy/overlays/feature/init-job.yaml | 30 +++ deploy/overlays/feature/kustomization.yaml | 44 ++++ deploy/overlays/main/kustomization.yaml | 57 +++++ 10 files changed, 573 insertions(+), 21 deletions(-) create mode 100644 deploy/base/kustomization.yaml create mode 100644 deploy/base/server.yaml create mode 100644 deploy/base/ui.yaml create mode 100644 deploy/overlays/feature/ingress.yaml create mode 100644 deploy/overlays/feature/init-job.yaml create mode 100644 deploy/overlays/feature/kustomization.yaml create mode 100644 deploy/overlays/main/kustomization.yaml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e04fc6c8..9e53fa62 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -2,15 +2,17 @@ name: CI on: push: - branches: [main] + branches: [main, 'feature/**', 'fix/**', 'feat/**'] tags-ignore: - 'v*' pull_request: branches: [main] + delete: jobs: build: runs-on: ubuntu-latest + if: github.event_name != 'delete' container: image: maven:3.9-eclipse-temurin-17 steps: @@ -60,7 +62,7 @@ jobs: docker: needs: build runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' + if: github.event_name == 'push' container: image: docker:27 steps: @@ -74,15 +76,36 @@ jobs: run: echo "$REGISTRY_TOKEN" | docker login gitea.siegeln.net -u cameleer --password-stdin env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + - name: Compute branch slug + run: | + sanitize_branch() { + echo "$1" | sed -E 's#^(feature|fix|feat|hotfix)/##' \ + | tr '[:upper:]' '[:lower:]' \ + | sed 's/[^a-z0-9-]/-/g' \ + | sed 's/--*/-/g; s/^-//; s/-$//' \ + | cut -c1-20 \ + | sed 's/-$//' + } + if [ "$GITHUB_REF_NAME" = "main" ]; then + echo "BRANCH_SLUG=main" >> "$GITHUB_ENV" + echo "IMAGE_TAGS=latest" >> "$GITHUB_ENV" + else + SLUG=$(sanitize_branch "$GITHUB_REF_NAME") + echo "BRANCH_SLUG=$SLUG" >> "$GITHUB_ENV" + echo "IMAGE_TAGS=branch-$SLUG" >> "$GITHUB_ENV" + fi - name: Set up QEMU for cross-platform builds run: docker run --rm --privileged tonistiigi/binfmt --install all - name: Build and push server run: | docker buildx create --use --name cibuilder + TAGS="-t gitea.siegeln.net/cameleer/cameleer3-server:${{ github.sha }}" + for TAG in $IMAGE_TAGS; do + TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer3-server:$TAG" + done docker buildx build --platform linux/amd64 \ --build-arg REGISTRY_TOKEN="$REGISTRY_TOKEN" \ - -t gitea.siegeln.net/cameleer/cameleer3-server:${{ github.sha }} \ - -t gitea.siegeln.net/cameleer/cameleer3-server:latest \ + $TAGS \ --cache-from type=registry,ref=gitea.siegeln.net/cameleer/cameleer3-server:buildcache \ --cache-to type=registry,ref=gitea.siegeln.net/cameleer/cameleer3-server:buildcache,mode=max \ --provenance=false \ @@ -91,10 +114,13 @@ jobs: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - name: Build and push UI run: | + TAGS="-t gitea.siegeln.net/cameleer/cameleer3-server-ui:${{ github.sha }}" + for TAG in $IMAGE_TAGS; do + TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer3-server-ui:$TAG" + done docker buildx build --platform linux/amd64 \ -f ui/Dockerfile \ - -t gitea.siegeln.net/cameleer/cameleer3-server-ui:${{ github.sha }} \ - -t gitea.siegeln.net/cameleer/cameleer3-server-ui:latest \ + $TAGS \ --cache-from type=registry,ref=gitea.siegeln.net/cameleer/cameleer3-server-ui:buildcache \ --cache-to type=registry,ref=gitea.siegeln.net/cameleer/cameleer3-server-ui:buildcache,mode=max \ --provenance=false \ @@ -110,13 +136,28 @@ jobs: API="https://gitea.siegeln.net/api/v1" AUTH="Authorization: token ${REGISTRY_TOKEN}" CURRENT_SHA="${{ github.sha }}" + # Build list of tags to keep + KEEP_TAGS="latest buildcache $CURRENT_SHA" + if [ "$BRANCH_SLUG" != "main" ]; then + KEEP_TAGS="$KEEP_TAGS branch-$BRANCH_SLUG" + fi for PKG in cameleer3-server cameleer3-server-ui; do curl -sf -H "$AUTH" "$API/packages/cameleer/container/$PKG" | \ jq -r '.[] | "\(.id) \(.version)"' | \ while read id version; do - if [ "$version" != "latest" ] && [ "$version" != "$CURRENT_SHA" ]; then - echo "Deleting old image tag: $PKG:$version" - curl -sf -X DELETE -H "$AUTH" "$API/packages/cameleer/container/$PKG/$version" + SHOULD_KEEP=false + for KEEP in $KEEP_TAGS; do + if [ "$version" = "$KEEP" ]; then + SHOULD_KEEP=true + break + fi + done + if [ "$SHOULD_KEEP" = "false" ]; then + # Only clean up images for this branch + if [ "$BRANCH_SLUG" = "main" ] || echo "$version" | grep -q "branch-$BRANCH_SLUG"; then + echo "Deleting old image tag: $PKG:$version" + curl -sf -X DELETE -H "$AUTH" "$API/packages/cameleer/container/$PKG/$version" + fi fi done done @@ -131,6 +172,9 @@ jobs: container: image: bitnami/kubectl:latest steps: + - name: Install required tools + run: | + apt-get update && apt-get install -y --no-install-recommends git ca-certificates - name: Checkout run: | git clone --depth=1 --branch=${GITHUB_REF_NAME} https://cameleer:${REGISTRY_TOKEN}@gitea.siegeln.net/${GITHUB_REPOSITORY}.git . @@ -198,12 +242,11 @@ jobs: kubectl apply -f deploy/authentik.yaml kubectl -n cameleer rollout status deployment/authentik-server --timeout=180s - kubectl apply -f deploy/server.yaml + kubectl apply -k deploy/overlays/main kubectl -n cameleer set image deployment/cameleer3-server \ server=gitea.siegeln.net/cameleer/cameleer3-server:${{ github.sha }} kubectl -n cameleer rollout status deployment/cameleer3-server --timeout=120s - kubectl apply -f deploy/ui.yaml kubectl -n cameleer set image deployment/cameleer3-ui \ ui=gitea.siegeln.net/cameleer/cameleer3-server-ui:${{ github.sha }} kubectl -n cameleer rollout status deployment/cameleer3-ui --timeout=120s @@ -225,3 +268,147 @@ jobs: CAMELEER_OIDC_ISSUER: ${{ secrets.CAMELEER_OIDC_ISSUER }} CAMELEER_OIDC_CLIENT_ID: ${{ secrets.CAMELEER_OIDC_CLIENT_ID }} CAMELEER_OIDC_CLIENT_SECRET: ${{ secrets.CAMELEER_OIDC_CLIENT_SECRET }} + + deploy-feature: + needs: docker + runs-on: ubuntu-latest + if: github.ref != 'refs/heads/main' && github.event_name == 'push' + container: + image: bitnami/kubectl:latest + steps: + - name: Install required tools + run: | + apt-get update && apt-get install -y --no-install-recommends git jq sed ca-certificates + - name: Checkout + run: | + git clone --depth=1 --branch=${GITHUB_REF_NAME} https://cameleer:${REGISTRY_TOKEN}@gitea.siegeln.net/${GITHUB_REPOSITORY}.git . + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "$KUBECONFIG_B64" | base64 -d > ~/.kube/config + env: + KUBECONFIG_B64: ${{ secrets.KUBECONFIG_BASE64 }} + - name: Compute branch variables + run: | + sanitize_branch() { + echo "$1" | sed -E 's#^(feature|fix|feat|hotfix)/##' \ + | tr '[:upper:]' '[:lower:]' \ + | sed 's/[^a-z0-9-]/-/g' \ + | sed 's/--*/-/g; s/^-//; s/-$//' \ + | cut -c1-20 \ + | sed 's/-$//' + } + SLUG=$(sanitize_branch "$GITHUB_REF_NAME") + NS="cam-${SLUG}" + SCHEMA="cam_$(echo $SLUG | tr '-' '_')" + echo "BRANCH_SLUG=$SLUG" >> "$GITHUB_ENV" + echo "BRANCH_NS=$NS" >> "$GITHUB_ENV" + echo "BRANCH_SCHEMA=$SCHEMA" >> "$GITHUB_ENV" + - name: Create namespace + run: kubectl create namespace "$BRANCH_NS" --dry-run=client -o yaml | kubectl apply -f - + - name: Copy secrets from cameleer namespace + run: | + for SECRET in gitea-registry postgres-credentials opensearch-credentials cameleer-auth cameleer-oidc; do + kubectl get secret "$SECRET" -n cameleer -o json \ + | jq 'del(.metadata.namespace, .metadata.resourceVersion, .metadata.uid, .metadata.creationTimestamp, .metadata.managedFields)' \ + | kubectl apply -n "$BRANCH_NS" -f - + done + - name: Substitute placeholders and deploy + run: | + # Work on a copy so we don't modify the repo + cp -r deploy/overlays/feature /tmp/feature-overlay + # Substitute all BRANCH_* placeholders + for f in /tmp/feature-overlay/*.yaml; do + sed -i \ + -e "s|BRANCH_NAMESPACE|${BRANCH_NS}|g" \ + -e "s|BRANCH_SCHEMA|${BRANCH_SCHEMA}|g" \ + -e "s|BRANCH_SLUG|${BRANCH_SLUG}|g" \ + -e "s|BRANCH_SHA|${{ github.sha }}|g" \ + "$f" + done + # Fix kustomization base path (since we moved the overlay) + sed -i 's|../../base|'"$(pwd)"'/deploy/base|g' /tmp/feature-overlay/kustomization.yaml + kubectl apply -k /tmp/feature-overlay + - name: Wait for init-job + run: | + kubectl -n "$BRANCH_NS" wait --for=condition=complete job/init-schema --timeout=60s || \ + echo "Warning: init-schema job did not complete in time" + - name: Wait for server rollout + run: kubectl -n "$BRANCH_NS" rollout status deployment/cameleer3-server --timeout=120s + - name: Wait for UI rollout + run: kubectl -n "$BRANCH_NS" rollout status deployment/cameleer3-ui --timeout=60s + - name: Print deployment URLs + run: | + echo "====================================" + echo "Feature branch deployed!" + echo "API: http://${BRANCH_SLUG}-api.cameleer.siegeln.net" + echo "UI: http://${BRANCH_SLUG}.cameleer.siegeln.net" + echo "====================================" + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + + cleanup-branch: + runs-on: ubuntu-latest + if: github.event_name == 'delete' && github.event.ref_type == 'branch' + container: + image: bitnami/kubectl:latest + steps: + - name: Install required tools + run: | + apt-get update && apt-get install -y --no-install-recommends curl jq ca-certificates + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "$KUBECONFIG_B64" | base64 -d > ~/.kube/config + env: + KUBECONFIG_B64: ${{ secrets.KUBECONFIG_BASE64 }} + - name: Compute branch variables + run: | + sanitize_branch() { + echo "$1" | sed -E 's#^(feature|fix|feat|hotfix)/##' \ + | tr '[:upper:]' '[:lower:]' \ + | sed 's/[^a-z0-9-]/-/g' \ + | sed 's/--*/-/g; s/^-//; s/-$//' \ + | cut -c1-20 \ + | sed 's/-$//' + } + SLUG=$(sanitize_branch "${{ github.event.ref }}") + NS="cam-${SLUG}" + SCHEMA="cam_$(echo $SLUG | tr '-' '_')" + echo "BRANCH_SLUG=$SLUG" >> "$GITHUB_ENV" + echo "BRANCH_NS=$NS" >> "$GITHUB_ENV" + echo "BRANCH_SCHEMA=$SCHEMA" >> "$GITHUB_ENV" + - name: Delete namespace + run: kubectl delete namespace "$BRANCH_NS" --ignore-not-found + - name: Drop PostgreSQL schema + run: | + kubectl run cleanup-schema-${BRANCH_SLUG} \ + --namespace=cameleer \ + --image=postgres:16 \ + --restart=Never \ + --env="PGPASSWORD=$(kubectl get secret postgres-credentials -n cameleer -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d)" \ + --command -- sh -c "psql -h postgres -U $(kubectl get secret postgres-credentials -n cameleer -o jsonpath='{.data.POSTGRES_USER}' | base64 -d) -d cameleer3 -c 'DROP SCHEMA IF EXISTS ${BRANCH_SCHEMA} CASCADE'" + kubectl wait --for=condition=Ready pod/cleanup-schema-${BRANCH_SLUG} -n cameleer --timeout=30s || true + kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/cleanup-schema-${BRANCH_SLUG} -n cameleer --timeout=60s || true + kubectl delete pod cleanup-schema-${BRANCH_SLUG} -n cameleer --ignore-not-found + - name: Delete OpenSearch indices + run: | + kubectl run cleanup-indices-${BRANCH_SLUG} \ + --namespace=cameleer \ + --image=curlimages/curl:latest \ + --restart=Never \ + --command -- curl -sf -X DELETE "http://opensearch:9200/cam-${BRANCH_SLUG}-*" + kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/cleanup-indices-${BRANCH_SLUG} -n cameleer --timeout=60s || true + kubectl delete pod cleanup-indices-${BRANCH_SLUG} -n cameleer --ignore-not-found + - name: Cleanup Docker images + run: | + API="https://gitea.siegeln.net/api/v1" + AUTH="Authorization: token ${REGISTRY_TOKEN}" + for PKG in cameleer3-server cameleer3-server-ui; do + # Delete branch-specific tag + curl -sf -X DELETE -H "$AUTH" "$API/packages/cameleer/container/$PKG/branch-${BRANCH_SLUG}" || true + done + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/OpenSearchIndex.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/OpenSearchIndex.java index 5d564b89..436b39cc 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/OpenSearchIndex.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/OpenSearchIndex.java @@ -17,6 +17,7 @@ import org.opensearch.client.opensearch.core.search.Hit; import org.opensearch.client.opensearch.indices.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Repository; import java.io.IOException; @@ -30,25 +31,29 @@ import java.util.stream.Collectors; public class OpenSearchIndex implements SearchIndex { private static final Logger log = LoggerFactory.getLogger(OpenSearchIndex.class); - private static final String INDEX_PREFIX = "executions-"; private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd") .withZone(ZoneOffset.UTC); private final OpenSearchClient client; + private final String indexPrefix; - public OpenSearchIndex(OpenSearchClient client) { + public OpenSearchIndex(OpenSearchClient client, + @Value("${opensearch.index-prefix:executions-}") String indexPrefix) { this.client = client; + this.indexPrefix = indexPrefix; } @PostConstruct void ensureIndexTemplate() { + String templateName = indexPrefix + "template"; + String indexPattern = indexPrefix + "*"; try { boolean exists = client.indices().existsIndexTemplate( - ExistsIndexTemplateRequest.of(b -> b.name("executions-template"))).value(); + ExistsIndexTemplateRequest.of(b -> b.name(templateName))).value(); if (!exists) { client.indices().putIndexTemplate(PutIndexTemplateRequest.of(b -> b - .name("executions-template") - .indexPatterns(List.of("executions-*")) + .name(templateName) + .indexPatterns(List.of(indexPattern)) .template(t -> t .settings(s -> s .numberOfShards("3") @@ -65,7 +70,7 @@ public class OpenSearchIndex implements SearchIndex { @Override public void index(ExecutionDocument doc) { - String indexName = INDEX_PREFIX + DAY_FMT.format(doc.startTime()); + String indexName = indexPrefix + DAY_FMT.format(doc.startTime()); try { client.index(IndexRequest.of(b -> b .index(indexName) @@ -98,7 +103,7 @@ public class OpenSearchIndex implements SearchIndex { public long count(SearchRequest request) { try { var countReq = CountRequest.of(b -> b - .index(INDEX_PREFIX + "*") + .index(indexPrefix + "*") .query(buildQuery(request))); return client.count(countReq).count(); } catch (IOException e) { @@ -111,7 +116,7 @@ public class OpenSearchIndex implements SearchIndex { public void delete(String executionId) { try { client.deleteByQuery(DeleteByQueryRequest.of(b -> b - .index(List.of(INDEX_PREFIX + "*")) + .index(List.of(indexPrefix + "*")) .query(Query.of(q -> q.term(t -> t .field("execution_id") .value(FieldValue.of(executionId))))))); @@ -123,7 +128,7 @@ public class OpenSearchIndex implements SearchIndex { private org.opensearch.client.opensearch.core.SearchRequest buildSearchRequest( SearchRequest request, int size) { return org.opensearch.client.opensearch.core.SearchRequest.of(b -> { - b.index(INDEX_PREFIX + "*") + b.index(indexPrefix + "*") .query(buildQuery(request)) .trackTotalHits(th -> th.enabled(true)) .size(size) diff --git a/cameleer3-server-app/src/main/resources/application.yml b/cameleer3-server-app/src/main/resources/application.yml index 26ca2f2d..2662faf8 100644 --- a/cameleer3-server-app/src/main/resources/application.yml +++ b/cameleer3-server-app/src/main/resources/application.yml @@ -3,13 +3,15 @@ server: spring: datasource: - url: jdbc:postgresql://localhost:5432/cameleer3 + url: jdbc:postgresql://localhost:5432/cameleer3?currentSchema=${CAMELEER_DB_SCHEMA:public} username: cameleer password: ${CAMELEER_DB_PASSWORD:cameleer_dev} driver-class-name: org.postgresql.Driver flyway: enabled: true locations: classpath:db/migration + schemas: ${CAMELEER_DB_SCHEMA:public} + default-schema: ${CAMELEER_DB_SCHEMA:public} mvc: async: request-timeout: -1 @@ -34,6 +36,7 @@ ingestion: opensearch: url: ${OPENSEARCH_URL:http://localhost:9200} + index-prefix: ${CAMELEER_OPENSEARCH_INDEX_PREFIX:executions-} queue-size: ${CAMELEER_OPENSEARCH_QUEUE_SIZE:10000} debounce-ms: ${CAMELEER_OPENSEARCH_DEBOUNCE_MS:2000} diff --git a/deploy/base/kustomization.yaml b/deploy/base/kustomization.yaml new file mode 100644 index 00000000..2f277188 --- /dev/null +++ b/deploy/base/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - server.yaml + - ui.yaml diff --git a/deploy/base/server.yaml b/deploy/base/server.yaml new file mode 100644 index 00000000..bad69a22 --- /dev/null +++ b/deploy/base/server.yaml @@ -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 diff --git a/deploy/base/ui.yaml b/deploy/base/ui.yaml new file mode 100644 index 00000000..12033e50 --- /dev/null +++ b/deploy/base/ui.yaml @@ -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 diff --git a/deploy/overlays/feature/ingress.yaml b/deploy/overlays/feature/ingress.yaml new file mode 100644 index 00000000..79655e17 --- /dev/null +++ b/deploy/overlays/feature/ingress.yaml @@ -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 diff --git a/deploy/overlays/feature/init-job.yaml b/deploy/overlays/feature/init-job.yaml new file mode 100644 index 00000000..803aa317 --- /dev/null +++ b/deploy/overlays/feature/init-job.yaml @@ -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 diff --git a/deploy/overlays/feature/kustomization.yaml b/deploy/overlays/feature/kustomization.yaml new file mode 100644 index 00000000..2aec4aad --- /dev/null +++ b/deploy/overlays/feature/kustomization.yaml @@ -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', + }; diff --git a/deploy/overlays/main/kustomization.yaml b/deploy/overlays/main/kustomization.yaml new file mode 100644 index 00000000..b9c86579 --- /dev/null +++ b/deploy/overlays/main/kustomization.yaml @@ -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', + };