Add Traefik ingress, TLS certificate, and Argo CI/CD pipeline manifests

- Traefik IngressRoute for api.aicodebattle.com with CORS, security
  headers, and rate limiting (100 req/min)
- cert-manager Certificate (Let's Encrypt prod, ECDSA P-256)
- Argo Events webhook EventSource + Sensor (triggers on master push)
- Argo Workflows: parallel Kaniko builds for all 10 container images
  plus web SPA site build, with layer caching
- CI ServiceAccount + RBAC for workflow execution
- Registry credentials SealedSecret template

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-03-26 01:26:01 -04:00
parent f1a0830c51
commit e88749b134
9 changed files with 457 additions and 0 deletions

View file

@ -7,6 +7,21 @@
**Last Updated: 2026-03-26**
### Recent Changes (2026-03-26)
- Added Traefik IngressRoute, cert-manager Certificate, and CI/CD pipeline manifests (`deploy/k8s/`):
- `ingress/acb-api-ingressroute.yaml` — Traefik IngressRoute for `api.aicodebattle.com`
with CORS middleware (allow origins for aicodebattle.com), security headers, rate limiting (100 req/min burst 200)
- `ingress/acb-api-certificate.yaml` — cert-manager Certificate (Let's Encrypt prod, ECDSA P-256)
- `ci/event-source.yaml` — Argo Events webhook EventSource (port 12000)
- `ci/sensor.yaml` — Argo Events Sensor: triggers Argo Workflow on push to master
with DAG of parallel Kaniko builds for all 10 container images + site build
- `ci/workflow-template-build-image.yaml` — WorkflowTemplate: Kaniko build with layer caching
- `ci/workflow-template-build-site.yaml` — WorkflowTemplate: npm ci + build for web SPA
- `ci/service-account.yaml` — ServiceAccount + Role + RoleBinding for CI workflows
- `sealed-secrets/registry-credentials.yaml` — SealedSecret template for Forgejo registry auth
- All 30 K8s manifest files validated (valid YAML with correct apiVersion/kind)
- All tests pass (engine + worker + mapgen + api)
### Previous Changes (2026-03-26)
- Built Go API server (`cmd/acb-api/`) — the K8s-native API service per plan architecture:
- HTTP server with graceful shutdown, configurable via environment variables
- PostgreSQL schema: `bots`, `matches`, `match_participants`, `jobs`, `rating_history` tables
@ -147,6 +162,19 @@
- `sealed-secrets/` - Templates for API key, R2 creds, bot secrets, Cloudflare token
- All containers from `forgejo.ardenone.com/ai-code-battle/` registry
- Health/readiness probes and resource limits on all deployments
- [x] Traefik IngressRoute + TLS (`deploy/k8s/ingress/`)
- `acb-api-ingressroute.yaml` - IngressRoute for `api.aicodebattle.com` (websecure entrypoint)
- CORS middleware: allow origins for aicodebattle.com, security headers (nosniff, DENY, strict-origin)
- Rate limiting middleware: 100 req/min, burst 200
- `acb-api-certificate.yaml` - cert-manager Certificate (Let's Encrypt prod, ECDSA P-256)
- [x] Argo Events + Workflows CI/CD pipeline (`deploy/k8s/ci/`)
- `event-source.yaml` - Webhook EventSource (port 12000)
- `sensor.yaml` - Sensor triggers on master push, submits build-all DAG Workflow
- `workflow-template-build-image.yaml` - Kaniko build with layer caching for container images
- `workflow-template-build-site.yaml` - npm build for web SPA (outputs dist/ artifact)
- `service-account.yaml` - CI ServiceAccount + RBAC (pods, workflows access)
- DAG builds all 10 images in parallel: acb-api, acb-worker, acb-indexer, 6 strategy bots, plus site build
- [x] Registry credentials SealedSecret template (`deploy/k8s/sealed-secrets/registry-credentials.yaml`)
### Remaining Phase 6 Work (requires Cloudflare account access)
@ -321,6 +349,8 @@ ai-code-battle/
│ ├── argocd-application.yaml
│ ├── deployments/ # Worker, index builder, 6 strategy bots
│ ├── services/ # ClusterIP services for bots
│ ├── ingress/ # Traefik IngressRoute + cert-manager Certificate
│ ├── ci/ # Argo Events + Workflows CI/CD pipeline
│ └── sealed-secrets/ # Secret templates
└── docs/
└── plan/

View file

@ -0,0 +1,19 @@
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
name: acb-webhook
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-webhook
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: ci
spec:
service:
ports:
- port: 12000
targetPort: 12000
webhook:
acb-push:
port: "12000"
endpoint: /push
method: POST

166
deploy/k8s/ci/sensor.yaml Normal file
View file

@ -0,0 +1,166 @@
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
name: acb-ci
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-ci
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: ci
spec:
dependencies:
- name: push
eventSourceName: acb-webhook
eventName: acb-push
filters:
data:
- path: body.ref
type: string
value:
- "refs/heads/master"
triggers:
- template:
name: build-images
argoWorkflow:
operation: submit
source:
resource:
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: acb-build-
namespace: ai-code-battle
spec:
serviceAccountName: acb-ci
entrypoint: build-all
arguments:
parameters:
- name: commit-sha
value: ""
- name: registry
value: forgejo.ardenone.com/ai-code-battle
templates:
- name: build-all
dag:
tasks:
- name: build-api
templateRef:
name: acb-build-image
template: kaniko-build
arguments:
parameters:
- name: context
value: .
- name: dockerfile
value: cmd/acb-api/Dockerfile
- name: image
value: "{{workflow.parameters.registry}}/acb-api:{{workflow.parameters.commit-sha}}"
- name: build-worker
templateRef:
name: acb-build-image
template: kaniko-build
arguments:
parameters:
- name: context
value: .
- name: dockerfile
value: cmd/acb-worker/Dockerfile
- name: image
value: "{{workflow.parameters.registry}}/acb-worker:{{workflow.parameters.commit-sha}}"
- name: build-indexer
templateRef:
name: acb-build-image
template: kaniko-build
arguments:
parameters:
- name: context
value: .
- name: dockerfile
value: cmd/acb-indexer/Dockerfile
- name: image
value: "{{workflow.parameters.registry}}/acb-indexer:{{workflow.parameters.commit-sha}}"
- name: build-bot-random
templateRef:
name: acb-build-image
template: kaniko-build
arguments:
parameters:
- name: context
value: bots/random
- name: dockerfile
value: bots/random/Dockerfile
- name: image
value: "{{workflow.parameters.registry}}/acb-strategy-random:{{workflow.parameters.commit-sha}}"
- name: build-bot-gatherer
templateRef:
name: acb-build-image
template: kaniko-build
arguments:
parameters:
- name: context
value: bots/gatherer
- name: dockerfile
value: bots/gatherer/Dockerfile
- name: image
value: "{{workflow.parameters.registry}}/acb-strategy-gatherer:{{workflow.parameters.commit-sha}}"
- name: build-bot-rusher
templateRef:
name: acb-build-image
template: kaniko-build
arguments:
parameters:
- name: context
value: bots/rusher
- name: dockerfile
value: bots/rusher/Dockerfile
- name: image
value: "{{workflow.parameters.registry}}/acb-strategy-rusher:{{workflow.parameters.commit-sha}}"
- name: build-bot-guardian
templateRef:
name: acb-build-image
template: kaniko-build
arguments:
parameters:
- name: context
value: bots/guardian
- name: dockerfile
value: bots/guardian/Dockerfile
- name: image
value: "{{workflow.parameters.registry}}/acb-strategy-guardian:{{workflow.parameters.commit-sha}}"
- name: build-bot-swarm
templateRef:
name: acb-build-image
template: kaniko-build
arguments:
parameters:
- name: context
value: bots/swarm
- name: dockerfile
value: bots/swarm/Dockerfile
- name: image
value: "{{workflow.parameters.registry}}/acb-strategy-swarm:{{workflow.parameters.commit-sha}}"
- name: build-bot-hunter
templateRef:
name: acb-build-image
template: kaniko-build
arguments:
parameters:
- name: context
value: bots/hunter
- name: dockerfile
value: bots/hunter/Dockerfile
- name: image
value: "{{workflow.parameters.registry}}/acb-strategy-hunter:{{workflow.parameters.commit-sha}}"
- name: build-site
templateRef:
name: acb-build-site
template: npm-build
arguments:
parameters:
- name: commit-sha
value: "{{workflow.parameters.commit-sha}}"
parameters:
- src:
dependencyName: push
dataKey: body.after
dest: spec.arguments.parameters.0.value

View file

@ -0,0 +1,44 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: acb-ci
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-ci
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: ci
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: acb-ci-workflow
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-ci
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: ci
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: ["argoproj.io"]
resources: ["workflows", "workflowtemplates"]
verbs: ["get", "list", "create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: acb-ci-workflow
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-ci
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: ci
subjects:
- kind: ServiceAccount
name: acb-ci
namespace: ai-code-battle
roleRef:
kind: Role
name: acb-ci-workflow
apiGroup: rbac.authorization.k8s.io

View file

@ -0,0 +1,49 @@
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
name: acb-build-image
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-build-image
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: ci
spec:
templates:
- name: kaniko-build
inputs:
parameters:
- name: context
- name: dockerfile
- name: image
artifacts:
- name: source
git:
repo: https://forgejo.ardenone.com/ai-code-battle/ai-code-battle.git
revision: "{{workflow.parameters.commit-sha}}"
container:
image: gcr.io/kaniko-project/executor:v1.23.2
args:
- --context=/workspace/source/{{inputs.parameters.context}}
- --dockerfile=/workspace/source/{{inputs.parameters.dockerfile}}
- --destination={{inputs.parameters.image}}
- --cache=true
- --cache-repo=forgejo.ardenone.com/ai-code-battle/cache
- --snapshot-mode=redo
- --use-new-run
volumeMounts:
- name: registry-credentials
mountPath: /kaniko/.docker
readOnly: true
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
memory: 2Gi
volumes:
- name: registry-credentials
secret:
secretName: acb-registry-credentials
items:
- key: .dockerconfigjson
path: config.json

View file

@ -0,0 +1,42 @@
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
name: acb-build-site
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-build-site
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: ci
spec:
templates:
- name: npm-build
inputs:
parameters:
- name: commit-sha
artifacts:
- name: source
git:
repo: https://forgejo.ardenone.com/ai-code-battle/ai-code-battle.git
revision: "{{inputs.parameters.commit-sha}}"
script:
image: node:22-alpine
command: [sh]
source: |
set -e
cd /workspace/source/web
npm ci
npm run build
echo "Site build complete — dist/ ready for Pages deployment"
ls -la dist/
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 1Gi
outputs:
artifacts:
- name: site-dist
path: /workspace/source/web/dist
archive:
none: {}

View file

@ -0,0 +1,19 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: acb-api-tls
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-api
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: tls
spec:
secretName: acb-api-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- api.aicodebattle.com
privateKey:
algorithm: ECDSA
size: 256

View file

@ -0,0 +1,64 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: acb-api
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-api
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: ingress
spec:
entryPoints:
- websecure
routes:
- match: Host(`api.aicodebattle.com`)
kind: Rule
services:
- name: acb-api
port: 8080
middlewares:
- name: acb-api-headers
- name: acb-api-ratelimit
tls:
secretName: acb-api-tls
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: acb-api-headers
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-api
app.kubernetes.io/part-of: ai-code-battle
spec:
headers:
accessControlAllowMethods:
- GET
- POST
- OPTIONS
accessControlAllowOriginList:
- "https://aicodebattle.com"
- "https://www.aicodebattle.com"
accessControlAllowHeaders:
- Content-Type
- Authorization
- X-API-Key
accessControlMaxAge: 86400
customResponseHeaders:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: acb-api-ratelimit
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-api
app.kubernetes.io/part-of: ai-code-battle
spec:
rateLimit:
average: 100
burst: 200
period: 1m

View file

@ -0,0 +1,24 @@
# Template: replace encryptedData with actual SealedSecret values
# Generate with:
# kubectl create secret docker-registry acb-registry-credentials \
# --docker-server=forgejo.ardenone.com \
# --docker-username=<user> \
# --docker-password=<token> \
# --dry-run=client -o yaml | kubeseal --format yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: acb-registry-credentials
namespace: ai-code-battle
labels:
app.kubernetes.io/name: acb-registry-credentials
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: ci
spec:
encryptedData:
.dockerconfigjson: REPLACE_WITH_SEALED_VALUE
template:
metadata:
name: acb-registry-credentials
namespace: ai-code-battle
type: kubernetes.io/dockerconfigjson