From 7c5bb6483a80e1b1cb76a02b37fb7a647332a214 Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Thu, 21 May 2026 13:02:50 -0300 Subject: [PATCH 01/16] docs: add design spec for CLIEN-781 memory & CPU limits --- ...5-21-clien-781-memory-cpu-limits-design.md | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md diff --git a/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md b/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md new file mode 100644 index 00000000..c5cbaa0c --- /dev/null +++ b/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md @@ -0,0 +1,163 @@ +# CLIEN-781 — Configurable CPU & RAM limits for k8s scope + +Status: design approved (2026-05-21) +Ticket: https://nullplatform.atlassian.net/browse/CLIEN-781 +Client: Spin +Assignee: Federico Maleh + +## Context + +Today the k8s scope exposes two capabilities — `ram_memory` and `cpu_millicores` — that are used as **both** the Kubernetes request and the Kubernetes limit. The Spin team needs to decouple them so that limits can be set higher than requests when desired, while keeping the default behavior unchanged for existing scopes. + +The risk that drives the UI shape: a memory `limit > request` increases the chance the scheduler/OOMKiller kills a pod under pressure. So memory limit is a sharp tool that should be hidden behind an "advanced" surface, not the main form. + +## Goals + +1. Add `cpu_millicores_limit` and `ram_memory_limit` as optional capabilities. +2. Keep the main form intact — `ram_memory` (request) stays at the top, untouched. +3. Group the new fields with the existing `cpu_millicores` in a renamed `Resources` tab inside the collapsable "ADVANCED" categorization. +4. Validate `limit >= request` at the JSON schema layer. +5. Be backwards compatible: missing or null limit ⇒ fall back to the request value, matching today's render. + +## Non-goals + +- No change to `ram_memory` or `cpu_millicores` themselves (same field types, same defaults). +- No cross-scope validation. +- No docsite update in this ticket (separate PR if requested). +- No CLI/API change beyond what naturally happens by adding properties to the scope spec. + +## UI design + +### Form layout (after the change) + +``` +Main form +├─ RAM Memory (request, dropdown — unchanged) +└─ Visibility + +▼ ADVANCED +├─ Resources ← renamed from "Processor" +│ ├─ CPU Millicores (request — existing) +│ ├─ CPU Millicores Limit ← NEW (optional integer) +│ └─ RAM Memory Limit ← NEW (dropdown with "Same as request") +├─ Size & Scaling +├─ Exposed Ports +├─ Scheduled Stop +├─ Protocol +├─ Continuous deployment +└─ Health Check +``` + +Asymmetry between RAM and CPU is intentional: RAM request stays in the main form (everyone tunes it), RAM limit lives in `Resources` (sharp tool). CPU request and CPU limit both live in `Resources` (CPU was already advanced). + +### Tab rename rationale + +`Resources` follows Kubernetes vocabulary (`resources: requests/limits`) and is generic enough to host both CPU and memory tuning. Alternatives considered (`Compute`, `Compute & Limits`) were rejected as less standard. + +## Schema changes — `k8s/specs/service-spec.json.tpl` + +### New properties (siblings of the existing ones) + +```json +"cpu_millicores_limit": { + "type": ["integer", "null"], + "title": "CPU Millicores Limit", + "default": null, + "maximum": 4000, + "minimum": { "$data": "1/cpu_millicores" }, + "description": "Maximum CPU the container can use. Leave empty to use the same value as the request." +}, +"ram_memory_limit": { + "type": ["integer", "null"], + "title": "RAM Memory Limit", + "default": null, + "oneOf": [ + { "const": null, "title": "Same as request" }, + { "const": 64, "title": "64 MB" }, + { "const": 128, "title": "128 MB" }, + { "const": 256, "title": "256 MB" }, + { "const": 512, "title": "512 MB" }, + { "const": 1024, "title": "1 GB" }, + { "const": 2048, "title": "2 GB" }, + { "const": 4096, "title": "4 GB" }, + { "const": 8192, "title": "8 GB" }, + { "const": 16384, "title": "16 GB" } + ], + "minimum": { "$data": "1/ram_memory" }, + "description": "Maximum memory the container can use. Setting this higher than the request increases OOMKill risk." +} +``` + +Neither property is added to the `required` array of `attributes.schema` — both are optional. + +### uiSchema changes + +Two edits in the existing `Categorization` block: + +1. Change `"label": "Processor"` → `"label": "Resources"`. +2. Add two `Control` entries inside that category's `elements`: + +```json +{ + "type": "Category", + "label": "Resources", + "elements": [ + { "type": "Control", "label": "CPU Millicores", "scope": "#/properties/cpu_millicores" }, + { "type": "Control", "label": "CPU Millicores Limit", "scope": "#/properties/cpu_millicores_limit" }, + { "type": "Control", "label": "RAM Memory Limit", "scope": "#/properties/ram_memory_limit" } + ] +} +``` + +No SHOW/HIDE rules are needed — the "Same as request" option (RAM) and empty value (CPU) act as the no-op state. + +## Validation + +`minimum` with `$data` references the sibling request field. JSON Schema only applies `minimum` to numeric instances, so `null` (or missing) values skip the check naturally — no `if/then` block required. + +The pattern matches the precedent already in this spec: +`health_check.period_seconds.exclusiveMinimum.$data = "1/timeout_seconds"`. + +## Render logic in the deployment template + +The k8s deployment manifest (currently rendering both request and limit from the same capability) must use the new fields with a jq `// fallback`: + +```bash +CPU_REQ=$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores') +CPU_LIM=$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores_limit // .scope.capabilities.cpu_millicores') + +RAM_REQ=$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory') +RAM_LIM=$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory_limit // .scope.capabilities.ram_memory') +``` + +`// .scope.capabilities.cpu_millicores` evaluates to the request value when the limit is `null` or missing, giving the exact retrocompat the ticket asks for. + +The implementation plan will locate the exact file(s) under `k8s/deployment/` that render `resources:` and apply this change. + +## Backwards compatibility + +| Scenario | Behavior | +|---|---| +| Existing scope, no new properties in DB | jq fallback ⇒ limit = request ⇒ identical manifest to today | +| New scope, user does not touch limits | Defaults are `null` ⇒ same as above | +| New scope, user picks a higher limit | Manifest renders the explicit limit; schema validates `limit ≥ request` | +| User tries `limit < request` | JSON schema rejects via `$data` minimum before the workflow runs | + +No data migration needed. + +## Testing plan (high-level) + +- **BATS unit tests** for the deployment script: cover the four matrix cells (limit set / limit null, for both CPU and RAM), asserting the rendered `resources:` block. +- **JSON schema validation tests** (if a test harness exists for the spec): assert that `limit < request` is rejected and `limit >= request` is accepted, including the `null` case. +- **Manual smoke** in a dev environment after the implementation lands. + +The testing detail belongs to the implementation plan (writing-plans), not this design doc. + +## Open questions + +- Exact deployment template file location and templating engine (gomplate vs helm vs raw bash + jq) — to be confirmed at implementation time. The render logic above is engine-agnostic in spirit but the syntax will be adapted. + +## Out of scope / follow-ups + +- Docsite documentation (under `~/nullplatform/apps/docsite/`) — separate ticket if Spin needs it user-facing. +- Symmetric treatment for other resource dimensions (ephemeral storage, GPUs) — not requested. From feb746a39b9652c53c6d1495962f0c6b00c6dc53 Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Thu, 21 May 2026 13:10:52 -0300 Subject: [PATCH 02/16] docs: add implementation plan for CLIEN-781 memory & CPU limits --- .../2026-05-21-clien-781-memory-cpu-limits.md | 525 ++++++++++++++++++ 1 file changed, 525 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-21-clien-781-memory-cpu-limits.md diff --git a/docs/superpowers/plans/2026-05-21-clien-781-memory-cpu-limits.md b/docs/superpowers/plans/2026-05-21-clien-781-memory-cpu-limits.md new file mode 100644 index 00000000..f954d73b --- /dev/null +++ b/docs/superpowers/plans/2026-05-21-clien-781-memory-cpu-limits.md @@ -0,0 +1,525 @@ +# CLIEN-781 — Memory & CPU Limits Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add optional `cpu_millicores_limit` and `ram_memory_limit` capabilities to the k8s scope so the Spin client can set Kubernetes `resources.limits` independently from `resources.requests`, with safe back-compat defaults. + +**Architecture:** Add two new optional properties to the k8s scope spec. Normalize them inside `build_context` (limit defaults to request when null/missing) so the deployment template stays trivial. Render the normalized values into the application container's `resources.limits` while keeping `resources.requests` bound to the original `cpu_millicores` / `ram_memory` fields. + +**Tech Stack:** JSON Schema (with JSONForms uiSchema), bash + jq for context normalization, gomplate for template rendering, BATS for tests. + +**Spec:** [`docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md`](../specs/2026-05-21-clien-781-memory-cpu-limits-design.md) + +--- + +## File Structure + +**Modified files:** + +- `k8s/specs/service-spec.json.tpl` — add two `properties` and update the `Categorization`/`Category` to rename "Processor" → "Resources" and add two new `Control` entries. +- `k8s/deployment/build_context` — add a `normalize_capability_limits` function that mutates `$CONTEXT` to fill `.scope.capabilities.cpu_millicores_limit` and `.scope.capabilities.ram_memory_limit` with the request value when null/missing. Call it before the final context write. +- `k8s/deployment/templates/deployment.yaml.tpl` — application container only (lines 313–319): keep `requests.cpu/memory` bound to `cpu_millicores` / `ram_memory`, change `limits.cpu/memory` to read `cpu_millicores_limit` / `ram_memory_limit`. Sidecars (lines 148–153, 201–206, 255–260) are NOT touched — they use `container_cpu_in_millicores` / `container_memory_in_memory` from a ConfigMap. + +**New tests:** + +- `k8s/deployment/tests/build_context.bats` — add a section for `normalize_capability_limits` covering the four matrix cells (limit set / limit null, for CPU and RAM) plus the "field absent" case. +- `k8s/deployment/tests/deployment_template_shape.bats` (new file) — grep-based structural assertions that the application container `resources` block uses the right field for request vs limit. Mirrors `tests/ingress_template_shape.bats`. + +**Not modified:** sidecar resource blocks, CLI, docsite, API spec. + +--- + +## Task 1: Add `cpu_millicores_limit` and `ram_memory_limit` properties to the JSON schema + +**Files:** +- Modify: `k8s/specs/service-spec.json.tpl` (properties block, lines 485–492 area for CPU; lines 315–358 area for RAM) + +There is no JSON-schema test harness in this repo, so this task has no automated test. The schema is validated implicitly by the deployment workflow and by manual `jq` sanity checks in step 2. + +- [ ] **Step 1: Add the two new properties to `attributes.schema.properties`** + +After the existing `cpu_millicores` property block (end at line 492), add `cpu_millicores_limit`: + +```json +, +"cpu_millicores_limit":{ + "type":["integer","null"], + "title":"CPU Millicores Limit", + "default":null, + "maximum":4000, + "minimum":{ + "$data":"1/cpu_millicores" + }, + "description":"Maximum CPU the container can use (in millicores). Leave empty to use the same value as the request." +} +``` + +After the existing `ram_memory` property block (end at line 358), add `ram_memory_limit`: + +```json +, +"ram_memory_limit":{ + "type":["integer","null"], + "oneOf":[ + {"const":null, "title":"Same as request"}, + {"const":64, "title":"64 MB"}, + {"const":128, "title":"128 MB"}, + {"const":256, "title":"256 MB"}, + {"const":512, "title":"512 MB"}, + {"const":1024, "title":"1 GB"}, + {"const":2048, "title":"2 GB"}, + {"const":4096, "title":"4 GB"}, + {"const":8192, "title":"8 GB"}, + {"const":16384, "title":"16 GB"} + ], + "title":"RAM Memory Limit", + "default":null, + "minimum":{ + "$data":"1/ram_memory" + }, + "description":"Maximum memory the container can use (in MB). Setting this higher than the request increases the chance the scheduler kills the pod under pressure." +} +``` + +Do NOT add either field to the top-level `required` array — both stay optional. + +- [ ] **Step 2: Validate the JSON is still well-formed** + +Run: +```bash +jq empty k8s/specs/service-spec.json.tpl +``` +Expected: no output, exit code 0. + +If gomplate is available locally, also confirm the template renders to valid JSON: +```bash +NRN="nrn:test" gomplate -f k8s/specs/service-spec.json.tpl | jq empty +``` +Expected: no output, exit code 0. + +- [ ] **Step 3: Commit** + +```bash +git add k8s/specs/service-spec.json.tpl +git commit -m "feat: add cpu_millicores_limit and ram_memory_limit properties to k8s scope spec" +``` + +--- + +## Task 2: Rename "Processor" → "Resources" and add the limit Controls to the uiSchema + +**Files:** +- Modify: `k8s/specs/service-spec.json.tpl` (uiSchema `Category` block, lines 46–55) + +No automated test — uiSchema is rendered by the frontend. We validate by grep-based assertion in step 2 and visual smoke later. + +- [ ] **Step 1: Rename the Category label and add two Controls** + +Locate the `Category` whose label is `"Processor"` (line 47). Replace the whole block (lines 46–55) with: + +```json +{ + "type":"Category", + "label":"Resources", + "elements":[ + { + "type":"Control", + "label":"CPU Millicores", + "scope":"#/properties/cpu_millicores" + }, + { + "type":"Control", + "label":"CPU Millicores Limit", + "scope":"#/properties/cpu_millicores_limit" + }, + { + "type":"Control", + "label":"RAM Memory Limit", + "scope":"#/properties/ram_memory_limit" + } + ] +} +``` + +- [ ] **Step 2: Sanity-check the uiSchema is well-formed and has the expected shape** + +Run: +```bash +jq -e ' + .attributes.schema.uiSchema + | .. | objects | select(.label? == "Resources") + | .elements | map(.scope) as $scopes + | ($scopes | length) == 3 + and ($scopes | index("#/properties/cpu_millicores") != null) + and ($scopes | index("#/properties/cpu_millicores_limit") != null) + and ($scopes | index("#/properties/ram_memory_limit") != null) +' k8s/specs/service-spec.json.tpl >/dev/null && echo OK +``` +Expected: `OK`. + +Also confirm "Processor" is gone: +```bash +! grep -q '"Processor"' k8s/specs/service-spec.json.tpl && echo OK +``` +Expected: `OK`. + +- [ ] **Step 3: Commit** + +```bash +git add k8s/specs/service-spec.json.tpl +git commit -m "feat: rename Processor tab to Resources and surface CPU/RAM limit controls" +``` + +--- + +## Task 3: Add `normalize_capability_limits` to `build_context` (TDD) + +**Files:** +- Modify: `k8s/deployment/build_context` +- Modify: `k8s/deployment/tests/build_context.bats` + +This is the back-compat heart of the change. The function takes `$CONTEXT` (JSON) and fills `.scope.capabilities.cpu_millicores_limit` and `.scope.capabilities.ram_memory_limit` with the corresponding request value when the field is `null` or missing. Existing values pass through unchanged. + +- [ ] **Step 1: Write failing tests in `tests/build_context.bats`** + +Append at the end of `k8s/deployment/tests/build_context.bats`: + +```bash +# ============================================================================= +# normalize_capability_limits Function Tests (CLIEN-781) +# Fills in *_limit with the corresponding request value when null or missing, +# leaves explicit values untouched. +# ============================================================================= + +setup_normalize_limits_fn() { + eval "$(sed -n '/^normalize_capability_limits()/,/^}/p' "$PROJECT_ROOT/k8s/deployment/build_context")" +} + +@test "normalize_capability_limits: fills CPU limit from request when limit is absent" { + setup_normalize_limits_fn + local in='{"scope":{"capabilities":{"cpu_millicores":500,"ram_memory":1024,"ram_memory_limit":2048}}}' + local out + out=$(normalize_capability_limits "$in") + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" +} + +@test "normalize_capability_limits: fills RAM limit from request when limit is absent" { + setup_normalize_limits_fn + local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":700,"ram_memory":1024}}}' + local out + out=$(normalize_capability_limits "$in") + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" +} + +@test "normalize_capability_limits: fills both limits when both are absent" { + setup_normalize_limits_fn + local in='{"scope":{"capabilities":{"cpu_millicores":500,"ram_memory":1024}}}' + local out + out=$(normalize_capability_limits "$in") + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" +} + +@test "normalize_capability_limits: fills both limits when both are explicit null" { + setup_normalize_limits_fn + local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":null,"ram_memory":1024,"ram_memory_limit":null}}}' + local out + out=$(normalize_capability_limits "$in") + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" +} + +@test "normalize_capability_limits: preserves explicit non-null limits" { + setup_normalize_limits_fn + local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":2000,"ram_memory":1024,"ram_memory_limit":4096}}}' + local out + out=$(normalize_capability_limits "$in") + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "2000" + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "4096" +} +``` + +- [ ] **Step 2: Run the new tests, confirm they fail** + +Run: +```bash +bats k8s/deployment/tests/build_context.bats -f normalize_capability_limits +``` +Expected: 5 failures, message about `normalize_capability_limits: command not found` (or similar — function does not exist yet). + +- [ ] **Step 3: Implement `normalize_capability_limits` in `build_context`** + +Open `k8s/deployment/build_context`. Above the `validate_status()` function (search for `^validate_status\(\)`), insert: + +```bash +# Fill in *_limit capability fields with the corresponding request value when +# the limit is missing or explicitly null. Idempotent. CLIEN-781. +normalize_capability_limits() { + echo "$1" | jq ' + .scope.capabilities.cpu_millicores_limit = (.scope.capabilities.cpu_millicores_limit // .scope.capabilities.cpu_millicores) + | .scope.capabilities.ram_memory_limit = (.scope.capabilities.ram_memory_limit // .scope.capabilities.ram_memory) + ' +} +``` + +Then wire it into the final context assembly. Find the block ending at line 314 (the big `jq '. + { ... }')` invocation around lines 285–314 that produces the final `$CONTEXT`). Immediately after that block (i.e., right before the `DEPLOYMENT_ID=$(echo "$CONTEXT" | jq -r '.deployment.id')` line at 316), add: + +```bash +CONTEXT=$(normalize_capability_limits "$CONTEXT") +``` + +- [ ] **Step 4: Run the new tests, confirm they pass** + +Run: +```bash +bats k8s/deployment/tests/build_context.bats -f normalize_capability_limits +``` +Expected: 5 tests pass. + +- [ ] **Step 5: Run the full build_context test suite to ensure no regressions** + +Run: +```bash +bats k8s/deployment/tests/build_context.bats +``` +Expected: all tests pass (baseline of this file is currently green per the existing CI; we are only adding tests). + +- [ ] **Step 6: Commit** + +```bash +git add k8s/deployment/build_context k8s/deployment/tests/build_context.bats +git commit -m "feat: normalize cpu/ram limit capabilities to request value when unset" +``` + +--- + +## Task 4: Render limits from normalized fields in the application container (TDD via template-shape test) + +**Files:** +- Create: `k8s/deployment/tests/deployment_template_shape.bats` +- Modify: `k8s/deployment/templates/deployment.yaml.tpl` (lines 313–319 only — the application container, NOT the sidecars) + +We assert the template shape with grep (same approach as `ingress_template_shape.bats`). End-to-end rendering through gomplate is exercised by the existing build pipeline; the shape test catches regressions like accidentally rebinding `limits.cpu` back to `cpu_millicores`. + +- [ ] **Step 1: Write the failing template-shape test** + +Create `k8s/deployment/tests/deployment_template_shape.bats`: + +```bash +#!/usr/bin/env bats +# ============================================================================= +# Structural tests for the deployment template. +# Verifies the application container's resources block uses the right +# capability for request vs limit. CLIEN-781. +# ============================================================================= + +setup() { + export PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" + source "$PROJECT_ROOT/testing/assertions.sh" + export TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/deployment.yaml.tpl" +} + +# Slice the file from "name: application" to the next container header, +# isolating the application container's block from the sidecars (which keep +# using container_cpu_in_millicores / container_memory_in_memory). +app_container_block() { + awk ' + /^[[:space:]]+- name: application[[:space:]]*$/ { in_app=1 } + in_app { print } + /^[[:space:]]+terminationMessagePolicy:/ && in_app { exit } + ' "$TEMPLATE" +} + +@test "deployment template: application container limits.cpu uses cpu_millicores_limit" { + block=$(app_container_block) + echo "$block" | grep -E 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores_limit[[:space:]]*\}\}m' >/dev/null +} + +@test "deployment template: application container limits.memory uses ram_memory_limit" { + block=$(app_container_block) + echo "$block" | grep -E 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory_limit[[:space:]]*\}\}Mi' >/dev/null +} + +@test "deployment template: application container requests.cpu still uses cpu_millicores" { + block=$(app_container_block) + echo "$block" | grep -E 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores[[:space:]]*\}\}m' >/dev/null +} + +@test "deployment template: application container requests.memory still uses ram_memory" { + block=$(app_container_block) + echo "$block" | grep -E 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory[[:space:]]*\}\}Mi' >/dev/null +} + +@test "deployment template: sidecars still use container_cpu_in_millicores / container_memory_in_memory" { + # Sidecars are everything BEFORE the application container block. + before=$(awk '/^[[:space:]]+- name: application[[:space:]]*$/ {exit} {print}' "$TEMPLATE") + echo "$before" | grep -F '{{ .container_cpu_in_millicores }}m' >/dev/null + echo "$before" | grep -F '{{ .container_memory_in_memory }}Mi' >/dev/null + # And sidecars must NOT have been switched to the new fields. + ! echo "$before" | grep -F 'cpu_millicores_limit' >/dev/null + ! echo "$before" | grep -F 'ram_memory_limit' >/dev/null +} +``` + +- [ ] **Step 2: Run the new tests, confirm they fail** + +Run: +```bash +bats k8s/deployment/tests/deployment_template_shape.bats +``` +Expected: at least the first two tests fail (limits.cpu / limits.memory still pointing at `cpu_millicores` / `ram_memory` — request fields). + +- [ ] **Step 3: Edit the application container's resource block** + +Open `k8s/deployment/templates/deployment.yaml.tpl`. Locate lines 313–319 (the `- name: application` container's `resources` block). Replace those exact lines with: + +```yaml + resources: + limits: + cpu: {{ .scope.capabilities.cpu_millicores_limit }}m + memory: {{ .scope.capabilities.ram_memory_limit }}Mi + requests: + cpu: {{ .scope.capabilities.cpu_millicores }}m + memory: {{ .scope.capabilities.ram_memory }}Mi +``` + +Do NOT touch the sidecar `resources:` blocks at lines 148–153, 201–206, or 255–260. + +- [ ] **Step 4: Run the template-shape tests, confirm they pass** + +Run: +```bash +bats k8s/deployment/tests/deployment_template_shape.bats +``` +Expected: all 5 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add k8s/deployment/templates/deployment.yaml.tpl k8s/deployment/tests/deployment_template_shape.bats +git commit -m "feat: render application container limits from normalized capability fields" +``` + +--- + +## Task 5: End-to-end smoke (manual) + +This is a sanity check, not a test — the project has no automated gomplate-render harness for `deployment.yaml.tpl`. Skip if `gomplate` is not installed locally. + +- [ ] **Step 1: Render the deployment template with a sample CONTEXT and inspect the output** + +```bash +cat > /tmp/clien781_ctx.json <<'JSON' +{ + "scope": { + "id": "scope-test", + "capabilities": { + "cpu_millicores": 500, + "cpu_millicores_limit": 1000, + "ram_memory": 1024, + "ram_memory_limit": 2048, + "health_check": {"enabled": true, "type": "HTTP", "path": "/health", "initial_delay_seconds": 10}, + "additional_ports": [] + } + }, + "deployment": {"id": "deploy-test"}, + "asset": {"url": "example.com/app:1.0"}, + "container_cpu_in_millicores": "93", + "container_memory_in_memory": "64", + "main_http_port": 8080, + "traffic_image": "example.com/traffic:1.0", + "blue_replicas": "0", + "green_replicas": "1", + "total_replicas": "1", + "blue_deployment_id": "", + "pull_secrets": [], + "pdb_enabled": "false", + "pdb_max_unavailable": "1", + "service_account_name": "default", + "traffic_manager_config_map": "tm-config", + "blue_additional_port_services": {} +} +JSON + +gomplate -c .=/tmp/clien781_ctx.json -f k8s/deployment/templates/deployment.yaml.tpl \ + | grep -A4 'name: application' \ + | grep -A3 'resources:' \ + | sed -n '1,8p' +``` + +Expected output should include: +``` + resources: + limits: + cpu: 1000m + memory: 2048Mi + requests: + cpu: 500m + memory: 1024Mi +``` + +- [ ] **Step 2: Render again with the limit fields omitted (back-compat case)** + +Edit `/tmp/clien781_ctx.json` and remove `cpu_millicores_limit` and `ram_memory_limit`. Then re-run the same `gomplate ... | grep` chain. + +**Wait** — gomplate will error on missing keys. This step illustrates that the back-compat path MUST go through `build_context` (which normalizes), not raw template rendering. The build pipeline always runs `build_context` first, so in production this is fine. The manual smoke here just confirms that the normalized context produces the right output; the "missing keys" path is covered by the BATS tests in Task 3. + +- [ ] **Step 3: Clean up** + +```bash +rm /tmp/clien781_ctx.json +``` + +--- + +## Task 6: Run the full k8s test suite and push the branch + +- [ ] **Step 1: Run all k8s BATS tests in batches** (per the project memory rule about avoiding BATS temp-dir collisions) + +Run: +```bash +bats k8s/deployment/tests/build_context.bats +bats k8s/deployment/tests/build_deployment.bats +bats k8s/deployment/tests/deployment_template_shape.bats +bats k8s/deployment/tests/ingress_template_shape.bats +bats k8s/deployment/tests/apply_templates.bats +``` +Expected: all green. + +- [ ] **Step 2: Confirm git status is clean and on the right branch** + +Run: +```bash +git status +git log --oneline beta..HEAD +``` +Expected: clean tree; four feature commits (Tasks 1–4) on top of beta. + +- [ ] **Step 3: Push the branch** + +Run: +```bash +git push -u origin feature/clien-781-memory-cpu-limits +``` + +- [ ] **Step 4: Run the quality-gate skill before opening a PR** + +Per the user's global `CLAUDE.md`, run `quality-gate` after non-trivial coding tasks and before claiming work is done. The skill orchestrates code-review, security audit, and simplification checks. + +--- + +## Out of scope (for follow-up tickets) + +- Docsite documentation for the new capabilities. +- CLI / OpenAPI changes — none required, the capability schema is consumed dynamically. +- Symmetric treatment for other resource dimensions (ephemeral storage, GPUs). +- Sidecar resource overrides — sidecars keep using `container_cpu_in_millicores` / `container_memory_in_memory` from the ConfigMap. + +--- + +## Self-review checklist (done by plan author) + +- [x] **Spec coverage:** every section of the spec (schema, uiSchema, render, back-compat, validation, testing) maps to a task. +- [x] **No placeholders:** every step has concrete code, paths, and expected output. +- [x] **Type consistency:** `normalize_capability_limits` is referenced consistently; field names match the schema (`cpu_millicores_limit`, `ram_memory_limit`). +- [x] **Scope:** single coherent change, one branch, one PR. From 3916ce83b77c4aadb316f6b9335812d90cad846d Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Thu, 21 May 2026 13:15:54 -0300 Subject: [PATCH 03/16] feat: add cpu_millicores_limit and ram_memory_limit properties to k8s scope spec --- k8s/specs/service-spec.json.tpl | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/k8s/specs/service-spec.json.tpl b/k8s/specs/service-spec.json.tpl index 656e641d..599de58d 100644 --- a/k8s/specs/service-spec.json.tpl +++ b/k8s/specs/service-spec.json.tpl @@ -356,6 +356,27 @@ "default":128, "description":"Amount of RAM memory to allocate to the container (in MB)" }, + "ram_memory_limit":{ + "type":["integer","null"], + "oneOf":[ + {"const":null, "title":"Same as request"}, + {"const":64, "title":"64 MB"}, + {"const":128, "title":"128 MB"}, + {"const":256, "title":"256 MB"}, + {"const":512, "title":"512 MB"}, + {"const":1024, "title":"1 GB"}, + {"const":2048, "title":"2 GB"}, + {"const":4096, "title":"4 GB"}, + {"const":8192, "title":"8 GB"}, + {"const":16384, "title":"16 GB"} + ], + "title":"RAM Memory Limit", + "default":null, + "minimum":{ + "$data":"1/ram_memory" + }, + "description":"Maximum memory the container can use (in MB). Setting this higher than the request increases the chance the scheduler kills the pod under pressure." + }, "visibility":{ "type":"string", "oneOf":[ @@ -490,6 +511,16 @@ "minimum":100, "description":"Amount of CPU to allocate (in millicores, 1000m = 1 CPU core)" }, + "cpu_millicores_limit":{ + "type":["integer","null"], + "title":"CPU Millicores Limit", + "default":null, + "maximum":4000, + "minimum":{ + "$data":"1/cpu_millicores" + }, + "description":"Maximum CPU the container can use (in millicores). Leave empty to use the same value as the request." + }, "scheduled_stop":{ "type":"object", "title":"Scheduled Stop", From 957debcbe4ef92ef256e43cef49947aa1ea52ce9 Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Thu, 21 May 2026 13:16:25 -0300 Subject: [PATCH 04/16] feat: rename Processor tab to Resources and surface CPU/RAM limit controls --- k8s/specs/service-spec.json.tpl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/k8s/specs/service-spec.json.tpl b/k8s/specs/service-spec.json.tpl index 599de58d..e75a7146 100644 --- a/k8s/specs/service-spec.json.tpl +++ b/k8s/specs/service-spec.json.tpl @@ -44,12 +44,22 @@ "elements":[ { "type":"Category", - "label":"Processor", + "label":"Resources", "elements":[ { "type":"Control", "label":"CPU Millicores", "scope":"#/properties/cpu_millicores" + }, + { + "type":"Control", + "label":"CPU Millicores Limit", + "scope":"#/properties/cpu_millicores_limit" + }, + { + "type":"Control", + "label":"RAM Memory Limit", + "scope":"#/properties/ram_memory_limit" } ] }, From 8bc5dac48372127751670d1f59367d00fdc11209 Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Thu, 21 May 2026 13:18:57 -0300 Subject: [PATCH 05/16] feat: normalize cpu/ram limit capabilities to request value when unset --- k8s/deployment/build_context | 11 +++++ k8s/deployment/tests/build_context.bats | 53 +++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/k8s/deployment/build_context b/k8s/deployment/build_context index 1f357deb..638cd5e6 100755 --- a/k8s/deployment/build_context +++ b/k8s/deployment/build_context @@ -23,6 +23,15 @@ MIN_REPLICAS=$(echo "$MIN_REPLICAS" | awk '{printf "%d", ($1 == int($1) ? $1 : i DEPLOYMENT_STATUS=$(echo "$CONTEXT" | jq -r ".deployment.status") +# Fill in *_limit capability fields with the corresponding request value when +# the limit is missing or explicitly null. Idempotent. CLIEN-781. +normalize_capability_limits() { + echo "$1" | jq ' + .scope.capabilities.cpu_millicores_limit = (.scope.capabilities.cpu_millicores_limit // .scope.capabilities.cpu_millicores) + | .scope.capabilities.ram_memory_limit = (.scope.capabilities.ram_memory_limit // .scope.capabilities.ram_memory) + ' +} + validate_status() { local action="$1" local status="$2" @@ -313,6 +322,8 @@ CONTEXT=$(echo "$CONTEXT" | jq \ main_http_port: ($main_http_port | tonumber) }') +CONTEXT=$(normalize_capability_limits "$CONTEXT") + DEPLOYMENT_ID=$(echo "$CONTEXT" | jq -r '.deployment.id') OUTPUT_DIR="$SERVICE_PATH/output/$SCOPE_ID-$DEPLOYMENT_ID" diff --git a/k8s/deployment/tests/build_context.bats b/k8s/deployment/tests/build_context.bats index 690a8ab4..020c2e84 100644 --- a/k8s/deployment/tests/build_context.bats +++ b/k8s/deployment/tests/build_context.bats @@ -946,3 +946,56 @@ set_additional_ports() { assert_equal "$(echo "$CONTEXT" | jq -c '.scope.capabilities.additional_ports')" "[]" } + +# ============================================================================= +# normalize_capability_limits Function Tests (CLIEN-781) +# Fills in *_limit with the corresponding request value when null or missing, +# leaves explicit values untouched. +# ============================================================================= + +setup_normalize_limits_fn() { + eval "$(sed -n '/^normalize_capability_limits()/,/^}/p' "$PROJECT_ROOT/k8s/deployment/build_context")" +} + +@test "normalize_capability_limits: fills CPU limit from request when limit is absent" { + setup_normalize_limits_fn + local in='{"scope":{"capabilities":{"cpu_millicores":500,"ram_memory":1024,"ram_memory_limit":2048}}}' + local out + out=$(normalize_capability_limits "$in") + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" +} + +@test "normalize_capability_limits: fills RAM limit from request when limit is absent" { + setup_normalize_limits_fn + local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":700,"ram_memory":1024}}}' + local out + out=$(normalize_capability_limits "$in") + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" +} + +@test "normalize_capability_limits: fills both limits when both are absent" { + setup_normalize_limits_fn + local in='{"scope":{"capabilities":{"cpu_millicores":500,"ram_memory":1024}}}' + local out + out=$(normalize_capability_limits "$in") + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" +} + +@test "normalize_capability_limits: fills both limits when both are explicit null" { + setup_normalize_limits_fn + local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":null,"ram_memory":1024,"ram_memory_limit":null}}}' + local out + out=$(normalize_capability_limits "$in") + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" +} + +@test "normalize_capability_limits: preserves explicit non-null limits" { + setup_normalize_limits_fn + local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":2000,"ram_memory":1024,"ram_memory_limit":4096}}}' + local out + out=$(normalize_capability_limits "$in") + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "2000" + assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "4096" +} From f50b59ca7851c36336a2c6b65bf927c4fc377c95 Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Thu, 21 May 2026 13:19:46 -0300 Subject: [PATCH 06/16] feat: render application container limits from normalized capability fields --- k8s/deployment/templates/deployment.yaml.tpl | 4 +- .../tests/deployment_template_shape.bats | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 k8s/deployment/tests/deployment_template_shape.bats diff --git a/k8s/deployment/templates/deployment.yaml.tpl b/k8s/deployment/templates/deployment.yaml.tpl index 3552c483..44f58d77 100644 --- a/k8s/deployment/templates/deployment.yaml.tpl +++ b/k8s/deployment/templates/deployment.yaml.tpl @@ -312,8 +312,8 @@ spec: {{ end }} resources: limits: - cpu: {{ .scope.capabilities.cpu_millicores }}m - memory: {{ .scope.capabilities.ram_memory }}Mi + cpu: {{ .scope.capabilities.cpu_millicores_limit }}m + memory: {{ .scope.capabilities.ram_memory_limit }}Mi requests: cpu: {{ .scope.capabilities.cpu_millicores }}m memory: {{ .scope.capabilities.ram_memory }}Mi diff --git a/k8s/deployment/tests/deployment_template_shape.bats b/k8s/deployment/tests/deployment_template_shape.bats new file mode 100644 index 00000000..f80db60f --- /dev/null +++ b/k8s/deployment/tests/deployment_template_shape.bats @@ -0,0 +1,53 @@ +#!/usr/bin/env bats +# ============================================================================= +# Structural tests for the deployment template. +# Verifies the application container's resources block uses the right +# capability for request vs limit. CLIEN-781. +# ============================================================================= + +setup() { + export PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" + source "$PROJECT_ROOT/testing/assertions.sh" + export TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/deployment.yaml.tpl" +} + +# Slice the file from "name: application" to the next container header, +# isolating the application container's block from the sidecars (which keep +# using container_cpu_in_millicores / container_memory_in_memory). +app_container_block() { + awk ' + /^[[:space:]]+- name: application[[:space:]]*$/ { in_app=1 } + in_app { print } + /^[[:space:]]+terminationMessagePolicy:/ && in_app { exit } + ' "$TEMPLATE" +} + +@test "deployment template: application container limits.cpu uses cpu_millicores_limit" { + block=$(app_container_block) + echo "$block" | grep -E 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores_limit[[:space:]]*\}\}m' >/dev/null +} + +@test "deployment template: application container limits.memory uses ram_memory_limit" { + block=$(app_container_block) + echo "$block" | grep -E 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory_limit[[:space:]]*\}\}Mi' >/dev/null +} + +@test "deployment template: application container requests.cpu still uses cpu_millicores" { + block=$(app_container_block) + echo "$block" | grep -E 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores[[:space:]]*\}\}m' >/dev/null +} + +@test "deployment template: application container requests.memory still uses ram_memory" { + block=$(app_container_block) + echo "$block" | grep -E 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory[[:space:]]*\}\}Mi' >/dev/null +} + +@test "deployment template: sidecars still use container_cpu_in_millicores / container_memory_in_memory" { + # Sidecars are everything BEFORE the application container block. + before=$(awk '/^[[:space:]]+- name: application[[:space:]]*$/ {exit} {print}' "$TEMPLATE") + echo "$before" | grep -F '{{ .container_cpu_in_millicores }}m' >/dev/null + echo "$before" | grep -F '{{ .container_memory_in_memory }}Mi' >/dev/null + # And sidecars must NOT have been switched to the new fields. + ! echo "$before" | grep -F 'cpu_millicores_limit' >/dev/null + ! echo "$before" | grep -F 'ram_memory_limit' >/dev/null +} From a40f54ab3c40d0769ed96625b9884df565333ddc Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Thu, 21 May 2026 13:26:30 -0300 Subject: [PATCH 07/16] refactor: tighten normalize_capability_limits jq + bats here-string idioms --- k8s/deployment/build_context | 8 ++--- .../tests/deployment_template_shape.bats | 35 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/k8s/deployment/build_context b/k8s/deployment/build_context index 638cd5e6..c89c7928 100755 --- a/k8s/deployment/build_context +++ b/k8s/deployment/build_context @@ -26,10 +26,10 @@ DEPLOYMENT_STATUS=$(echo "$CONTEXT" | jq -r ".deployment.status") # Fill in *_limit capability fields with the corresponding request value when # the limit is missing or explicitly null. Idempotent. CLIEN-781. normalize_capability_limits() { - echo "$1" | jq ' - .scope.capabilities.cpu_millicores_limit = (.scope.capabilities.cpu_millicores_limit // .scope.capabilities.cpu_millicores) - | .scope.capabilities.ram_memory_limit = (.scope.capabilities.ram_memory_limit // .scope.capabilities.ram_memory) - ' + echo "$1" | jq ' + .scope.capabilities.cpu_millicores_limit //= .scope.capabilities.cpu_millicores + | .scope.capabilities.ram_memory_limit //= .scope.capabilities.ram_memory + ' } validate_status() { diff --git a/k8s/deployment/tests/deployment_template_shape.bats b/k8s/deployment/tests/deployment_template_shape.bats index f80db60f..6f44ffba 100644 --- a/k8s/deployment/tests/deployment_template_shape.bats +++ b/k8s/deployment/tests/deployment_template_shape.bats @@ -11,9 +11,9 @@ setup() { export TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/deployment.yaml.tpl" } -# Slice the file from "name: application" to the next container header, -# isolating the application container's block from the sidecars (which keep -# using container_cpu_in_millicores / container_memory_in_memory). +# Slice the file from "name: application" up to the application container's +# terminationMessagePolicy, isolating it from the sidecars (which keep using +# container_cpu_in_millicores / container_memory_in_memory). app_container_block() { awk ' /^[[:space:]]+- name: application[[:space:]]*$/ { in_app=1 } @@ -22,32 +22,33 @@ app_container_block() { ' "$TEMPLATE" } +# Everything BEFORE the application container — the sidecar definitions. +sidecars_block() { + awk '/^[[:space:]]+- name: application[[:space:]]*$/ {exit} {print}' "$TEMPLATE" +} + @test "deployment template: application container limits.cpu uses cpu_millicores_limit" { - block=$(app_container_block) - echo "$block" | grep -E 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores_limit[[:space:]]*\}\}m' >/dev/null + grep -qE 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores_limit[[:space:]]*\}\}m' <<<"$(app_container_block)" } @test "deployment template: application container limits.memory uses ram_memory_limit" { - block=$(app_container_block) - echo "$block" | grep -E 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory_limit[[:space:]]*\}\}Mi' >/dev/null + grep -qE 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory_limit[[:space:]]*\}\}Mi' <<<"$(app_container_block)" } @test "deployment template: application container requests.cpu still uses cpu_millicores" { - block=$(app_container_block) - echo "$block" | grep -E 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores[[:space:]]*\}\}m' >/dev/null + grep -qE 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores[[:space:]]*\}\}m' <<<"$(app_container_block)" } @test "deployment template: application container requests.memory still uses ram_memory" { - block=$(app_container_block) - echo "$block" | grep -E 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory[[:space:]]*\}\}Mi' >/dev/null + grep -qE 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory[[:space:]]*\}\}Mi' <<<"$(app_container_block)" } @test "deployment template: sidecars still use container_cpu_in_millicores / container_memory_in_memory" { - # Sidecars are everything BEFORE the application container block. - before=$(awk '/^[[:space:]]+- name: application[[:space:]]*$/ {exit} {print}' "$TEMPLATE") - echo "$before" | grep -F '{{ .container_cpu_in_millicores }}m' >/dev/null - echo "$before" | grep -F '{{ .container_memory_in_memory }}Mi' >/dev/null + local sidecars + sidecars=$(sidecars_block) + grep -qF '{{ .container_cpu_in_millicores }}m' <<<"$sidecars" + grep -qF '{{ .container_memory_in_memory }}Mi' <<<"$sidecars" # And sidecars must NOT have been switched to the new fields. - ! echo "$before" | grep -F 'cpu_millicores_limit' >/dev/null - ! echo "$before" | grep -F 'ram_memory_limit' >/dev/null + ! grep -qF 'cpu_millicores_limit' <<<"$sidecars" + ! grep -qF 'ram_memory_limit' <<<"$sidecars" } From 3eff675c103a3132ee632810c2a4916d4a3c75b9 Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Thu, 21 May 2026 13:28:51 -0300 Subject: [PATCH 08/16] fix: mark cpu_millicores_limit and ram_memory_limit as required for UI visibility --- .../specs/2026-05-21-clien-781-memory-cpu-limits-design.md | 2 +- k8s/specs/service-spec.json.tpl | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md b/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md index c5cbaa0c..6aef352f 100644 --- a/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md +++ b/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md @@ -88,7 +88,7 @@ Asymmetry between RAM and CPU is intentional: RAM request stays in the main form } ``` -Neither property is added to the `required` array of `attributes.schema` — both are optional. +Both properties are added to the `required` array of `attributes.schema`. This is the nullplatform UI's contract: the frontend only renders properties that appear in `required` (established during CLIEN-739). Defaults of `null` keep this non-breaking — existing scopes materialize the default, and `normalize_capability_limits` collapses `null` back to the request value before the deployment template renders. ### uiSchema changes diff --git a/k8s/specs/service-spec.json.tpl b/k8s/specs/service-spec.json.tpl index e75a7146..f9742bcf 100644 --- a/k8s/specs/service-spec.json.tpl +++ b/k8s/specs/service-spec.json.tpl @@ -5,11 +5,13 @@ "type":"object", "required":[ "ram_memory", + "ram_memory_limit", "visibility", "autoscaling", "health_check", "scaling_type", "cpu_millicores", + "cpu_millicores_limit", "fixed_instances", "scheduled_stop", "additional_ports", From 6856c9cbfc807453ea602656e6bdffe517629e1b Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Thu, 21 May 2026 14:35:18 -0300 Subject: [PATCH 09/16] refactor: make cpu_millicores_limit a dropdown with 'Same as request' option --- .../2026-05-21-clien-781-memory-cpu-limits-design.md | 12 ++++++++++-- k8s/deployment/tests/build_context.bats | 2 +- k8s/specs/service-spec.json.tpl | 12 ++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md b/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md index 6aef352f..24741172 100644 --- a/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md +++ b/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md @@ -63,9 +63,17 @@ Asymmetry between RAM and CPU is intentional: RAM request stays in the main form "type": ["integer", "null"], "title": "CPU Millicores Limit", "default": null, - "maximum": 4000, + "oneOf": [ + { "const": null, "title": "Same as request" }, + { "const": 100, "title": "100 m" }, + { "const": 250, "title": "250 m" }, + { "const": 500, "title": "500 m" }, + { "const": 1000, "title": "1000 m" }, + { "const": 2000, "title": "2000 m" }, + { "const": 4000, "title": "4000 m" } + ], "minimum": { "$data": "1/cpu_millicores" }, - "description": "Maximum CPU the container can use. Leave empty to use the same value as the request." + "description": "Maximum CPU the container can use (in millicores). Pick 'Same as request' to leave it equal to the request value." }, "ram_memory_limit": { "type": ["integer", "null"], diff --git a/k8s/deployment/tests/build_context.bats b/k8s/deployment/tests/build_context.bats index 020c2e84..0f035d6c 100644 --- a/k8s/deployment/tests/build_context.bats +++ b/k8s/deployment/tests/build_context.bats @@ -967,7 +967,7 @@ setup_normalize_limits_fn() { @test "normalize_capability_limits: fills RAM limit from request when limit is absent" { setup_normalize_limits_fn - local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":700,"ram_memory":1024}}}' + local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":1000,"ram_memory":1024}}}' local out out=$(normalize_capability_limits "$in") assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" diff --git a/k8s/specs/service-spec.json.tpl b/k8s/specs/service-spec.json.tpl index f9742bcf..3032936f 100644 --- a/k8s/specs/service-spec.json.tpl +++ b/k8s/specs/service-spec.json.tpl @@ -525,13 +525,21 @@ }, "cpu_millicores_limit":{ "type":["integer","null"], + "oneOf":[ + {"const":null, "title":"Same as request"}, + {"const":100, "title":"100 m"}, + {"const":250, "title":"250 m"}, + {"const":500, "title":"500 m"}, + {"const":1000, "title":"1000 m"}, + {"const":2000, "title":"2000 m"}, + {"const":4000, "title":"4000 m"} + ], "title":"CPU Millicores Limit", "default":null, - "maximum":4000, "minimum":{ "$data":"1/cpu_millicores" }, - "description":"Maximum CPU the container can use (in millicores). Leave empty to use the same value as the request." + "description":"Maximum CPU the container can use (in millicores). Pick 'Same as request' to leave it equal to the request value." }, "scheduled_stop":{ "type":"object", From d08ca797efb6bb133fa1b732bb9f25c44aebcede Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Fri, 22 May 2026 13:25:07 -0300 Subject: [PATCH 10/16] docs: align ram_memory_limit description with cpu_millicores_limit phrasing --- k8s/specs/service-spec.json.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/specs/service-spec.json.tpl b/k8s/specs/service-spec.json.tpl index 3032936f..f2cd6507 100644 --- a/k8s/specs/service-spec.json.tpl +++ b/k8s/specs/service-spec.json.tpl @@ -387,7 +387,7 @@ "minimum":{ "$data":"1/ram_memory" }, - "description":"Maximum memory the container can use (in MB). Setting this higher than the request increases the chance the scheduler kills the pod under pressure." + "description":"Maximum memory the container can use (in MB). Pick 'Same as request' to leave it equal to the request value." }, "visibility":{ "type":"string", From 88f65ee73bfaf61c8bee4ca690dba8daafa29ffd Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Fri, 22 May 2026 13:25:08 -0300 Subject: [PATCH 11/16] chore: move design spec and plan to .claude (untracked working notes) --- .../2026-05-21-clien-781-memory-cpu-limits.md | 525 ------------------ ...5-21-clien-781-memory-cpu-limits-design.md | 171 ------ 2 files changed, 696 deletions(-) delete mode 100644 docs/superpowers/plans/2026-05-21-clien-781-memory-cpu-limits.md delete mode 100644 docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md diff --git a/docs/superpowers/plans/2026-05-21-clien-781-memory-cpu-limits.md b/docs/superpowers/plans/2026-05-21-clien-781-memory-cpu-limits.md deleted file mode 100644 index f954d73b..00000000 --- a/docs/superpowers/plans/2026-05-21-clien-781-memory-cpu-limits.md +++ /dev/null @@ -1,525 +0,0 @@ -# CLIEN-781 — Memory & CPU Limits Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add optional `cpu_millicores_limit` and `ram_memory_limit` capabilities to the k8s scope so the Spin client can set Kubernetes `resources.limits` independently from `resources.requests`, with safe back-compat defaults. - -**Architecture:** Add two new optional properties to the k8s scope spec. Normalize them inside `build_context` (limit defaults to request when null/missing) so the deployment template stays trivial. Render the normalized values into the application container's `resources.limits` while keeping `resources.requests` bound to the original `cpu_millicores` / `ram_memory` fields. - -**Tech Stack:** JSON Schema (with JSONForms uiSchema), bash + jq for context normalization, gomplate for template rendering, BATS for tests. - -**Spec:** [`docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md`](../specs/2026-05-21-clien-781-memory-cpu-limits-design.md) - ---- - -## File Structure - -**Modified files:** - -- `k8s/specs/service-spec.json.tpl` — add two `properties` and update the `Categorization`/`Category` to rename "Processor" → "Resources" and add two new `Control` entries. -- `k8s/deployment/build_context` — add a `normalize_capability_limits` function that mutates `$CONTEXT` to fill `.scope.capabilities.cpu_millicores_limit` and `.scope.capabilities.ram_memory_limit` with the request value when null/missing. Call it before the final context write. -- `k8s/deployment/templates/deployment.yaml.tpl` — application container only (lines 313–319): keep `requests.cpu/memory` bound to `cpu_millicores` / `ram_memory`, change `limits.cpu/memory` to read `cpu_millicores_limit` / `ram_memory_limit`. Sidecars (lines 148–153, 201–206, 255–260) are NOT touched — they use `container_cpu_in_millicores` / `container_memory_in_memory` from a ConfigMap. - -**New tests:** - -- `k8s/deployment/tests/build_context.bats` — add a section for `normalize_capability_limits` covering the four matrix cells (limit set / limit null, for CPU and RAM) plus the "field absent" case. -- `k8s/deployment/tests/deployment_template_shape.bats` (new file) — grep-based structural assertions that the application container `resources` block uses the right field for request vs limit. Mirrors `tests/ingress_template_shape.bats`. - -**Not modified:** sidecar resource blocks, CLI, docsite, API spec. - ---- - -## Task 1: Add `cpu_millicores_limit` and `ram_memory_limit` properties to the JSON schema - -**Files:** -- Modify: `k8s/specs/service-spec.json.tpl` (properties block, lines 485–492 area for CPU; lines 315–358 area for RAM) - -There is no JSON-schema test harness in this repo, so this task has no automated test. The schema is validated implicitly by the deployment workflow and by manual `jq` sanity checks in step 2. - -- [ ] **Step 1: Add the two new properties to `attributes.schema.properties`** - -After the existing `cpu_millicores` property block (end at line 492), add `cpu_millicores_limit`: - -```json -, -"cpu_millicores_limit":{ - "type":["integer","null"], - "title":"CPU Millicores Limit", - "default":null, - "maximum":4000, - "minimum":{ - "$data":"1/cpu_millicores" - }, - "description":"Maximum CPU the container can use (in millicores). Leave empty to use the same value as the request." -} -``` - -After the existing `ram_memory` property block (end at line 358), add `ram_memory_limit`: - -```json -, -"ram_memory_limit":{ - "type":["integer","null"], - "oneOf":[ - {"const":null, "title":"Same as request"}, - {"const":64, "title":"64 MB"}, - {"const":128, "title":"128 MB"}, - {"const":256, "title":"256 MB"}, - {"const":512, "title":"512 MB"}, - {"const":1024, "title":"1 GB"}, - {"const":2048, "title":"2 GB"}, - {"const":4096, "title":"4 GB"}, - {"const":8192, "title":"8 GB"}, - {"const":16384, "title":"16 GB"} - ], - "title":"RAM Memory Limit", - "default":null, - "minimum":{ - "$data":"1/ram_memory" - }, - "description":"Maximum memory the container can use (in MB). Setting this higher than the request increases the chance the scheduler kills the pod under pressure." -} -``` - -Do NOT add either field to the top-level `required` array — both stay optional. - -- [ ] **Step 2: Validate the JSON is still well-formed** - -Run: -```bash -jq empty k8s/specs/service-spec.json.tpl -``` -Expected: no output, exit code 0. - -If gomplate is available locally, also confirm the template renders to valid JSON: -```bash -NRN="nrn:test" gomplate -f k8s/specs/service-spec.json.tpl | jq empty -``` -Expected: no output, exit code 0. - -- [ ] **Step 3: Commit** - -```bash -git add k8s/specs/service-spec.json.tpl -git commit -m "feat: add cpu_millicores_limit and ram_memory_limit properties to k8s scope spec" -``` - ---- - -## Task 2: Rename "Processor" → "Resources" and add the limit Controls to the uiSchema - -**Files:** -- Modify: `k8s/specs/service-spec.json.tpl` (uiSchema `Category` block, lines 46–55) - -No automated test — uiSchema is rendered by the frontend. We validate by grep-based assertion in step 2 and visual smoke later. - -- [ ] **Step 1: Rename the Category label and add two Controls** - -Locate the `Category` whose label is `"Processor"` (line 47). Replace the whole block (lines 46–55) with: - -```json -{ - "type":"Category", - "label":"Resources", - "elements":[ - { - "type":"Control", - "label":"CPU Millicores", - "scope":"#/properties/cpu_millicores" - }, - { - "type":"Control", - "label":"CPU Millicores Limit", - "scope":"#/properties/cpu_millicores_limit" - }, - { - "type":"Control", - "label":"RAM Memory Limit", - "scope":"#/properties/ram_memory_limit" - } - ] -} -``` - -- [ ] **Step 2: Sanity-check the uiSchema is well-formed and has the expected shape** - -Run: -```bash -jq -e ' - .attributes.schema.uiSchema - | .. | objects | select(.label? == "Resources") - | .elements | map(.scope) as $scopes - | ($scopes | length) == 3 - and ($scopes | index("#/properties/cpu_millicores") != null) - and ($scopes | index("#/properties/cpu_millicores_limit") != null) - and ($scopes | index("#/properties/ram_memory_limit") != null) -' k8s/specs/service-spec.json.tpl >/dev/null && echo OK -``` -Expected: `OK`. - -Also confirm "Processor" is gone: -```bash -! grep -q '"Processor"' k8s/specs/service-spec.json.tpl && echo OK -``` -Expected: `OK`. - -- [ ] **Step 3: Commit** - -```bash -git add k8s/specs/service-spec.json.tpl -git commit -m "feat: rename Processor tab to Resources and surface CPU/RAM limit controls" -``` - ---- - -## Task 3: Add `normalize_capability_limits` to `build_context` (TDD) - -**Files:** -- Modify: `k8s/deployment/build_context` -- Modify: `k8s/deployment/tests/build_context.bats` - -This is the back-compat heart of the change. The function takes `$CONTEXT` (JSON) and fills `.scope.capabilities.cpu_millicores_limit` and `.scope.capabilities.ram_memory_limit` with the corresponding request value when the field is `null` or missing. Existing values pass through unchanged. - -- [ ] **Step 1: Write failing tests in `tests/build_context.bats`** - -Append at the end of `k8s/deployment/tests/build_context.bats`: - -```bash -# ============================================================================= -# normalize_capability_limits Function Tests (CLIEN-781) -# Fills in *_limit with the corresponding request value when null or missing, -# leaves explicit values untouched. -# ============================================================================= - -setup_normalize_limits_fn() { - eval "$(sed -n '/^normalize_capability_limits()/,/^}/p' "$PROJECT_ROOT/k8s/deployment/build_context")" -} - -@test "normalize_capability_limits: fills CPU limit from request when limit is absent" { - setup_normalize_limits_fn - local in='{"scope":{"capabilities":{"cpu_millicores":500,"ram_memory":1024,"ram_memory_limit":2048}}}' - local out - out=$(normalize_capability_limits "$in") - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" -} - -@test "normalize_capability_limits: fills RAM limit from request when limit is absent" { - setup_normalize_limits_fn - local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":700,"ram_memory":1024}}}' - local out - out=$(normalize_capability_limits "$in") - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" -} - -@test "normalize_capability_limits: fills both limits when both are absent" { - setup_normalize_limits_fn - local in='{"scope":{"capabilities":{"cpu_millicores":500,"ram_memory":1024}}}' - local out - out=$(normalize_capability_limits "$in") - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" -} - -@test "normalize_capability_limits: fills both limits when both are explicit null" { - setup_normalize_limits_fn - local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":null,"ram_memory":1024,"ram_memory_limit":null}}}' - local out - out=$(normalize_capability_limits "$in") - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" -} - -@test "normalize_capability_limits: preserves explicit non-null limits" { - setup_normalize_limits_fn - local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":2000,"ram_memory":1024,"ram_memory_limit":4096}}}' - local out - out=$(normalize_capability_limits "$in") - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "2000" - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "4096" -} -``` - -- [ ] **Step 2: Run the new tests, confirm they fail** - -Run: -```bash -bats k8s/deployment/tests/build_context.bats -f normalize_capability_limits -``` -Expected: 5 failures, message about `normalize_capability_limits: command not found` (or similar — function does not exist yet). - -- [ ] **Step 3: Implement `normalize_capability_limits` in `build_context`** - -Open `k8s/deployment/build_context`. Above the `validate_status()` function (search for `^validate_status\(\)`), insert: - -```bash -# Fill in *_limit capability fields with the corresponding request value when -# the limit is missing or explicitly null. Idempotent. CLIEN-781. -normalize_capability_limits() { - echo "$1" | jq ' - .scope.capabilities.cpu_millicores_limit = (.scope.capabilities.cpu_millicores_limit // .scope.capabilities.cpu_millicores) - | .scope.capabilities.ram_memory_limit = (.scope.capabilities.ram_memory_limit // .scope.capabilities.ram_memory) - ' -} -``` - -Then wire it into the final context assembly. Find the block ending at line 314 (the big `jq '. + { ... }')` invocation around lines 285–314 that produces the final `$CONTEXT`). Immediately after that block (i.e., right before the `DEPLOYMENT_ID=$(echo "$CONTEXT" | jq -r '.deployment.id')` line at 316), add: - -```bash -CONTEXT=$(normalize_capability_limits "$CONTEXT") -``` - -- [ ] **Step 4: Run the new tests, confirm they pass** - -Run: -```bash -bats k8s/deployment/tests/build_context.bats -f normalize_capability_limits -``` -Expected: 5 tests pass. - -- [ ] **Step 5: Run the full build_context test suite to ensure no regressions** - -Run: -```bash -bats k8s/deployment/tests/build_context.bats -``` -Expected: all tests pass (baseline of this file is currently green per the existing CI; we are only adding tests). - -- [ ] **Step 6: Commit** - -```bash -git add k8s/deployment/build_context k8s/deployment/tests/build_context.bats -git commit -m "feat: normalize cpu/ram limit capabilities to request value when unset" -``` - ---- - -## Task 4: Render limits from normalized fields in the application container (TDD via template-shape test) - -**Files:** -- Create: `k8s/deployment/tests/deployment_template_shape.bats` -- Modify: `k8s/deployment/templates/deployment.yaml.tpl` (lines 313–319 only — the application container, NOT the sidecars) - -We assert the template shape with grep (same approach as `ingress_template_shape.bats`). End-to-end rendering through gomplate is exercised by the existing build pipeline; the shape test catches regressions like accidentally rebinding `limits.cpu` back to `cpu_millicores`. - -- [ ] **Step 1: Write the failing template-shape test** - -Create `k8s/deployment/tests/deployment_template_shape.bats`: - -```bash -#!/usr/bin/env bats -# ============================================================================= -# Structural tests for the deployment template. -# Verifies the application container's resources block uses the right -# capability for request vs limit. CLIEN-781. -# ============================================================================= - -setup() { - export PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" - source "$PROJECT_ROOT/testing/assertions.sh" - export TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/deployment.yaml.tpl" -} - -# Slice the file from "name: application" to the next container header, -# isolating the application container's block from the sidecars (which keep -# using container_cpu_in_millicores / container_memory_in_memory). -app_container_block() { - awk ' - /^[[:space:]]+- name: application[[:space:]]*$/ { in_app=1 } - in_app { print } - /^[[:space:]]+terminationMessagePolicy:/ && in_app { exit } - ' "$TEMPLATE" -} - -@test "deployment template: application container limits.cpu uses cpu_millicores_limit" { - block=$(app_container_block) - echo "$block" | grep -E 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores_limit[[:space:]]*\}\}m' >/dev/null -} - -@test "deployment template: application container limits.memory uses ram_memory_limit" { - block=$(app_container_block) - echo "$block" | grep -E 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory_limit[[:space:]]*\}\}Mi' >/dev/null -} - -@test "deployment template: application container requests.cpu still uses cpu_millicores" { - block=$(app_container_block) - echo "$block" | grep -E 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores[[:space:]]*\}\}m' >/dev/null -} - -@test "deployment template: application container requests.memory still uses ram_memory" { - block=$(app_container_block) - echo "$block" | grep -E 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory[[:space:]]*\}\}Mi' >/dev/null -} - -@test "deployment template: sidecars still use container_cpu_in_millicores / container_memory_in_memory" { - # Sidecars are everything BEFORE the application container block. - before=$(awk '/^[[:space:]]+- name: application[[:space:]]*$/ {exit} {print}' "$TEMPLATE") - echo "$before" | grep -F '{{ .container_cpu_in_millicores }}m' >/dev/null - echo "$before" | grep -F '{{ .container_memory_in_memory }}Mi' >/dev/null - # And sidecars must NOT have been switched to the new fields. - ! echo "$before" | grep -F 'cpu_millicores_limit' >/dev/null - ! echo "$before" | grep -F 'ram_memory_limit' >/dev/null -} -``` - -- [ ] **Step 2: Run the new tests, confirm they fail** - -Run: -```bash -bats k8s/deployment/tests/deployment_template_shape.bats -``` -Expected: at least the first two tests fail (limits.cpu / limits.memory still pointing at `cpu_millicores` / `ram_memory` — request fields). - -- [ ] **Step 3: Edit the application container's resource block** - -Open `k8s/deployment/templates/deployment.yaml.tpl`. Locate lines 313–319 (the `- name: application` container's `resources` block). Replace those exact lines with: - -```yaml - resources: - limits: - cpu: {{ .scope.capabilities.cpu_millicores_limit }}m - memory: {{ .scope.capabilities.ram_memory_limit }}Mi - requests: - cpu: {{ .scope.capabilities.cpu_millicores }}m - memory: {{ .scope.capabilities.ram_memory }}Mi -``` - -Do NOT touch the sidecar `resources:` blocks at lines 148–153, 201–206, or 255–260. - -- [ ] **Step 4: Run the template-shape tests, confirm they pass** - -Run: -```bash -bats k8s/deployment/tests/deployment_template_shape.bats -``` -Expected: all 5 tests pass. - -- [ ] **Step 5: Commit** - -```bash -git add k8s/deployment/templates/deployment.yaml.tpl k8s/deployment/tests/deployment_template_shape.bats -git commit -m "feat: render application container limits from normalized capability fields" -``` - ---- - -## Task 5: End-to-end smoke (manual) - -This is a sanity check, not a test — the project has no automated gomplate-render harness for `deployment.yaml.tpl`. Skip if `gomplate` is not installed locally. - -- [ ] **Step 1: Render the deployment template with a sample CONTEXT and inspect the output** - -```bash -cat > /tmp/clien781_ctx.json <<'JSON' -{ - "scope": { - "id": "scope-test", - "capabilities": { - "cpu_millicores": 500, - "cpu_millicores_limit": 1000, - "ram_memory": 1024, - "ram_memory_limit": 2048, - "health_check": {"enabled": true, "type": "HTTP", "path": "/health", "initial_delay_seconds": 10}, - "additional_ports": [] - } - }, - "deployment": {"id": "deploy-test"}, - "asset": {"url": "example.com/app:1.0"}, - "container_cpu_in_millicores": "93", - "container_memory_in_memory": "64", - "main_http_port": 8080, - "traffic_image": "example.com/traffic:1.0", - "blue_replicas": "0", - "green_replicas": "1", - "total_replicas": "1", - "blue_deployment_id": "", - "pull_secrets": [], - "pdb_enabled": "false", - "pdb_max_unavailable": "1", - "service_account_name": "default", - "traffic_manager_config_map": "tm-config", - "blue_additional_port_services": {} -} -JSON - -gomplate -c .=/tmp/clien781_ctx.json -f k8s/deployment/templates/deployment.yaml.tpl \ - | grep -A4 'name: application' \ - | grep -A3 'resources:' \ - | sed -n '1,8p' -``` - -Expected output should include: -``` - resources: - limits: - cpu: 1000m - memory: 2048Mi - requests: - cpu: 500m - memory: 1024Mi -``` - -- [ ] **Step 2: Render again with the limit fields omitted (back-compat case)** - -Edit `/tmp/clien781_ctx.json` and remove `cpu_millicores_limit` and `ram_memory_limit`. Then re-run the same `gomplate ... | grep` chain. - -**Wait** — gomplate will error on missing keys. This step illustrates that the back-compat path MUST go through `build_context` (which normalizes), not raw template rendering. The build pipeline always runs `build_context` first, so in production this is fine. The manual smoke here just confirms that the normalized context produces the right output; the "missing keys" path is covered by the BATS tests in Task 3. - -- [ ] **Step 3: Clean up** - -```bash -rm /tmp/clien781_ctx.json -``` - ---- - -## Task 6: Run the full k8s test suite and push the branch - -- [ ] **Step 1: Run all k8s BATS tests in batches** (per the project memory rule about avoiding BATS temp-dir collisions) - -Run: -```bash -bats k8s/deployment/tests/build_context.bats -bats k8s/deployment/tests/build_deployment.bats -bats k8s/deployment/tests/deployment_template_shape.bats -bats k8s/deployment/tests/ingress_template_shape.bats -bats k8s/deployment/tests/apply_templates.bats -``` -Expected: all green. - -- [ ] **Step 2: Confirm git status is clean and on the right branch** - -Run: -```bash -git status -git log --oneline beta..HEAD -``` -Expected: clean tree; four feature commits (Tasks 1–4) on top of beta. - -- [ ] **Step 3: Push the branch** - -Run: -```bash -git push -u origin feature/clien-781-memory-cpu-limits -``` - -- [ ] **Step 4: Run the quality-gate skill before opening a PR** - -Per the user's global `CLAUDE.md`, run `quality-gate` after non-trivial coding tasks and before claiming work is done. The skill orchestrates code-review, security audit, and simplification checks. - ---- - -## Out of scope (for follow-up tickets) - -- Docsite documentation for the new capabilities. -- CLI / OpenAPI changes — none required, the capability schema is consumed dynamically. -- Symmetric treatment for other resource dimensions (ephemeral storage, GPUs). -- Sidecar resource overrides — sidecars keep using `container_cpu_in_millicores` / `container_memory_in_memory` from the ConfigMap. - ---- - -## Self-review checklist (done by plan author) - -- [x] **Spec coverage:** every section of the spec (schema, uiSchema, render, back-compat, validation, testing) maps to a task. -- [x] **No placeholders:** every step has concrete code, paths, and expected output. -- [x] **Type consistency:** `normalize_capability_limits` is referenced consistently; field names match the schema (`cpu_millicores_limit`, `ram_memory_limit`). -- [x] **Scope:** single coherent change, one branch, one PR. diff --git a/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md b/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md deleted file mode 100644 index 24741172..00000000 --- a/docs/superpowers/specs/2026-05-21-clien-781-memory-cpu-limits-design.md +++ /dev/null @@ -1,171 +0,0 @@ -# CLIEN-781 — Configurable CPU & RAM limits for k8s scope - -Status: design approved (2026-05-21) -Ticket: https://nullplatform.atlassian.net/browse/CLIEN-781 -Client: Spin -Assignee: Federico Maleh - -## Context - -Today the k8s scope exposes two capabilities — `ram_memory` and `cpu_millicores` — that are used as **both** the Kubernetes request and the Kubernetes limit. The Spin team needs to decouple them so that limits can be set higher than requests when desired, while keeping the default behavior unchanged for existing scopes. - -The risk that drives the UI shape: a memory `limit > request` increases the chance the scheduler/OOMKiller kills a pod under pressure. So memory limit is a sharp tool that should be hidden behind an "advanced" surface, not the main form. - -## Goals - -1. Add `cpu_millicores_limit` and `ram_memory_limit` as optional capabilities. -2. Keep the main form intact — `ram_memory` (request) stays at the top, untouched. -3. Group the new fields with the existing `cpu_millicores` in a renamed `Resources` tab inside the collapsable "ADVANCED" categorization. -4. Validate `limit >= request` at the JSON schema layer. -5. Be backwards compatible: missing or null limit ⇒ fall back to the request value, matching today's render. - -## Non-goals - -- No change to `ram_memory` or `cpu_millicores` themselves (same field types, same defaults). -- No cross-scope validation. -- No docsite update in this ticket (separate PR if requested). -- No CLI/API change beyond what naturally happens by adding properties to the scope spec. - -## UI design - -### Form layout (after the change) - -``` -Main form -├─ RAM Memory (request, dropdown — unchanged) -└─ Visibility - -▼ ADVANCED -├─ Resources ← renamed from "Processor" -│ ├─ CPU Millicores (request — existing) -│ ├─ CPU Millicores Limit ← NEW (optional integer) -│ └─ RAM Memory Limit ← NEW (dropdown with "Same as request") -├─ Size & Scaling -├─ Exposed Ports -├─ Scheduled Stop -├─ Protocol -├─ Continuous deployment -└─ Health Check -``` - -Asymmetry between RAM and CPU is intentional: RAM request stays in the main form (everyone tunes it), RAM limit lives in `Resources` (sharp tool). CPU request and CPU limit both live in `Resources` (CPU was already advanced). - -### Tab rename rationale - -`Resources` follows Kubernetes vocabulary (`resources: requests/limits`) and is generic enough to host both CPU and memory tuning. Alternatives considered (`Compute`, `Compute & Limits`) were rejected as less standard. - -## Schema changes — `k8s/specs/service-spec.json.tpl` - -### New properties (siblings of the existing ones) - -```json -"cpu_millicores_limit": { - "type": ["integer", "null"], - "title": "CPU Millicores Limit", - "default": null, - "oneOf": [ - { "const": null, "title": "Same as request" }, - { "const": 100, "title": "100 m" }, - { "const": 250, "title": "250 m" }, - { "const": 500, "title": "500 m" }, - { "const": 1000, "title": "1000 m" }, - { "const": 2000, "title": "2000 m" }, - { "const": 4000, "title": "4000 m" } - ], - "minimum": { "$data": "1/cpu_millicores" }, - "description": "Maximum CPU the container can use (in millicores). Pick 'Same as request' to leave it equal to the request value." -}, -"ram_memory_limit": { - "type": ["integer", "null"], - "title": "RAM Memory Limit", - "default": null, - "oneOf": [ - { "const": null, "title": "Same as request" }, - { "const": 64, "title": "64 MB" }, - { "const": 128, "title": "128 MB" }, - { "const": 256, "title": "256 MB" }, - { "const": 512, "title": "512 MB" }, - { "const": 1024, "title": "1 GB" }, - { "const": 2048, "title": "2 GB" }, - { "const": 4096, "title": "4 GB" }, - { "const": 8192, "title": "8 GB" }, - { "const": 16384, "title": "16 GB" } - ], - "minimum": { "$data": "1/ram_memory" }, - "description": "Maximum memory the container can use. Setting this higher than the request increases OOMKill risk." -} -``` - -Both properties are added to the `required` array of `attributes.schema`. This is the nullplatform UI's contract: the frontend only renders properties that appear in `required` (established during CLIEN-739). Defaults of `null` keep this non-breaking — existing scopes materialize the default, and `normalize_capability_limits` collapses `null` back to the request value before the deployment template renders. - -### uiSchema changes - -Two edits in the existing `Categorization` block: - -1. Change `"label": "Processor"` → `"label": "Resources"`. -2. Add two `Control` entries inside that category's `elements`: - -```json -{ - "type": "Category", - "label": "Resources", - "elements": [ - { "type": "Control", "label": "CPU Millicores", "scope": "#/properties/cpu_millicores" }, - { "type": "Control", "label": "CPU Millicores Limit", "scope": "#/properties/cpu_millicores_limit" }, - { "type": "Control", "label": "RAM Memory Limit", "scope": "#/properties/ram_memory_limit" } - ] -} -``` - -No SHOW/HIDE rules are needed — the "Same as request" option (RAM) and empty value (CPU) act as the no-op state. - -## Validation - -`minimum` with `$data` references the sibling request field. JSON Schema only applies `minimum` to numeric instances, so `null` (or missing) values skip the check naturally — no `if/then` block required. - -The pattern matches the precedent already in this spec: -`health_check.period_seconds.exclusiveMinimum.$data = "1/timeout_seconds"`. - -## Render logic in the deployment template - -The k8s deployment manifest (currently rendering both request and limit from the same capability) must use the new fields with a jq `// fallback`: - -```bash -CPU_REQ=$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores') -CPU_LIM=$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores_limit // .scope.capabilities.cpu_millicores') - -RAM_REQ=$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory') -RAM_LIM=$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory_limit // .scope.capabilities.ram_memory') -``` - -`// .scope.capabilities.cpu_millicores` evaluates to the request value when the limit is `null` or missing, giving the exact retrocompat the ticket asks for. - -The implementation plan will locate the exact file(s) under `k8s/deployment/` that render `resources:` and apply this change. - -## Backwards compatibility - -| Scenario | Behavior | -|---|---| -| Existing scope, no new properties in DB | jq fallback ⇒ limit = request ⇒ identical manifest to today | -| New scope, user does not touch limits | Defaults are `null` ⇒ same as above | -| New scope, user picks a higher limit | Manifest renders the explicit limit; schema validates `limit ≥ request` | -| User tries `limit < request` | JSON schema rejects via `$data` minimum before the workflow runs | - -No data migration needed. - -## Testing plan (high-level) - -- **BATS unit tests** for the deployment script: cover the four matrix cells (limit set / limit null, for both CPU and RAM), asserting the rendered `resources:` block. -- **JSON schema validation tests** (if a test harness exists for the spec): assert that `limit < request` is rejected and `limit >= request` is accepted, including the `null` case. -- **Manual smoke** in a dev environment after the implementation lands. - -The testing detail belongs to the implementation plan (writing-plans), not this design doc. - -## Open questions - -- Exact deployment template file location and templating engine (gomplate vs helm vs raw bash + jq) — to be confirmed at implementation time. The render logic above is engine-agnostic in spirit but the syntax will be adapted. - -## Out of scope / follow-ups - -- Docsite documentation (under `~/nullplatform/apps/docsite/`) — separate ticket if Spin needs it user-facing. -- Symmetric treatment for other resource dimensions (ephemeral storage, GPUs) — not requested. From 26aae22fafd544a9044c14b8f877a074f7398bde Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Fri, 22 May 2026 13:42:41 -0300 Subject: [PATCH 12/16] docs: add changelog entry for configurable CPU and memory limits --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14165264..d8d93cdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Public and private scopes now register DNS records in their correct Route53 hosted zone when using `DNS_TYPE=external_dns`, preventing cross-zone record leakage - Add configurable main HTTP port for k8s scopes (default 8080) and HTTP support for additional ports - Improve **wait deployment active** failure logging: consolidate repeated `Unhealthy` probe events per pod into a single human-readable line, emit a progress heartbeat every 10% of timeout, and surface a targeted suggested fix based on the probe failure mode (port not open / HTTP non-2xx / probe timeout) +- Add configurable memory and CPU limits, independent from requests, for k8s scope containers ## [1.11.0] - 2026-04-16 - Add unit testing support From 81726e1ca027e3f041b9e55818ae4c53b88d4b76 Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Fri, 22 May 2026 16:24:31 -0300 Subject: [PATCH 13/16] refactor: drop ticket id and noise from normalize_capability_limits comment --- k8s/deployment/build_context | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/k8s/deployment/build_context b/k8s/deployment/build_context index c89c7928..aedd2b6f 100755 --- a/k8s/deployment/build_context +++ b/k8s/deployment/build_context @@ -24,7 +24,9 @@ MIN_REPLICAS=$(echo "$MIN_REPLICAS" | awk '{printf "%d", ($1 == int($1) ? $1 : i DEPLOYMENT_STATUS=$(echo "$CONTEXT" | jq -r ".deployment.status") # Fill in *_limit capability fields with the corresponding request value when -# the limit is missing or explicitly null. Idempotent. CLIEN-781. +# the limit is missing or explicitly null, so downstream templates can render +# resources.limits unconditionally and stay back-compat for scopes that never +# had these fields. normalize_capability_limits() { echo "$1" | jq ' .scope.capabilities.cpu_millicores_limit //= .scope.capabilities.cpu_millicores From 0a3ab0fff6353f199c984eaf0457b0f047976518 Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Fri, 22 May 2026 16:24:31 -0300 Subject: [PATCH 14/16] test: exercise normalize via full build_context instead of private function --- k8s/deployment/tests/build_context.bats | 85 ++++++++++++++----------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/k8s/deployment/tests/build_context.bats b/k8s/deployment/tests/build_context.bats index 0f035d6c..ef54509a 100644 --- a/k8s/deployment/tests/build_context.bats +++ b/k8s/deployment/tests/build_context.bats @@ -948,54 +948,63 @@ set_additional_ports() { } # ============================================================================= -# normalize_capability_limits Function Tests (CLIEN-781) -# Fills in *_limit with the corresponding request value when null or missing, -# leaves explicit values untouched. +# Capability limits normalization +# These tests source the real deployment/build_context and assert on the +# resulting CONTEXT, exercising the full pipeline. Limits default to their +# corresponding request value when missing or explicitly null; explicit values +# pass through. # ============================================================================= -setup_normalize_limits_fn() { - eval "$(sed -n '/^normalize_capability_limits()/,/^}/p' "$PROJECT_ROOT/k8s/deployment/build_context")" +# Patches CONTEXT.scope.capabilities with the given JSON fragment (merged into +# the existing capabilities object). +set_capabilities() { + CONTEXT=$(echo "$CONTEXT" | jq --argjson v "$1" '.scope.capabilities = (.scope.capabilities + $v)') } -@test "normalize_capability_limits: fills CPU limit from request when limit is absent" { - setup_normalize_limits_fn - local in='{"scope":{"capabilities":{"cpu_millicores":500,"ram_memory":1024,"ram_memory_limit":2048}}}' - local out - out=$(normalize_capability_limits "$in") - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" +@test "capability limits: cpu limit defaults to cpu_millicores when absent" { + setup_full_build_context + set_capabilities '{"cpu_millicores":500,"ram_memory":1024,"ram_memory_limit":2048}' + + source "$SCRIPT" + + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" } -@test "normalize_capability_limits: fills RAM limit from request when limit is absent" { - setup_normalize_limits_fn - local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":1000,"ram_memory":1024}}}' - local out - out=$(normalize_capability_limits "$in") - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" +@test "capability limits: ram limit defaults to ram_memory when absent" { + setup_full_build_context + set_capabilities '{"cpu_millicores":500,"cpu_millicores_limit":1000,"ram_memory":1024}' + + source "$SCRIPT" + + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" } -@test "normalize_capability_limits: fills both limits when both are absent" { - setup_normalize_limits_fn - local in='{"scope":{"capabilities":{"cpu_millicores":500,"ram_memory":1024}}}' - local out - out=$(normalize_capability_limits "$in") - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" +@test "capability limits: both limits default to their requests when both absent" { + setup_full_build_context + set_capabilities '{"cpu_millicores":500,"ram_memory":1024}' + + source "$SCRIPT" + + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" } -@test "normalize_capability_limits: fills both limits when both are explicit null" { - setup_normalize_limits_fn - local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":null,"ram_memory":1024,"ram_memory_limit":null}}}' - local out - out=$(normalize_capability_limits "$in") - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" +@test "capability limits: explicit null limits fall back to their requests" { + setup_full_build_context + set_capabilities '{"cpu_millicores":500,"cpu_millicores_limit":null,"ram_memory":1024,"ram_memory_limit":null}' + + source "$SCRIPT" + + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" } -@test "normalize_capability_limits: preserves explicit non-null limits" { - setup_normalize_limits_fn - local in='{"scope":{"capabilities":{"cpu_millicores":500,"cpu_millicores_limit":2000,"ram_memory":1024,"ram_memory_limit":4096}}}' - local out - out=$(normalize_capability_limits "$in") - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.cpu_millicores_limit')" "2000" - assert_equal "$(echo "$out" | jq -r '.scope.capabilities.ram_memory_limit')" "4096" +@test "capability limits: explicit non-null limits pass through unchanged" { + setup_full_build_context + set_capabilities '{"cpu_millicores":500,"cpu_millicores_limit":2000,"ram_memory":1024,"ram_memory_limit":4096}' + + source "$SCRIPT" + + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores_limit')" "2000" + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory_limit')" "4096" } From 811b607834bd7de1296e3fafb9a3731e705c8ef6 Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Fri, 22 May 2026 16:27:01 -0300 Subject: [PATCH 15/16] test: remove deployment template shape tests in favor of integration coverage --- .../tests/deployment_template_shape.bats | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 k8s/deployment/tests/deployment_template_shape.bats diff --git a/k8s/deployment/tests/deployment_template_shape.bats b/k8s/deployment/tests/deployment_template_shape.bats deleted file mode 100644 index 6f44ffba..00000000 --- a/k8s/deployment/tests/deployment_template_shape.bats +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bats -# ============================================================================= -# Structural tests for the deployment template. -# Verifies the application container's resources block uses the right -# capability for request vs limit. CLIEN-781. -# ============================================================================= - -setup() { - export PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" - source "$PROJECT_ROOT/testing/assertions.sh" - export TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/deployment.yaml.tpl" -} - -# Slice the file from "name: application" up to the application container's -# terminationMessagePolicy, isolating it from the sidecars (which keep using -# container_cpu_in_millicores / container_memory_in_memory). -app_container_block() { - awk ' - /^[[:space:]]+- name: application[[:space:]]*$/ { in_app=1 } - in_app { print } - /^[[:space:]]+terminationMessagePolicy:/ && in_app { exit } - ' "$TEMPLATE" -} - -# Everything BEFORE the application container — the sidecar definitions. -sidecars_block() { - awk '/^[[:space:]]+- name: application[[:space:]]*$/ {exit} {print}' "$TEMPLATE" -} - -@test "deployment template: application container limits.cpu uses cpu_millicores_limit" { - grep -qE 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores_limit[[:space:]]*\}\}m' <<<"$(app_container_block)" -} - -@test "deployment template: application container limits.memory uses ram_memory_limit" { - grep -qE 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory_limit[[:space:]]*\}\}Mi' <<<"$(app_container_block)" -} - -@test "deployment template: application container requests.cpu still uses cpu_millicores" { - grep -qE 'cpu:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.cpu_millicores[[:space:]]*\}\}m' <<<"$(app_container_block)" -} - -@test "deployment template: application container requests.memory still uses ram_memory" { - grep -qE 'memory:[[:space:]]*\{\{[[:space:]]*\.scope\.capabilities\.ram_memory[[:space:]]*\}\}Mi' <<<"$(app_container_block)" -} - -@test "deployment template: sidecars still use container_cpu_in_millicores / container_memory_in_memory" { - local sidecars - sidecars=$(sidecars_block) - grep -qF '{{ .container_cpu_in_millicores }}m' <<<"$sidecars" - grep -qF '{{ .container_memory_in_memory }}Mi' <<<"$sidecars" - # And sidecars must NOT have been switched to the new fields. - ! grep -qF 'cpu_millicores_limit' <<<"$sidecars" - ! grep -qF 'ram_memory_limit' <<<"$sidecars" -} From 44776c7889c38e0d23f32cf1579b33dfdc1875be Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Fri, 22 May 2026 16:38:06 -0300 Subject: [PATCH 16/16] feat: clamp limit to request when below it as defense-in-depth --- k8s/deployment/build_context | 9 +++++--- k8s/deployment/tests/build_context.bats | 30 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/k8s/deployment/build_context b/k8s/deployment/build_context index aedd2b6f..31880dc7 100755 --- a/k8s/deployment/build_context +++ b/k8s/deployment/build_context @@ -24,13 +24,16 @@ MIN_REPLICAS=$(echo "$MIN_REPLICAS" | awk '{printf "%d", ($1 == int($1) ? $1 : i DEPLOYMENT_STATUS=$(echo "$CONTEXT" | jq -r ".deployment.status") # Fill in *_limit capability fields with the corresponding request value when -# the limit is missing or explicitly null, so downstream templates can render -# resources.limits unconditionally and stay back-compat for scopes that never -# had these fields. +# the limit is missing or explicitly null, then clamp any limit below its +# request up to the request value. The schema rejects limit < request at save +# time; this is defense-in-depth so the script can never produce an invalid +# resources block, regardless of how the context was built. normalize_capability_limits() { echo "$1" | jq ' .scope.capabilities.cpu_millicores_limit //= .scope.capabilities.cpu_millicores | .scope.capabilities.ram_memory_limit //= .scope.capabilities.ram_memory + | .scope.capabilities.cpu_millicores_limit = ([.scope.capabilities.cpu_millicores, .scope.capabilities.cpu_millicores_limit] | max) + | .scope.capabilities.ram_memory_limit = ([.scope.capabilities.ram_memory, .scope.capabilities.ram_memory_limit] | max) ' } diff --git a/k8s/deployment/tests/build_context.bats b/k8s/deployment/tests/build_context.bats index ef54509a..72d9d020 100644 --- a/k8s/deployment/tests/build_context.bats +++ b/k8s/deployment/tests/build_context.bats @@ -1008,3 +1008,33 @@ set_capabilities() { assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores_limit')" "2000" assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory_limit')" "4096" } + +@test "capability limits: cpu limit below request is clamped up to request" { + setup_full_build_context + set_capabilities '{"cpu_millicores":500,"cpu_millicores_limit":100,"ram_memory":1024,"ram_memory_limit":2048}' + + source "$SCRIPT" + + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory_limit')" "2048" +} + +@test "capability limits: ram limit below request is clamped up to request" { + setup_full_build_context + set_capabilities '{"cpu_millicores":500,"cpu_millicores_limit":1000,"ram_memory":1024,"ram_memory_limit":64}' + + source "$SCRIPT" + + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores_limit')" "1000" + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" +} + +@test "capability limits: both limits below their requests are clamped up" { + setup_full_build_context + set_capabilities '{"cpu_millicores":500,"cpu_millicores_limit":100,"ram_memory":1024,"ram_memory_limit":64}' + + source "$SCRIPT" + + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.cpu_millicores_limit')" "500" + assert_equal "$(echo "$CONTEXT" | jq -r '.scope.capabilities.ram_memory_limit')" "1024" +}