Skip to content

Commit 3ef76fb

Browse files
Merge pull request #43 from yangchuansheng/codex/llmgateway-runtime-contract
fix(sealos): harden multi-service web runtime smoke
2 parents 9ee2d3a + a4ed14b commit 3ef76fb

6 files changed

Lines changed: 110 additions & 5 deletions

File tree

skills/docker-to-sealos/SKILL.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ Extract from Docker Compose/docs:
3232
- ports, dependencies, service communication
3333
- env vars and secret usage
3434
- startup-time validation rules for bootstrap credentials, API keys, salts, secrets, and feature flags
35+
- multi-service web roles: browser entry, REST API, OpenAI/API gateway, docs, workers, and one-shot jobs
3536
- resource limits/requests and health checks
36-
- if official Kubernetes installation docs/manifests are available, also extract app-runtime behavior from them (bootstrap admin fields, external endpoint/protocol assumptions, health probes, startup/init flow)
37+
- if official Kubernetes installation docs/manifests are available, also extract app-runtime behavior from them (bootstrap admin fields, external endpoint/protocol assumptions, health probes, startup/init flow, migration ordering)
3738

3839
### Step 2: Infer metadata
3940

@@ -65,6 +66,8 @@ Apply field-level mappings from `references/conversion-mappings.md`, including:
6566
- for DB URL/DSN envs (for example `*_DATABASE_URL`, `*_DB_URL`), when Kubeblocks `endpoint` is host:port, inject `host`/`port`/`username`/`password` via approved `secretKeyRef` envs and compose the final URL with `$(VAR)` expansion
6667
- edge gateway normalization: when Compose includes Traefik-like edge proxy plus business services, skip the proxy workload and expose business services via Sealos Ingress directly
6768
- TLS offload normalization for Sealos Ingress: when a business service exposes both 80 and 443, drop 443 from workload/service ports and remove in-container TLS certificate mounts (for example `/etc/nginx/ssl`, `/etc/ssl`, `/certs`) unless official Kubernetes docs explicitly require HTTPS backend-to-service traffic
69+
- multi-service web normalization: expose the verified browser entry in the App resource, expose API/gateway/docs only when they are intended public surfaces, and keep workers private with no Service/Ingress
70+
- URL topology: browser-facing env vars must use public HTTPS URLs, while server-to-server env vars must use Kubernetes Service FQDNs unless the app explicitly requires public callbacks
6871
- prefer `scripts/compose_to_template.py --kompose-mode always` as deterministic conversion entrypoint (require `kompose` for reproducible workload shaping)
6972
- when official Kubernetes installation docs/manifests exist, perform a dual-source merge: use Compose as baseline topology, then align app-runtime semantics with official Kubernetes guidance
7073

@@ -77,6 +80,8 @@ Apply field-level mappings from `references/conversion-mappings.md`, including:
7780
- For PostgreSQL custom databases (non-`postgres`), the init Job must wait for PostgreSQL readiness before execution and create the target database idempotently.
7881
- Critical application compatibility objects must be verified in live database state. Use idempotent initContainer self-healing for compatibility views, legacy tables/views, indexes, extensions, search paths, and bootstrap state that the app requires on every cold start.
7982
- One-shot init Jobs may create initial databases or seed state, but app startup gates must verify the final database objects directly. Treat TTL-expired Jobs as historical evidence and rely on database state for acceptance.
83+
- Worker, gateway, and background services that depend on app migrations must wait for the required tables, migration markers, or app-specific readiness objects, not only for the database port.
84+
- Redis readiness probes or initContainers must tolerate authenticated Redis responses such as `NOAUTH` or `Authentication required` when credentials are not needed for readiness.
8085
- PostgreSQL bootstrap shell must use safe quoting patterns. Prefer shell-level existence checks plus simple SQL statements when possible. Use single-quoted heredocs or SQL files for psql variable interpolation, and avoid PL/pgSQL `DO $$` blocks in inline shell commands when a guard query can express the same logic.
8186
- Do not use `psql -c "..."` for `:'var'` variable interpolation. Use `psql -v name=value <<'SQL' ... :'name' ... SQL` or pass already-safe literal SQL.
8287

