Files
cleveragents-core/robot/k8s_helm_chart.robot
T
brent.edwards b51df2ee0f
CI / docker (push) Blocked by required conditions
CI / status-check (push) Blocked by required conditions
CI / build (push) Successful in 20s
CI / helm (push) Successful in 23s
CI / lint (push) Successful in 3m19s
CI / typecheck (push) Successful in 3m57s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m5s
CI / integration_tests (push) Successful in 7m25s
CI / unit_tests (push) Failing after 13m18s
CI / quality (push) Failing after 13m19s
CI / e2e_tests (push) Failing after 18m16s
CI / coverage (push) Failing after 19m20s
CI / benchmark-publish (push) Failing after 28m16s
feat(server): add Kubernetes Helm chart for server deployment (#1085)
## Summary

This PR adds Kubernetes Helm deployment support for the CleverAgents server.

Closes #928

### What this PR includes

- Helm chart under `k8s/` with Deployment, Service, optional Ingress, ConfigMap,
  ServiceAccount, Secrets, NOTES, and optional Redis subchart configuration.
- Multi-stage `Dockerfile.server` for server runtime deployment.
- Deployment-focused docs in `k8s/README.md`.
- Behave + Robot + benchmark coverage for chart/deployment wiring.

### Review fixes applied (cycle 11 — hurui200320 review #2687)

**Critical fix:**

1. **CI SHA256 checksum verification fixed** — All 3 Helm install blocks (`unit_tests`, `integration_tests`, `helm` jobs) now save the tarball using its original filename (`helm-v3.16.4-linux-amd64.tar.gz`) instead of `helm.tgz`, so the `.sha256sum` file can correctly locate and verify it. This was causing all 3 CI Helm jobs to fail with "No such file or directory".

**Major fixes (test coverage gaps):**

2. **405 Allow header now tested** — The existing "POST to known path returns method not allowed" scenario now asserts that the `Allow: GET` header is present. Added a second 405 scenario testing `POST /live` for broader `_KNOWN_PATHS` coverage (review item #11).
3. **Security-hardening headers now tested** — New scenario "HTTP responses include security-hardening headers" verifies `content-length`, `x-content-type-options: nosniff`, and `cache-control: no-store` headers are present on HTTP responses.
4. **Lifespan warning logging now tested** — New scenario "Unrecognised lifespan message type logs warning and continues" queues `lifespan.startup` → `lifespan.bogus` → `lifespan.shutdown` and verifies: (a) the app completes the lifespan cycle cleanly, (b) a warning is logged mentioning the unrecognised type.

### Review fixes applied (cycle 10)

**Critical/Major fixes (from hurui200320's prior REQUEST_CHANGES):**

1. **Rebased branch onto master** — Removed merge commit per CONTRIBUTING.md rebase-only policy. Clean linear history restored.
2. **Fixed commit message body** — Replaced literal `\n` sequences with actual newlines. `ISSUES CLOSED: #928` footer is now on its own line after a blank separator.
3. **Moved uvicorn import to module top level** — `from uvicorn import run as uvicorn_run` is now at the top of `src/cleveragents/cli/commands/server.py` per Import Guidelines. Updated test mock target from `uvicorn.run` to `cleveragents.cli.commands.server.uvicorn_run`.
4. **Added SHA256 checksum verification** — All three Helm CLI install blocks in `.forgejo/workflows/ci.yml` now download and verify `helm.sha256sum` before extracting the binary.

**Minor fixes:**

5. **Added `Dockerfile.server` build to CI** — New "Build Docker image (Server)" step in the `docker` job validates the server Dockerfile.
6. **ASGI 405 Method Not Allowed** — Known paths (`/`, `/live`, `/ready`, `/health`) now return 405 with `Allow: GET` header for non-GET methods, per RFC 9110 §15.5.6. Added `_KNOWN_PATHS` frozenset.
7. **WebSocket close protocol fix** — App now calls `await receive()` to consume the `websocket.connect` event before closing. Changed close code from 1000 (Normal Closure) to 1008 (Policy Violation).
8. **Lifespan handler logging** — Unrecognised lifespan message types are now logged as warnings instead of silently consumed.
9. **Security-hardening headers** — `_send_response` now includes `content-length`, `x-content-type-options: nosniff`, and `cache-control: no-store` on all HTTP responses.
10. **`.dockerignore` credential patterns** — Added `*.pem`, `*.key`, `*.p12`, `*.pfx`, `credentials*.json`.
11. **`--log-level` validation** — Constrained to `click.Choice(["critical", "error", "warning", "info", "debug", "trace"])` for clean CLI validation errors.
12. **Reverted unrelated semgrep pre-commit change** — `pass_filenames` and `entry` restored to original values per atomic commit hygiene.
13. **Removed unused `ReceiveCallable` type alias** and `Callable`/`Awaitable` imports from `asgi_app_steps.py`.
14. **Fixed redundant `shutil.which("helm")` check** — `_skip_if_helm_missing` now returns `bool` to eliminate the duplicate check in `_render_chart`.
15. **Improved test deque error handling** — Lifespan test receive mock now raises descriptive `AssertionError` instead of opaque `IndexError`.
16. **Scope type dispatch** — Changed `if/if/if` to `if/elif/elif` for mutually exclusive ASGI scope types.
17. **Dockerfile.server base image** — Standardised to `python:3.13-slim` (floating minor) consistent with CLI Dockerfile.
18. **Dockerfile layer caching** — Split `uv pip install build` and `python -m build` into separate `RUN` instructions.
19. **Removed extraneous double blank line** in Dockerfile.server.

### Deferred items (acknowledged, not in scope)

- PodDisruptionBudget, HorizontalPodAutoscaler, NetworkPolicy — Follow-up for production hardening.
- `appVersion: "1.0.0"` placeholder — Needs tracking issue for release versioning alignment.
- Readiness probe with downstream dependency checks — Documented limitation.
- Cross-system test for probe paths matching ASGI routes — Test enhancement.
- Improved benchmarks (helm template timing vs PyYAML parsing) — Benchmark quality improvement.
- CI DRY violation (Helm install 3×) — Code quality improvement, consider composite action.
- File length limits exceeded (`k8s_helm_chart_steps.py` 551 lines, `helper_k8s_helm_chart.py` 678 lines) — Non-blocking, can be split in follow-up.
- `runAsGroup: 1000` in pod security context — Defense-in-depth improvement.
- HEAD method support on known paths — RFC compliance, does not affect K8s probes.
- `click.Choice` log-level validation via CLI runner test — Test gap.

### Scope note: status-check CI gate

The `status-check` job now includes `integration_tests`, `e2e_tests`, and `helm` in its `needs` list. The `helm` job is new in this PR. The `integration_tests` and `e2e_tests` additions fix previously-missing gate checks — included here since this PR modifies both of those jobs to install Helm.

### Quality gates

- `nox -e lint` 
- `nox -e typecheck` 
- `nox -e unit_tests`  (12,321 scenarios passed, 4 skipped)
- `nox -e integration_tests` — 3 pre-existing failures in unrelated areas (plan correction, resource types)
- `nox -e e2e_tests` — pre-existing failures (LLM API keys not available in local env)
- `nox -e coverage_report`  (**97.7%**)

Reviewed-on: #1085
Reviewed-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com>
Co-authored-by: Brent E. Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent E. Edwards <brent.edwards@cleverthis.com>
2026-03-27 23:53:22 +00:00

203 lines
11 KiB
Plaintext

*** Settings ***
Documentation Integration tests for Kubernetes Helm chart cross-file consistency.
... Every test case validates interactions between multiple chart files
... (templates, values.yaml, Chart.yaml, Dockerfile) to catch wiring
... errors that single-file Behave scenarios cannot detect.
Resource ${CURDIR}/common.resource
Library OperatingSystem
Library Collections
Library String
Library Process
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${K8S_DIR} ${CURDIR}/../k8s
${TEMPLATES_DIR} ${K8S_DIR}/templates
${DOCKERFILE_SERVER} ${CURDIR}/../Dockerfile.server
${HELPER} ${CURDIR}/helper_k8s_helm_chart.py
*** Keywords ***
Assert Rendered Check Result
[Arguments] ${result} ${ok_marker} ${skip_marker}
Should Be Equal As Integers ${result.rc} 0 Render check failed: ${result.stderr}
${stdout}= Strip String ${result.stdout}
${strict}= Get Environment Variable CLEVERAGENTS_REQUIRE_HELM_RENDER_ASSERTIONS false
${strict}= Convert To Lower Case ${strict}
IF '${strict}' == 'true' or '${strict}' == '1' or '${strict}' == 'yes'
Should Be Equal As Strings ${stdout} ${ok_marker}
ELSE
${is_ok}= Run Keyword And Return Status Should Be Equal As Strings ${stdout} ${ok_marker}
IF not ${is_ok}
Should Be Equal As Strings ${stdout} ${skip_marker}
END
END
*** Test Cases ***
Port Values Are Consistent Across Templates Values And Dockerfile
[Documentation] Cross-file check: service.port and server.port must agree
... in values.yaml, deployment.yaml must reference server.port
... for containerPort, service.yaml must use named port, and
... Dockerfile EXPOSE must match the default port.
[Tags] k8s integration critical
${result}= Run Process ${PYTHON} ${HELPER} validate_cross_file_port_consistency
... cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0 Port consistency check failed: ${result.stderr}
Should Contain ${result.stdout} Port consistency OK
Secrets Template References Match Values Schema And Deployment Wiring
[Documentation] Cross-file check: secrets.yaml must reference the same
... credential keys defined in values.yaml, and deployment.yaml
... must reference the same secret key names.
[Tags] k8s integration critical
${result}= Run Process ${PYTHON} ${HELPER} validate_secrets_structure
... cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0 Secrets structure check failed: ${result.stderr}
Should Contain ${result.stdout} Secrets structure OK
Deployment Command Args Match Dockerfile Entrypoint And ASGI Module
[Documentation] Cross-file check: Deployment template passes server config
... as CLI args to uvicorn, matching the Dockerfile ENTRYPOINT,
... and both reference the same ASGI application module.
[Tags] k8s integration critical
${result}= Run Process ${PYTHON} ${HELPER} validate_deployment_command_args
... cwd=${WORKSPACE} on_timeout=kill timeout=120s
Assert Rendered Check Result
... ${result}
... OK: deployment-command-args-rendered
... SKIP: deployment-command-args-rendered
Deployment EnvFrom References ConfigMap Name And Values Keys
[Documentation] Cross-file check: Deployment template uses envFrom with
... configMapRef matching the ConfigMap name suffix, and the
... ConfigMap env vars correspond to values.yaml server keys.
[Tags] k8s integration
${result}= Run Process ${PYTHON} ${HELPER} validate_envfrom_wiring
... cwd=${WORKSPACE} on_timeout=kill timeout=120s
Assert Rendered Check Result
... ${result}
... OK: envfrom-wiring-rendered
... SKIP: envfrom-wiring-rendered
Redis Host Helper Matches Bitnami Naming And Chart Dependency
[Documentation] Cross-file check: the Redis host helper uses .Release.Name
... to match Bitnami subchart naming, the deployment template
... uses this helper, and Chart.yaml declares the dependency.
[Tags] k8s integration
${result}= Run Process ${PYTHON} ${HELPER} validate_redis_host_helper
... cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0 Redis host helper check failed: ${result.stderr}
Should Contain ${result.stdout} Redis host helper OK
Probe Configuration Matches Between Values And Deployment Template
[Documentation] Cross-file check: probes in values.yaml split /live and /ready
... and the deployment template renders them via toYaml.
[Tags] k8s integration
${result}= Run Process ${PYTHON} ${HELPER} validate_probe_config
... cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0 Probe config check failed: ${result.stderr}
Should Contain ${result.stdout} Probe config OK
Image Defaults Are Consistent Between Values And Deployment Template
[Documentation] Cross-file check: image section in values.yaml has sensible
... defaults and the deployment template correctly references
... repository, pullPolicy, and tag values.
[Tags] k8s integration
${result}= Run Process ${PYTHON} ${HELPER} validate_image_defaults
... cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0 Image defaults check failed: ${result.stderr}
Should Contain ${result.stdout} Image defaults OK
Service Defaults Match Between Values And Service Template
[Documentation] Cross-file check: service section in values.yaml defaults
... to ClusterIP on port 8000 and the service template
... references the correct values paths.
[Tags] k8s integration
${result}= Run Process ${PYTHON} ${HELPER} validate_service_defaults
... cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0 Service defaults check failed: ${result.stderr}
Should Contain ${result.stdout} Service defaults OK
Server Dockerfile Is Multi-Stage With Non-Root User
[Documentation] Cross-file check: Dockerfile.server uses multi-stage build
... and creates the same non-root user referenced by the
... podSecurityContext in values.yaml.
[Tags] k8s dockerfile
${result}= Run Process ${PYTHON} ${HELPER} validate_dockerfile_multistage_nonroot
... cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0 Dockerfile multi-stage check failed: ${result.stderr}
Should Contain ${result.stdout} Dockerfile multi-stage OK
Redis Disabled Rendering Omits Redis Environment Wiring
[Documentation] Render-based check (when Helm is available): with
... redis.enabled=false, rendered Deployment must not
... include Redis environment variables.
... If Helm is unavailable, helper returns a skip note.
[Tags] k8s integration
${result}= Run Process ${PYTHON} ${HELPER} validate_redis_disabled_rendering
... cwd=${WORKSPACE} on_timeout=kill timeout=120s
Assert Rendered Check Result
... ${result}
... OK: redis-disabled-rendering
... SKIP: redis-disabled-rendering
Database Existing Secret Rendering Uses Existing Secret Wiring
[Documentation] Render-based check (when Helm is available): with
... database.existingSecret configured, rendered Deployment
... should reference that secret for CLEVERAGENTS_DATABASE_URL.
[Tags] k8s integration
${result}= Run Process ${PYTHON} ${HELPER} validate_database_existing_secret_rendering
... cwd=${WORKSPACE} on_timeout=kill timeout=120s
Assert Rendered Check Result
... ${result}
... OK: database-existing-secret-rendering
... SKIP: database-existing-secret-rendering
Redis Enabled Existing Secret Rendering Uses Existing Secret Wiring
[Documentation] Render-based check (when Helm is available): with
... redis.enabled=true and redis.auth.existingSecret set,
... rendered Deployment should consume that secret.
[Tags] k8s integration
${result}= Run Process ${PYTHON} ${HELPER} validate_redis_enabled_existing_secret_rendering
... cwd=${WORKSPACE} on_timeout=kill timeout=120s
Assert Rendered Check Result
... ${result}
... OK: redis-enabled-existing-secret-rendering
... SKIP: redis-enabled-existing-secret-rendering
Redis Enabled Inline Password Rendering Uses Generated Secret Wiring
[Documentation] Render-based check (when Helm is available): with
... redis.enabled=true and redis.auth.password set,
... rendered Deployment should reference generated redis Secret.
[Tags] k8s integration
${result}= Run Process ${PYTHON} ${HELPER} validate_redis_enabled_inline_password_rendering
... cwd=${WORKSPACE} on_timeout=kill timeout=120s
Assert Rendered Check Result
... ${result}
... OK: redis-enabled-inline-password-rendering
... SKIP: redis-enabled-inline-password-rendering
Redis Enabled Fallback Rendering Uses Bitnami Secret Wiring
[Documentation] Render-based check (when Helm is available): with
... redis.enabled=true and no explicit Redis credential,
... rendered Deployment should reference Bitnami secret.
[Tags] k8s integration
${result}= Run Process ${PYTHON} ${HELPER} validate_redis_enabled_bitnami_fallback_rendering
... cwd=${WORKSPACE} on_timeout=kill timeout=120s
Assert Rendered Check Result
... ${result}
... OK: redis-enabled-bitnami-fallback-rendering
... SKIP: redis-enabled-bitnami-fallback-rendering
Ingress TLS Rendering Succeeds With TLS Configured
[Documentation] Render-based positive check (when Helm is available):
... ingress.enabled=true with TLS configured and
... allowInsecure=false should render Ingress successfully.
[Tags] k8s integration
${result}= Run Process ${PYTHON} ${HELPER} validate_ingress_tls_rendering_success
... cwd=${WORKSPACE} on_timeout=kill timeout=120s
Assert Rendered Check Result
... ${result}
... OK: ingress-tls-rendering-success
... SKIP: ingress-tls-rendering-success