Skip to content

Commit a1a2bb6

Browse files
mjudeikisclaude
andcommitted
feat(infrastructure): template catalog hardening — worker + cron-job, honest redis knobs, tenant LimitRange
Batch one of the catalog review, aimed at simple developers (coverage gaps) and enterprise actors (resource honesty + bounds): - redis-cache (v0.2.0): the size knob claimed to pick a memory limit but the StatefulSet set no resources, and persistent was accepted while no PVC existed — two silent no-ops. size is now wired to the container memory request+limit (64Mi/256Mi/1Gi) AND redis maxmemory (~25% under the limit, allkeys-lru) so a full cache evicts instead of OOM-killing; persistent is removed — the template is explicitly an ephemeral cache. Portal view + agent guidance updated to match. - worker (new): one container running forever, no Service, no URL — queue consumers, bots, pollers. Development-capable: single component claiming the workspace root on the node toolchain, full data-plane verbs; image required in production only (the simple-webapp CEL pattern). - cron-job (new): a container on a 5-field cron schedule (ScheduledJob instance kind) — Forbid concurrency, 3 retries, history limits. No dev mode by design (the dev overlay supports Deployments only); the agent contract says bake the task into the image entrypoint. - Tenant LimitRange: every tenant namespace now gets a create-only "kedge-defaults" LimitRange (defaultRequest 50m/128Mi, default limit 500m/512Mi, max 2cpu/2Gi per container) stamped by EnsureTenantNamespace — no seed template pins its own resources, so tenant workloads on the shared runtime cluster were unbounded. Create-only means operator-tuned copies are never overwritten; a provider restart retrofits pre-existing namespaces. Opt out with KEDGE_TENANT_LIMITRANGE=disabled (chart: tenantLimitRange.enabled). - database: portal view block (connection, version, readiness) so it stops rendering as a raw values dump. - e2e: worker/simple-webapp dev instances are created without their image inputs to prove dev mode needs none; dead e2eMinimalSpecs entries dropped (every seed template now ships sampleValues). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a4c8370 commit a1a2bb6

10 files changed

Lines changed: 557 additions & 14 deletions

File tree