@@ -121,6 +126,7 @@ If validation fails, fix template/rules/examples first.
121126
- App resource `spec.displayType` must be `normal`.
122127
- App resource `spec.type` must be `link`.
123128
- App resource `spec.data.url` must be the browser entry URL that succeeds from a fresh Sealos launch. For apps with safe-path, setup-path, or entrance-path behavior, verify the configured path and root path, then choose the URL that supports login or first-run setup without hidden prior navigation.
129+
- SSR/Next.js/React server apps must not use a path that renders a server-side exception as the App URL or HTTP probe. Treat visible `Application error`, `server-side exception`, `Internal Server Error`, or `Unhandled Runtime Error` text as a failed entry path even if the HTTP status is 2xx/3xx.
124130
- Never use `spec.template` in App resource.
125131
- `cloud.sealos.io/app-deploy-manager` label value must equal resource `metadata.name`.
126132
- `metadata.labels.app` label value must equal resource `metadata.name` for managed app workloads.
@@ -170,6 +176,7 @@ If validation fails, fix template/rules/examples first.
170176
- Database connection/bootstrap may use Kubeblocks-provided secrets, and reserved Kubeblocks database secret names must not be redefined by custom `Secret` resources.
171177
- Env vars must be declared before referenced (for example password before URL composition).
172178
- Follow official app env var naming; do not invent prefixes.
179+
- For split frontend/API/gateway apps, keep public browser URLs and internal service URLs separate. Frontend/browser callback variables use `https://${{ defaults.<host> }}.${{ SEALOS_CLOUD_DOMAIN }}`; backend-to-backend variables use `http://<service>.$(SEALOS_NAMESPACE).svc.cluster.local:<port>` or the fully rendered Service FQDN.
173180
- When the application requires its public URL configured via a file-based config system (e.g., node-config `config/default.json`, PHP config files), create a ConfigMap containing the config file with the public URL set to `https://${{ defaults.app_host }}.${{ SEALOS_CLOUD_DOMAIN }}`, and mount it to the application's config directory. The ConfigMap must follow standard naming and label conventions.
174181
- For PostgreSQL custom databases (non-`postgres`), include `${{ defaults.app_name }}-pg-init` Job and implement startup-safe/idempotent creation logic (readiness wait + existence check before create).
175182
- For application-specific database compatibility, include an initContainer or startup gate that idempotently creates or repairs required views, aliases, indexes, extensions, privileges, role search paths, and legacy compatibility objects before the business container starts.

skills/docker-to-sealos/references/must-rules-map.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ must_rules:
6060
enforcement:
6161
type: manual
6262
note: "Requires live Sealos App URL smoke from a fresh browser/session."
63+
- must: "SSR/Next.js/React server apps must not use a path that renders a server-side exception as the App URL or HTTP probe. Treat visible `Application error`, `server-side exception`, `Internal Server Error`, or `Unhandled Runtime Error` text as a failed entry path even if the HTTP status is 2xx/3xx."
64+
enforcement:
65+
type: manual
66+
note: "Requires HTTP body/runtime smoke inspection, covered by sealos-live-smoke failure signals."
6367
- must: "Never use `spec.template` in App resource."
6468
enforcement:
6569
type: rule
@@ -208,6 +212,10 @@ must_rules:
208212
enforcement:
209213
type: manual
210214
note: "Requires per-application canonical env dictionary from upstream docs."
215+
- must: "For split frontend/API/gateway apps, keep public browser URLs and internal service URLs separate. Frontend/browser callback variables use `https://${{ defaults.<host> }}.${{ SEALOS_CLOUD_DOMAIN }}`; backend-to-backend variables use `http://<service>.$(SEALOS_NAMESPACE).svc.cluster.local:<port>` or the fully rendered Service FQDN."
216+
enforcement:
217+
type: manual
218+
note: "Requires app-specific URL topology review across frontend, API, gateway, and worker env vars."
211219
- must: "When the application requires its public URL configured via a file-based config system (e.g., node-config `config/default.json`, PHP config files), create a ConfigMap containing the config file with the public URL set to `https://${{ defaults.app_host }}.${{ SEALOS_CLOUD_DOMAIN }}`, and mount it to the application's config directory. The ConfigMap must follow standard naming and label conventions."
212220
enforcement:
213221
type: manual

skills/docker-to-sealos/references/sealos-specs.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,38 @@ spec:
150150

151151
**Do NOT add any other fields.** Fields like `menuData`, `nameColor`, `template`, etc. do not exist in the App CRD and will cause `strict decoding error: unknown field` on apply.
152152

