diff --git a/PROGRESS.md b/PROGRESS.md index dd22056..a3fb22d 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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/ diff --git a/deploy/k8s/ci/event-source.yaml b/deploy/k8s/ci/event-source.yaml new file mode 100644 index 0000000..4b95bf9 --- /dev/null +++ b/deploy/k8s/ci/event-source.yaml @@ -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 diff --git a/deploy/k8s/ci/sensor.yaml b/deploy/k8s/ci/sensor.yaml new file mode 100644 index 0000000..d516374 --- /dev/null +++ b/deploy/k8s/ci/sensor.yaml @@ -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 diff --git a/deploy/k8s/ci/service-account.yaml b/deploy/k8s/ci/service-account.yaml new file mode 100644 index 0000000..6500400 --- /dev/null +++ b/deploy/k8s/ci/service-account.yaml @@ -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 diff --git a/deploy/k8s/ci/workflow-template-build-image.yaml b/deploy/k8s/ci/workflow-template-build-image.yaml new file mode 100644 index 0000000..80cd746 --- /dev/null +++ b/deploy/k8s/ci/workflow-template-build-image.yaml @@ -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 diff --git a/deploy/k8s/ci/workflow-template-build-site.yaml b/deploy/k8s/ci/workflow-template-build-site.yaml new file mode 100644 index 0000000..6af9b09 --- /dev/null +++ b/deploy/k8s/ci/workflow-template-build-site.yaml @@ -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: {} diff --git a/deploy/k8s/ingress/acb-api-certificate.yaml b/deploy/k8s/ingress/acb-api-certificate.yaml new file mode 100644 index 0000000..512b973 --- /dev/null +++ b/deploy/k8s/ingress/acb-api-certificate.yaml @@ -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 diff --git a/deploy/k8s/ingress/acb-api-ingressroute.yaml b/deploy/k8s/ingress/acb-api-ingressroute.yaml new file mode 100644 index 0000000..3e538fc --- /dev/null +++ b/deploy/k8s/ingress/acb-api-ingressroute.yaml @@ -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 diff --git a/deploy/k8s/sealed-secrets/registry-credentials.yaml b/deploy/k8s/sealed-secrets/registry-credentials.yaml new file mode 100644 index 0000000..22bce51 --- /dev/null +++ b/deploy/k8s/sealed-secrets/registry-credentials.yaml @@ -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= \ +# --docker-password= \ +# --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