providers/infrastructure/backend/kro/e2e_devmode_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ import (
6363
// NOT need — dev mode swaps the tier images for platform dev images, so the
6464
// instance is created without them to prove the graph renders regardless.
6565
var e2eDevImageStripped = map[string][]string{
66-
"application": {"frontendImage", "backendImage"},
66+
"application": {"frontendImage", "backendImage"},
67+
"simple-webapp": {"image"},
68+
"worker": {"image"},
6769
}
6870

6971
func TestE2EDevelopmentMode(t *testing.T) {

providers/infrastructure/backend/kro/e2e_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,7 @@ const (
8181

8282
// e2eMinimalSpecs supplies a valid instance spec for templates that ship no
8383
// sampleValues (those that do — application, database — use them directly).
84-
var e2eMinimalSpecs = map[string]map[string]any{
85-
"redis-cache": {"name": "e2e-redis"},
86-
}
84+
var e2eMinimalSpecs = map[string]map[string]any{}
8785

8886
// e2ePlatformStamped are spec fields a platform component normally writes onto an
8987
// instance before kro reconciles it — values the user does NOT supply (the

providers/infrastructure/deploy/chart/templates/deployment.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ spec:
141141
- name: KEDGE_GATEWAY_NAMESPACE
142142
value: {{ . | quote }}
143143
{{- end }}
144+
{{- if not .Values.tenantLimitRange.enabled }}
145+
# Opt out of stamping the kedge-defaults LimitRange into tenant
146+
# namespaces (for clusters that manage resource policy themselves).
147+
- name: KEDGE_TENANT_LIMITRANGE
148+
value: "disabled"
149+
{{- end }}
144150
{{- with .Values.development.agentImage }}
145151
# Dev-mode agent injector image (${kedge.devAgentImage}).
146152
- name: KEDGE_DEV_AGENT_IMAGE

providers/infrastructure/deploy/chart/values.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ application:
5858
name: "cloudflare-tunnel"
5959
namespace: "cfgate-system"
6060

61+
# Default container resource policy stamped into every tenant namespace as a
62+
# LimitRange named "kedge-defaults" (defaultRequest 50m/128Mi, default limit
63+
# 500m/512Mi, max 2cpu/2Gi per container). Create-only — operators may
64+
# hand-tune a tenant's copy without it being overwritten. Disable when the
65+
# runtime cluster manages its own resource policy.
66+
tenantLimitRange:
67+
enabled: true
68+
6169
# Development-mode images (docs/app-studio-template-sandboxes.md). These run
6270
# TENANT code, so production deployments should pin them by digest. Empty
6371
# values fall back to the in-binary defaults (node → the kedge sandbox-runner
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
apiVersion: infrastructure.kedge.faros.sh/v1alpha1
2+
kind: Template
3+
metadata:
4+
name: cron-job
5+
spec:
6+
displayName: "Scheduled job"
7+
description: "A container that runs on a cron schedule and exits — CronJob only, no Service, no URL. Reports, cleanups, periodic syncs."
8+
category: Workloads
9+
version: 0.1.0
10+
backend: kro
11+
# One-click provision defaults for the portal form. busybox's default
12+
# entrypoint exits 0 immediately, so the sample schedule visibly produces
13+
# completed Jobs with zero external setup.
14+
sampleValues:
15+
name: demo-job
16+
image: busybox:stable
17+
schedule: "*/30 * * * *"
18+
# Operational guidance for AI agents discovering this template over MCP.
19+
agent:
20+
usage: |
21+
Runs ONE container on a cron schedule; each run must do its work and
22+
EXIT 0. Choose this for periodic work — reports, cleanups, backups,
23+
syncs. It is the opposite lifecycle of the worker template (a process
24+
that runs forever): a cron-job container that never exits will pile up
25+
overlapping-forbidden runs, not "keep running".
26+
27+
YOU SUPPLY exactly one container image whose ENTRYPOINT performs the
28+
task and exits: 0 on success, non-zero on failure (failed runs are
29+
retried up to 3 times, then recorded as a failed Job). There is no
30+
command/args input — bake the task into the image entrypoint.
31+
32+
SCHEDULE: standard 5-field cron (schedule input, default "0 * * * *" =
33+
hourly, evaluated in UTC). Concurrent runs are forbidden — if a run is
34+
still going when the next tick fires, the new run is skipped. Runs may
35+
occasionally fire late or be skipped; the task must be idempotent and
36+
tolerate that.
37+
38+
This template has NO development mode — develop and test the image
39+
elsewhere, then provision it here. No network exposure; durable state
40+
belongs in a database template instance.
41+
42+
INPUTS you set when provisioning: name, image (both required); schedule.
43+
outputs:
44+
- "status.schedule — the active cron schedule."
45+
# ── Portal presentation ───────────────────────────────────────────────
46+
view:
47+
columns:
48+
- header: Schedule
49+
path: spec.schedule
50+
type: code
51+
detail:
52+
- title: Configuration
53+
fields:
54+
- label: Schedule
55+
path: status.schedule
56+
type: code
57+
- label: Image
58+
path: spec.image
59+
type: code
60+
instanceCRD:
61+
group: infrastructure.kedge.faros.sh
62+
version: v1alpha1
63+
resource: scheduledjobs
64+
kind: ScheduledJob
65+
schema:
66+
type: object
67+
properties:
68+
name:
69+
type: string
70+
description: "Logical name; used as the CronJob name in the tenant namespace."
71+
image:
72+
type: string
73+
description: "Container image whose entrypoint performs the task and exits (0 = success)."
74+
schedule:
75+
type: string
76+
default: "0 * * * *"
77+
description: "Standard 5-field cron expression, evaluated in UTC. Default: hourly."
78+
required:
79+
- name
80+
- image
81+
82+
# ── backendConfig: the kro resource graph ─────────────────────────────
83+
# namespace: default is remapped to the per-tenant namespace on the runtime
84+
# cluster — same convention as every seed template.
85+
backendConfig:
86+
resources:
87+
- id: cronJob
88+
template:
89+
apiVersion: batch/v1
90+
kind: CronJob
91+
metadata:
92+
name: ${schema.spec.name}
93+
namespace: default
94+
spec:
95+
schedule: ${schema.spec.schedule}
96+
# A run still going when the next tick fires wins; the new run is
97+
# skipped. Matches the agent contract above.
98+
concurrencyPolicy: Forbid
99+
successfulJobsHistoryLimit: 3
100+
failedJobsHistoryLimit: 1
101+
jobTemplate:
102+
spec:
103+
backoffLimit: 3
104+
template:
105+
metadata:
106+
labels:
107+
app: ${schema.spec.name}
108+
spec:
109+
restartPolicy: Never
110+
containers:
111+
- name: job
112+
image: ${schema.spec.image}
113+
# Echoed from the materialized CronJob (this kro fork forbids ${schema.*}
114+
# in status — status may reference resources only).
115+
status:
116+
schedule: ${cronJob.spec.schedule}

providers/infrastructure/install/templates/database.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,33 @@ spec:
3333
- "status.host / status.port - the in-cluster Service endpoint for PostgreSQL."
3434
- "status.ready - ready replica count for the Postgres StatefulSet."
3535
- "status.connectionSecretRef - Secret '<name>-db-credentials' containing host, port, user, dbname, password, and uri keys. Secret values are not surfaced in status."
36+
# ── Portal presentation ───────────────────────────────────────────────
37+
view:
38+
columns:
39+
- header: Version
40+
path: spec.version
41+
type: badge
42+
detail:
43+
- title: Connection
44+
fields:
45+
- label: Host
46+
path: status.host
47+
type: code
48+
- label: Port
49+
path: status.port
50+
type: code
51+
- label: Credentials secret
52+
path: status.connectionSecretRef.name
53+
type: code
54+
- title: Configuration
55+
fields:
56+
- label: Postgres version
57+
path: spec.version
58+
type: badge
59+
- title: Readiness
60+
fields:
61+
- label: Database
62+
path: status.ready
3663
instanceCRD:
3764
group: infrastructure.kedge.faros.sh
3865
version: v1alpha1

providers/infrastructure/install/templates/redis-cache.yaml

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ metadata:
44
name: redis-cache
55
spec:
66
displayName: "Redis cache"
7-
description: "A Redis instance for caching workloads. StatefulSet + Service; size knob picks the memory limit."
7+
description: "A Redis instance for caching workloadsStatefulSet + Service; the size knob picks the memory bucket. Ephemeral: data does not survive a restart."
88
category: Databases
9-
version: 0.1.0
9+
version: 0.2.0
1010
backend: kro
1111
sampleValues:
1212
name: cache
@@ -25,12 +25,46 @@ spec:
2525
`<name>-credentials`, key `uri`. The URI has the form
2626
`redis://:<password>@<name>:6379`. Redis may not be ready the moment a
2727
consuming app starts — retry the initial connection. This template does
28-
not expose Redis publicly, and persistent=false (the default) means data
29-
does not survive a restart.
28+
not expose Redis publicly.
29+
30+
Redis is EPHEMERAL here: no persistence is configured, so a pod restart
31+
loses all data. Treat it strictly as a cache — never as a system of
32+
record; durable state belongs in the database template. The size input
33+
picks the container memory bucket: small=64Mi, medium=256Mi, large=1Gi.
3034
outputs:
3135
- "status.host / status.port — the in-cluster Service endpoint for Redis."
3236
- "status.ready — ready replica count for the Redis StatefulSet."
3337
- "status.connectionSecretRef — Secret '<name>-credentials' containing host, port, password, and uri keys. Secret values are not surfaced in status."
38+
# ── Portal presentation ───────────────────────────────────────────────
39+
view:
40+
columns:
41+
- header: Size
42+
path: spec.size
43+
type: badge
44+
detail:
45+
- title: Connection
46+
fields:
47+
- label: Host
48+
path: status.host
49+
type: code
50+
- label: Port
51+
path: status.port
52+
type: code
53+
- label: Credentials secret
54+
path: status.connectionSecretRef.name
55+
type: code
56+
- title: Configuration
57+
fields:
58+
- label: Size
59+
path: spec.size
60+
type: badge
61+
- label: Redis version
62+
path: spec.version
63+
type: badge
64+
- title: Readiness
65+
fields:
66+
- label: Redis
67+
path: status.ready
3468
instanceCRD:
3569
group: infrastructure.kedge.faros.sh
3670
version: v1alpha1
@@ -46,14 +80,11 @@ spec:
4680
type: string
4781
enum: ["small", "medium", "large"]
4882
default: "small"
49-
description: "Memory bucket (64Mi / 256Mi / 1Gi)."
83+
description: "Memory bucket, wired to the container's memory request+limit (small=64Mi, medium=256Mi, large=1Gi)."
5084
version:
5185
type: string
5286
enum: ["6", "7"]
5387
default: "7"
54-
persistent:
55-
type: boolean
56-
default: false
5788
required:
5889
- name
5990
# backendConfig is interpreted by the kro backend. Every resource sets
@@ -168,7 +199,17 @@ spec:
168199
containers:
169200
- name: redis
170201
image: redis:${schema.spec.version}
171-
command: ["redis-server", "--requirepass", "$(REDIS_PASSWORD)"]
202+
# maxmemory sits ~25% under the container limit (working
203+
# memory + fragmentation headroom) with LRU eviction, so
204+
# a full cache evicts instead of OOM-killing the pod.
205+
command:
206+
- redis-server
207+
- --requirepass
208+
- $(REDIS_PASSWORD)
209+
- --maxmemory
210+
- '${schema.spec.size == "small" ? "48mb" : (schema.spec.size == "medium" ? "192mb" : "768mb")}'
211+
- --maxmemory-policy
212+
- allkeys-lru
172213
env:
173214
- name: REDIS_PASSWORD
174215
valueFrom:
@@ -177,6 +218,16 @@ spec:
177218
key: password
178219
ports:
179220
- containerPort: 6379
221+
# The size bucket bounds the container: memory request ==
222+
# limit (Redis is memory, an OOM-kill under the limit is
223+
# the contract working). Single-quoted: the CEL ternaries
224+
# contain ": " which YAML would otherwise read as mappings.
225+
resources:
226+
requests:
227+
cpu: 50m
228+
memory: '${schema.spec.size == "small" ? "64Mi" : (schema.spec.size == "medium" ? "256Mi" : "1Gi")}'
229+
limits:
230+
memory: '${schema.spec.size == "small" ? "64Mi" : (schema.spec.size == "medium" ? "256Mi" : "1Gi")}'
180231
- id: service
181232
template:
182233
apiVersion: v1

0 commit comments

Comments
 (0)