153+
### Multi-Service Web Runtime Contract
154+
155+
For split-service web apps, classify each component before writing resources:
156+
157+
- **Browser entry**: dashboard, console, setup UI, or docs page opened by users.
158+
- **REST API**: authenticated application or dashboard API.
159+
- **Protocol gateway**: OpenAI-compatible, webhook, or SDK-facing API surface.
160+
- **Docs/static service**: optional public documentation.
161+
- **Worker**: background processor with no public Service/Ingress unless upstream explicitly exposes one.
162+
163+
The App CRD `spec.data.url` must point to the browser entry URL that works from a fresh Sealos launch. Test the root path and any login/setup/entrance path from upstream docs or source. Choose the path that loads without prior navigation and reaches login, registration, or setup.
164+
165+
For SSR/Next.js/React server apps, HTTP status is not enough. Treat the entry as failed when the response body contains visible runtime failure text such as:
166+
167+
- `Application error`
168+
- `server-side exception`
169+
- `Internal Server Error`
170+
- `Unhandled Runtime Error`
171+
- Next.js runtime digest markers such as `NEXT_`
172+
173+
These failures invalidate the App URL and any HTTP probe path even when the HTTP status is 2xx or 3xx.
174+
175+
Use `nginx.ingress.kubernetes.io/app-root` only after the target path has been verified as a stable fresh-session entry. It must not hide a broken root path, missing public URL configuration, or wrong backend service routing.
176+
177+
Public and internal URLs must stay separate:
178+
179+
- Browser-facing env vars use public HTTPS URLs: `https://${{ defaults.<host> }}.${{ SEALOS_CLOUD_DOMAIN }}`.
180+
- Backend-to-backend env vars use Kubernetes Service DNS names, for example `http://<service>.${{ SEALOS_NAMESPACE }}.svc.cluster.local:80`.
181+
- Use public URLs for server-side callbacks only when upstream explicitly requires external callback or OAuth-style redirect behavior.
182+
183+
When workers, protocol gateways, or background services depend on database migrations, gate startup on app-specific database state such as migration markers or required tables, not merely on the database port.
184+
153185
## Defaults and Inputs Configuration Specification
154186

155187
### Basic Principles

skills/sealos-deploy/modules/pipeline.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,7 @@ Acceptance checklist:
10831083
- Service endpoints are populated.
10841084
- The actual App URL loads from a fresh session.
10851085
- Login-gated apps complete setup/login and one authenticated action.
1086+
- SSR/browser failure text such as `Application error`, `server-side exception`, `Internal Server Error`, and `Unhandled Runtime Error` is absent from smoke responses.
10861087
- Recent logs are clear of recurring startup, migration, bootstrap, and access-control failures.
10871088
- Main business containers keep `command`/`args` short and close to the official entrypoint; repeated file preparation, permission repair, database bootstrap, or compatibility self-healing belongs in initContainers, Jobs, or ConfigMap scripts.
10881089
- Shell wrappers in main containers end with `exec <final-process>` so signal handling remains correct.

skills/sealos-deploy/references/live-smoke-playbooks.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ Run this pass after Phase 6 for every deployment unless the user explicitly asks
1616
3. Visit the actual App URL exactly as Sealos launches it. Test the root path and the configured App URL path when the app uses an entrance or safe-path mechanism.
1717
4. For login-gated web apps, complete registration or login, confirm a token/session, and open at least one authenticated page or API route.
1818
5. Scan recent logs after login. Treat recurring application errors as deployment failures even when all Pods are Running.
19-
6. Inventory the full footprint before cleanup or handoff:
19+
6. Treat visible SSR/browser failure text such as `Application error`, `server-side exception`, `Internal Server Error`, and `Unhandled Runtime Error` as failed smoke even when HTTP returns 2xx/3xx.
20+
7. Inventory the full footprint before cleanup or handoff:
2021
- `instances.app.sealos.io`
2122
- `apps.app.sealos.io`
2223
- Deployments/StatefulSets/CronJobs/Jobs
@@ -68,6 +69,30 @@ Bootstrap quoting guidance:
6869
- Use `psql -tAc "SELECT ..."` plus guarded `psql -c` or single-quoted heredocs for idempotent object creation.
6970
- Use `psql -v name=value` variable interpolation inside heredocs for sensitive SQL values such as passwords. Do not rely on `psql -c "ALTER ROLE ... :'var'"`; psql colon variables are not expanded in that form.
7071

