From 6eb53a4d1cfdb77237679f85dfea621e1a0d2ac7 Mon Sep 17 00:00:00 2001 From: Admin Bot Date: Sun, 13 Sep 2026 21:12:15 +0900 Subject: [PATCH] fix: rewrite Tekton integration tests for X-Service routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FIXES: - Remove stale files: k8s/argocd-apps/, k8s/tekton/base/, overlays/ (Tekton infra is in homelab repo, not here) - Fix step.resources → step.computeResources (Tekton v1 API) - Fix Task: use curl sidecar pattern instead of distroless image (distroless has no shell/curl/go) - Fix routing: use X-Service + X-Resource headers, not path-based - Extract test script to scripts/integration-test.sh (ConfigMap mount) - Install kubectl in CI runner (was missing) - Prune README to essentials TASK ARCHITECTURE: sidecar: gateway image (mounts config secret, runs on localhost) step: curlimages/curl (runs integration-test.sh from ConfigMap) TEST COVERAGE: health, header validation, memory, s3, sqs, workflow, iam --- .gitea/workflows/ci.yaml | 114 +++++++++---------- k8s/argocd-apps/tekton.yaml | 33 ------ k8s/tekton/README.md | 128 +++++----------------- k8s/tekton/base/tekton-release.yaml | 44 -------- k8s/tekton/kustomization.yaml | 13 ++- k8s/tekton/pipeline-integration-test.yaml | 25 ++--- k8s/tekton/scripts/integration-test.sh | 115 +++++++++++++++++++ k8s/tekton/task-integration-test.yaml | 112 +++++++++---------- 8 files changed, 265 insertions(+), 319 deletions(-) delete mode 100644 k8s/argocd-apps/tekton.yaml delete mode 100644 k8s/tekton/base/tekton-release.yaml create mode 100755 k8s/tekton/scripts/integration-test.sh diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 94efcf5..3f56aba 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -17,10 +17,13 @@ jobs: name: CI runs-on: golang steps: - - name: Install Node.js and Docker + - name: Install dependencies run: | apt-get update - apt-get install -y nodejs docker.io + apt-get install -y docker.io curl + curl -sLO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl && mv kubectl /usr/local/bin/ + kubectl version --client - name: Checkout code uses: actions/checkout@v4 @@ -48,90 +51,81 @@ jobs: docker build --no-cache \ -t "${IMAGE}:${{ steps.sha.outputs.short_sha }}" \ -f Dockerfile . - echo "Built image: ${IMAGE}:${{ steps.sha.outputs.short_sha }}" - - name: Push test image (SHA tag only, not latest yet) - run: | - docker push "${IMAGE}:${{ steps.sha.outputs.short_sha }}" - echo "✓ Pushed test image: ${IMAGE}:${{ steps.sha.outputs.short_sha }}" + - name: Push image (SHA tag) + run: docker push "${IMAGE}:${{ steps.sha.outputs.short_sha }}" - - name: Setup kubeconfig for Tekton trigger + # ── Tekton integration tests ───────────────────────────── + - name: Setup kubeconfig run: | mkdir -p ~/.kube echo "${KUBECONFIG_B64}" | base64 -d > ~/.kube/config + kubectl cluster-info env: KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }} - continue-on-error: true - - name: Trigger integration tests via Tekton PipelineRun + - name: Trigger Tekton PipelineRun + id: tekton run: | - echo "Triggering integration tests via Tekton..." - - # Create PipelineRun to run integration tests - kubectl create -f - << 'YAML' + SHA="${{ steps.sha.outputs.short_sha }}" + RUN_NAME="integration-test-${SHA}" + + # Clean up any previous run with the same name + kubectl delete pipelinerun "${RUN_NAME}" -n api --ignore-not-found + + # Create PipelineRun — spins up gateway sidecar + curl tests + cat </dev/null || \ - kubectl wait --for=condition=Failed \ - pipelineruns/integration-test-${{ steps.sha.outputs.short_sha }} \ - -n api --timeout=1s 2>/dev/null || true - - # Get test results - echo "" - echo "=== Test Results ===" - RESULT=$(kubectl get pipelinerun integration-test-${{ steps.sha.outputs.short_sha }} \ - -n api -o jsonpath='{.status.conditions[0].reason}') - TEST_MESSAGE=$(kubectl get pipelinerun integration-test-${{ steps.sha.outputs.short_sha }} \ - -n api -o jsonpath='{.status.taskRuns[*].status.taskResults[?(@.name=="result")].value}') - - echo "PipelineRun Status: $RESULT" - echo "Test Result: $TEST_MESSAGE" - - # Get logs + + echo "✓ PipelineRun created: ${RUN_NAME}" + + # Wait for completion (Succeeded or Failed) + echo "Waiting for tests (timeout 5m)..." + if kubectl wait pipelinerun/"${RUN_NAME}" -n api \ + --for=condition=Succeeded --timeout=5m 2>/dev/null; then + echo "result=pass" >> $GITHUB_OUTPUT + else + echo "result=fail" >> $GITHUB_OUTPUT + fi + + # Print logs + results echo "" echo "=== Test Logs ===" - kubectl logs -n api pipelinerun/integration-test-${{ steps.sha.outputs.short_sha }} || true - - # Determine if tests passed - if [ "$RESULT" = "Succeeded" ]; then - echo "✓ Integration tests PASSED" - exit 0 - else - echo "✗ Integration tests FAILED" - exit 1 - fi - continue-on-error: false + kubectl logs -n api "pipelinerun/${RUN_NAME}" --all-containers 2>/dev/null || true + echo "" + REASON=$(kubectl get pipelinerun "${RUN_NAME}" -n api \ + -o jsonpath='{.status.conditions[0].reason}') + SUMMARY=$(kubectl get pipelinerun "${RUN_NAME}" -n api \ + -o jsonpath='{.status.results[?(@.name=="test-summary")].value}') + echo "Status: ${REASON}" + echo "Summary: ${SUMMARY}" - - name: Promote image to latest (only if tests passed) - if: success() + - name: Gate on test result + if: steps.tekton.outputs.result != 'pass' + run: | + echo "✗ Integration tests FAILED — image NOT promoted" + exit 1 + + # ── Promote only after tests pass ──────────────────────── + - name: Promote image to latest run: | - docker pull "${IMAGE}:${{ steps.sha.outputs.short_sha }}" docker tag "${IMAGE}:${{ steps.sha.outputs.short_sha }}" "${IMAGE}:latest" docker push "${IMAGE}:latest" - echo "✓ Promoted ${IMAGE}:${{ steps.sha.outputs.short_sha }} to latest" + echo "✓ Promoted to latest" - name: Cleanup if: always() - run: docker image prune -a --force 2>&1 | tail -3 || true + run: docker image prune -af 2>&1 | tail -3 || true diff --git a/k8s/argocd-apps/tekton.yaml b/k8s/argocd-apps/tekton.yaml deleted file mode 100644 index e9329b1..0000000 --- a/k8s/argocd-apps/tekton.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: tekton-pipelines - namespace: argocd - labels: - app.kubernetes.io/name: tekton - app.kubernetes.io/part-of: homelab -spec: - project: default - - source: - repoURL: https://github.com/tektoncd/operator.git - targetRevision: main - path: config/release - - destination: - server: https://kubernetes.default.svc - namespace: tekton-pipelines - - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - Validate=false - retry: - limit: 5 - backoff: - duration: 5s - factor: 2 - maxDuration: 3m diff --git a/k8s/tekton/README.md b/k8s/tekton/README.md index a6c3a41..e598259 100644 --- a/k8s/tekton/README.md +++ b/k8s/tekton/README.md @@ -1,49 +1,32 @@ -# Tekton Integration Testing +# Tekton Integration Tests -Tekton Pipelines for running integration tests on API Gateway changes before merging to main. +Curl-based integration tests for the API gateway, orchestrated by Tekton. -## Architecture +## How It Works ``` -Gitea CI (builds image:sha) - ↓ -Creates PipelineRun - ↓ -Tekton Controller (watches PipelineRun) - ↓ -Runs Task: integration-test - ↓ -Task runs tests in container - ↓ -Reports pass/fail to PipelineRun status - ↓ -CI reads status and promotes image (if pass) - ↓ -ArgoCD deploys new image +CI pushes image:sha → creates PipelineRun → Tekton spins up gateway sidecar +→ runs curl tests → reports pass/fail → CI promotes to :latest if pass ``` -## Components +The Task runs the gateway image as a **sidecar** (same pod, localhost), +then executes `scripts/integration-test.sh` which tests every adapter +via `X-Service` + `X-Resource` header routing. -### Task: `integration-test` -- **File**: `task-integration-test.yaml` -- **Purpose**: Run integration tests in a container -- **Inputs**: Image to test, timeout -- **Outputs**: pass/fail result, message -- **Security**: Non-root user, resource limits +## Files -### Pipeline: `integration-test-pipeline` -- **File**: `pipeline-integration-test.yaml` -- **Purpose**: Orchestrate integration test execution -- **Tasks**: Runs the integration-test task -- **Results**: Aggregates task results for CI consumption +| File | Purpose | +|------|---------| +| `task-integration-test.yaml` | Task: sidecar gateway + curl test step | +| `pipeline-integration-test.yaml` | Pipeline: wraps the Task | +| `scripts/integration-test.sh` | Test script (mounted as ConfigMap) | +| `kustomization.yaml` | Generates ConfigMap from script | -## Usage - -### Manual Trigger +## Manual Run ```bash -# Create a PipelineRun to test an image -kubectl create -f - << 'YAML' +kubectl apply -k k8s/tekton/ +kubectl create -f - <<'EOF' apiVersion: tekton.dev/v1 kind: PipelineRun metadata: @@ -54,77 +37,22 @@ spec: name: integration-test-pipeline params: - name: image - value: forgejo.riotpiao.com/rock/api-gateway:abc123 - - name: test-timeout - value: "5m" -YAML + value: forgejo.riotpiao.com/rock/api-gateway:latest +EOF -# Watch test progress -kubectl logs -f -n api pipelinerun/integration-test-manual - -# Check results -kubectl get pipelinerun -n api integration-test-manual -o yaml +# Watch +kubectl logs -f -n api pipelinerun/integration-test-manual -c step-run-tests ``` -### CI Trigger +## Updating Tests -CI automatically creates PipelineRun with: -- Image tag: current commit SHA -- Timeout: 5 minutes -- Labels: PR ID, commit SHA for traceability - -## Management - -Tekton is managed by ArgoCD Application: `tekton-pipelines` (in `k8s/argocd-apps/tekton.yaml`) - -To update: -1. Edit manifest files -2. Commit to git -3. ArgoCD syncs automatically - -Do NOT manually apply manifests - let ArgoCD manage everything. - -## Monitoring +Edit `scripts/integration-test.sh`, then: ```bash -# List all PipelineRuns -kubectl get pipelineruns -n api - -# Watch a specific run -kubectl logs -f -n api pipelinerun/integration-test- - -# Get detailed status -kubectl describe pipelinerun -n api integration-test- +kubectl apply -k k8s/tekton/ # recreates ConfigMap ``` -## Results +## Tekton Infrastructure -PipelineRun status contains: -- `status.conditions[0].reason`: Succeeded | Failed | Unknown -- `status.taskRuns[*].status.taskResults`: Test outputs -- Pod logs: Detailed test output - -## Best Practices - -1. **DRY**: Task and Pipeline are parameterized, reusable -2. **SOLID**: Single responsibility (Task runs tests, Pipeline orchestrates) -3. **GitOps**: Everything in git, managed by ArgoCD -4. **Security**: Non-root containers, resource limits, no hardcoded values -5. **Observability**: Clear logging, status tracking, result aggregation - -## Troubleshooting - -**PipelineRun stuck in Running** -- Check pod logs: `kubectl logs -n api pod/` -- Check gateway availability: `kubectl get pods -n api -l app=api-gateway` -- Increase timeout in pipeline params - -**Tests failing** -- Check test logs: `kubectl logs -n api pipelinerun/` -- Verify gateway is ready and accessible -- Check downstream services (memory, S3, etc.) - -**Image not promoted** -- CI only promotes if PipelineRun succeeds -- Check PipelineRun status: `kubectl get pipelinerun -n api -o yaml` -- Review CI logs in Gitea for error details +Tekton Pipelines is installed in `~/workplace/homelab` via ArgoCD +(`k8s/argocd/apps/06-ci-cd.yaml` → vendored `k8s/infra/tekton/release.yaml`). diff --git a/k8s/tekton/base/tekton-release.yaml b/k8s/tekton/base/tekton-release.yaml deleted file mode 100644 index 2dcca26..0000000 --- a/k8s/tekton/base/tekton-release.yaml +++ /dev/null @@ -1,44 +0,0 @@ -# Tekton Pipelines Release manifest -# Source: https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml -# This is managed by ArgoCD - do NOT manually apply -# ArgoCD syncs this from git - -apiVersion: v1 -kind: Namespace -metadata: - name: tekton-pipelines - labels: - managed-by: argocd - ---- -# CRDs and RBAC are part of the full release manifest -# Using a reference approach for cleaner GitOps -apiVersion: argoproj.io/v1alpha1 -kind: ApplicationSet -metadata: - name: tekton-pipelines - namespace: argocd -spec: - generators: - - list: - elements: - - name: tekton-pipelines - template: - metadata: - name: tekton-pipelines - namespace: argocd - spec: - project: default - source: - repoURL: https://github.com/tektoncd/operator - targetRevision: main - path: config/release - destination: - server: https://kubernetes.default.svc - namespace: tekton-pipelines - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true diff --git a/k8s/tekton/kustomization.yaml b/k8s/tekton/kustomization.yaml index ffc58ff..fefd3fd 100644 --- a/k8s/tekton/kustomization.yaml +++ b/k8s/tekton/kustomization.yaml @@ -1,7 +1,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -metadata: - name: api-gateway-tekton namespace: api @@ -9,7 +7,10 @@ resources: - task-integration-test.yaml - pipeline-integration-test.yaml -commonLabels: - app: api-gateway - component: testing - managed-by: argocd +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: +- name: integration-test-script + files: + - scripts/integration-test.sh diff --git a/k8s/tekton/pipeline-integration-test.yaml b/k8s/tekton/pipeline-integration-test.yaml index fe3528f..6ad284e 100644 --- a/k8s/tekton/pipeline-integration-test.yaml +++ b/k8s/tekton/pipeline-integration-test.yaml @@ -7,29 +7,24 @@ metadata: app: api-gateway component: testing spec: - description: Pipeline to run integration tests for API gateway + description: > + Run integration tests against a gateway image. + Spins up the image as a sidecar, tests via curl, reports pass/fail. params: - name: image type: string - description: Container image to test (repo:tag) - default: "forgejo.riotpiao.com/rock/api-gateway:latest" - - name: test-timeout - type: string - default: "5m" - description: Test execution timeout + description: "Container image to test (repo:sha)" results: - name: test-result - description: Overall test result (pass/fail) - value: $(tasks.run-integration-tests.results.result) - - name: test-message - description: Test summary message - value: $(tasks.run-integration-tests.results.message) + description: "pass or fail" + value: $(tasks.integration-test.results.result) + - name: test-summary + description: "e.g. 8/8 passed" + value: $(tasks.integration-test.results.summary) tasks: - - name: run-integration-tests + - name: integration-test taskRef: name: integration-test params: - name: image value: $(params.image) - - name: timeout - value: $(params.test-timeout) diff --git a/k8s/tekton/scripts/integration-test.sh b/k8s/tekton/scripts/integration-test.sh new file mode 100755 index 0000000..d13d858 --- /dev/null +++ b/k8s/tekton/scripts/integration-test.sh @@ -0,0 +1,115 @@ +#!/bin/sh +set -e + +# Integration test runner for API gateway. +# Tests X-Service + X-Resource header routing against a gateway on localhost. +# +# Required env: +# GW — gateway base URL (e.g. http://localhost:8080) +# RESULTS_DIR — directory to write Tekton results + +PASS=0; FAIL=0; TOTAL=0 + +assert() { + NAME="$1"; EXPECT="$2" + shift 2 + # remaining args are the full curl flags + TOTAL=$((TOTAL + 1)) + CODE=$(curl -s -o /dev/null -w '%{http_code}' "$@" 2>/dev/null || echo "000") + + if [ "$CODE" = "$EXPECT" ]; then + echo " ✓ ${NAME} (${CODE})" + PASS=$((PASS + 1)) + else + echo " ✗ ${NAME} — expected ${EXPECT}, got ${CODE}" + FAIL=$((FAIL + 1)) + fi +} + +# ── Wait for sidecar gateway to be fully ready ── +echo "⏳ Waiting for gateway sidecar..." +READY=false +for i in $(seq 1 60); do + CODE=$(curl -s -o /dev/null -w '%{http_code}' "${GW}/healthz" 2>/dev/null || echo "000") + if [ "$CODE" = "200" ]; then + sleep 1 + C2=$(curl -s -o /dev/null -w '%{http_code}' "${GW}/healthz" 2>/dev/null || echo "000") + C3=$(curl -s -o /dev/null -w '%{http_code}' "${GW}/healthz" 2>/dev/null || echo "000") + if [ "$C2" = "200" ] && [ "$C3" = "200" ]; then + READY=true + echo "✓ Gateway ready (stable after 3 checks)" + break + fi + fi + sleep 2 +done + +if [ "$READY" = "false" ]; then + echo "✗ Gateway never became ready" + echo "fail" > "${RESULTS_DIR}/result" + echo "0/0 gateway timeout" > "${RESULTS_DIR}/summary" + exit 1 +fi + +echo "" +echo "═══ Integration Tests ═══" +echo "" + +# ── Health (path-based, no headers) ── +echo "▸ Health" +assert "GET /healthz" 200 \ + -X GET "${GW}/healthz" +assert "GET /readyz" 200 \ + -X GET "${GW}/readyz" + +# ── Routing: missing headers → 400 ── +echo "▸ Header validation" +assert "no X-Service → 400" 400 \ + -X GET "${GW}/" +assert "X-Service without X-Resource → 400" 400 \ + -X GET -H "X-Service: memory" "${GW}/" +assert "unknown service → 404" 404 \ + -X GET -H "X-Service: nonexistent" -H "X-Resource: foo" "${GW}/" + +# ── Memory service (POST, no auth) ── +echo "▸ Memory service" +assert "memory/query (POST)" 200 \ + -X POST -H "X-Service: memory" -H "X-Resource: query" \ + -H "Content-Type: application/json" -d '{"query":"test"}' "${GW}/" +assert "memory/ingest (POST)" 200 \ + -X POST -H "X-Service: memory" -H "X-Resource: ingest" \ + -H "Content-Type: application/json" \ + -d '{"content":"integration test","metadata":{"source":"tekton"}}' "${GW}/" + +# ── S3 service (GET, no auth → MinIO 403) ── +echo "▸ S3 service" +assert "s3/list-objects (GET → 403)" 403 \ + -X GET -H "X-Service: s3" -H "X-Resource: list-objects" "${GW}/" + +# ── SQS service (GET, auth required → 401) ── +echo "▸ SQS service" +assert "sqs/list-queues (GET → 401)" 401 \ + -X GET -H "X-Service: sqs" -H "X-Resource: list-queues" "${GW}/" + +# ── Workflow service (gRPC, GET) ── +echo "▸ Workflow service" +assert "workflow/list (GET → upstream err)" 502 \ + -X GET -H "X-Service: workflow" -H "X-Resource: list" "${GW}/" + +# ── IAM service (GET, Authentik) ── +echo "▸ IAM service" +assert "iam/list-users (GET → Authentik redirect)" 302 \ + -X GET -H "X-Service: iam" -H "X-Resource: list-users" "${GW}/" + +echo "" +echo "═══ Results: ${PASS}/${TOTAL} passed, ${FAIL} failed ═══" + +# Write Tekton results +if [ "$FAIL" -eq 0 ]; then + echo "pass" > "${RESULTS_DIR}/result" +else + echo "fail" > "${RESULTS_DIR}/result" +fi +echo "${PASS}/${TOTAL} passed, ${FAIL} failed" > "${RESULTS_DIR}/summary" + +[ "$FAIL" -eq 0 ] diff --git a/k8s/tekton/task-integration-test.yaml b/k8s/tekton/task-integration-test.yaml index 9d9ff87..7971e25 100644 --- a/k8s/tekton/task-integration-test.yaml +++ b/k8s/tekton/task-integration-test.yaml @@ -7,79 +7,69 @@ metadata: app: api-gateway component: testing spec: - description: Run integration tests for API gateway + description: > + Spin up a gateway pod from the given image as a sidecar, + run curl-based integration tests, report pass/fail. params: - name: image type: string - description: Container image to test (including tag) - - name: timeout + description: "Container image to test (repo:tag)" + - name: gateway-port type: string - default: "5m" - description: Test timeout + default: "8080" results: - name: result - description: Test result (pass/fail) type: string - - name: message - description: Test summary message + - name: summary type: string + + sidecars: + - name: gateway + image: $(params.image) + env: + - name: LISTEN_ADDR + value: "0.0.0.0:$(params.gateway-port)" + - name: CONFIG_PATH + value: /etc/gateway/config.yaml + - name: LOG_LEVEL + value: info + - name: AUTH_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: api-gw-client-secret + key: client-secret + optional: true + volumeMounts: + - name: gateway-config + mountPath: /etc/gateway + readOnly: true + steps: - name: run-tests - image: $(params.image) - securityContext: - runAsNonRoot: true - runAsUser: 65532 - allowPrivilegeEscalation: false + image: curlimages/curl:8.13.0 env: - - name: GATEWAY_URL - value: "http://api-gateway:8080" - - name: CI - value: "true" - script: | - #!/bin/sh - set -e - - echo "🧪 Starting integration tests..." - echo "Image: $(params.image)" - echo "Gateway: $GATEWAY_URL" - echo "" - - # Wait for gateway to be ready - echo "Waiting for gateway service..." - for i in $(seq 1 30); do - if curl -s $GATEWAY_URL/healthz > /dev/null 2>&1; then - echo "✓ Gateway is ready" - break - fi - echo "Attempt $i/30: Waiting for gateway..." - sleep 2 - done - - # Run integration tests - echo "Running integration tests..." - if go test -v -tags=integration -timeout=$(params.timeout) ./internal/integration/...; then - echo "pass" | tee $(results.result.path) - echo "✓ All integration tests passed" | tee $(results.message.path) - exit 0 - else - echo "fail" | tee $(results.result.path) - echo "✗ Some integration tests failed" | tee $(results.message.path) - exit 1 - fi + - name: GW + value: "http://localhost:$(params.gateway-port)" + - name: RESULTS_DIR + value: /tekton/results + command: ["sh", "/scripts/integration-test.sh"] volumeMounts: - - name: tmp - mountPath: /tmp - - name: home - mountPath: /home/nonroot - resources: + - name: test-script + mountPath: /scripts + readOnly: true + computeResources: requests: - cpu: 250m - memory: 512Mi + cpu: 100m + memory: 64Mi limits: - cpu: 500m - memory: 1Gi + cpu: 200m + memory: 128Mi + volumes: - - name: tmp - emptyDir: {} - - name: home - emptyDir: {} + - name: gateway-config + secret: + secretName: api-gateway-config + - name: test-script + configMap: + name: integration-test-script + defaultMode: 0755