72+
## LLM Gateway / Multi-Service SSR Web App
73+
74+
Use this playbook for split-service apps with a dashboard, REST API, protocol gateway, docs service, and workers.
75+
76+
Component checks:
77+
78+
- Dashboard/browser entry: visit both root and the App resource URL path from a fresh session.
79+
- API service: check readiness and recent logs before and after login or signup.
80+
- Gateway/API protocol service: check readiness and recent logs; do not require provider credentials for basic startup unless upstream requires them.
81+
- Worker: inspect logs after API migrations complete and after one authenticated dashboard action.
82+
- Docs/static service: verify it serves a page if exposed publicly.
83+
84+
Runtime acceptance:
85+
86+
- The App URL reaches login, signup, or setup without `Application error: a server-side exception has occurred`.
87+
- For login-gated apps, complete signup or login, then open at least one authenticated dashboard page.
88+
- Recent dashboard, API, gateway, and worker logs are clear of recurring SSR, migration, auth/session, and service-to-service URL errors.
89+
- Gateway or worker pods that depend on database migrations wait for required tables or migration markers, not only PostgreSQL readiness.
90+
- Public browser URLs and internal service URLs are not mixed: browser-facing config uses public HTTPS hosts, while backend-to-backend config uses Kubernetes Service DNS.
91+
92+
Debug loop:
93+
94+
If the App URL shows `Application error: a server-side exception has occurred`, read dashboard/API logs first, then verify App URL path, public URL env vars, API backend URL, migration completion gates, and Redis/PostgreSQL readiness behavior before reporting success.
95+
7196
## Generic Login-Gated Web App
7297

7398
Minimum smoke:

skills/sealos-deploy/scripts/sealos-live-smoke.mjs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ function fail(message, extra = {}) {
3939
}
4040

4141
function joinUrl(base, path) {
42-
return new URL(path || "/", base).toString();
42+
if (path === undefined || path === null || path === "") {
43+
return new URL(base).toString();
44+
}
45+
return new URL(path, base).toString();
4346
}
4447

4548
function redact(value) {
@@ -74,6 +77,33 @@ function redactJson(value, key = "") {
7477
return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, redactJson(entry, entryKey)]));
7578
}
7679

80+
const FAILURE_SIGNAL_PATTERNS = [
81+
{
82+
id: "application-error",
83+
pattern: /Application error/i,
84+
},
85+
{
86+
id: "server-side-exception",
87+
pattern: /server-side exception/i,
88+
},
89+
{
90+
id: "internal-server-error",
91+
pattern: /Internal Server Error/i,
92+
},
93+
{
94+
id: "unhandled-runtime-error",
95+
pattern: /Unhandled Runtime Error/i,
96+
},
97+
{
98+
id: "next-runtime-digest",
99+
pattern: /\bNEXT_[A-Z0-9_]+\b/i,
100+
},
101+
];
102+
103+
function detectFailureSignals(text) {
104+
return FAILURE_SIGNAL_PATTERNS.filter(({ pattern }) => pattern.test(text)).map(({ id }) => id);
105+
}
106+
77107
async function requestStep(name, url, options = {}) {
78108
const startedAt = Date.now();
79109
const response = await fetch(url, {
@@ -95,16 +125,18 @@ async function requestStep(name, url, options = {}) {
95125
}
96126
}
97127
const safeJson = redactJson(json);
128+
const failureSignals = detectFailureSignals(text);
98129
return {
99130
name,
100131
url,
101132
status: response.status,
102-
ok: response.status >= 200 && response.status < 400,
133+
ok: response.status >= 200 && response.status < 400 && failureSignals.length === 0,
103134
elapsedMs: Date.now() - startedAt,
104135
contentType,
105136
location: response.headers.get("location"),
106137
setCookie: response.headers.get("set-cookie") ? "<present>" : null,
107138
bodyPreview: safeJson ? JSON.stringify(safeJson).slice(0, 240) : text.slice(0, 240),
139+
failureSignals,
108140
json: safeJson,
109141
rawJson: json,
110142
};
@@ -155,7 +187,7 @@ if (!baseUrl) {
155187

156188
const steps = [];
157189
try {
158-
steps.push(await requestStep("root", joinUrl(baseUrl, args.rootPath || "/")));
190+
steps.push(await requestStep("root", joinUrl(baseUrl, args.rootPath)));
159191

160192
if (args.captchaPath) {
161193
steps.push(await requestStep("captcha", joinUrl(baseUrl, args.captchaPath)));

0 commit comments

Comments
 (0)