Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,44 @@ Refer to other apps to add some configuration.
In order to support multiple namespaces, the method of obtaining 'namespace' in the
backend should be replaced with the method of obtaining it from 'kubeconfig' instead
of adding 'ns-' to 'user' for generation purpose.

## Health probes

Desktop, Costcenter and License expose root `GET /healthz` (and `HEAD`). A ready
process returns HTTP 200; missing local configuration returns HTTP 503. Responses
are not cached. These routes require no user credentials and do not query databases,
Kubernetes, billing APIs or other external services. They are not business-health checks.

- Desktop checks `/app/data/config.yaml` in production, or `CONFIG_PATH` (default
`data/config.local.yaml`) in development: cloud domain/region, database URI strings
and internal/regional/global JWT keys must be present.
- Costcenter checks that instrumentation has installed its validated `__APP_CONFIG__`.
It does not reload configuration or terminate the process from the health route.
- License requires nonempty `SEALOS_DOMAIN`, `LICENSE_DOMAIN` and `MONGODB_URI`.
These existing variables are injected from chart configuration and the generated Secret.

All three charts expose `startupProbe`, `readinessProbe`, `livenessProbe` and
`tests.path` in both `values.yaml` and the installation `*-values.yaml`. Defaults:

| App | Startup (period / timeout / failures) | Readiness | Liveness |
| --- | --- | --- | --- |
| Desktop | 2s / 3s / 60 | delay 5s, period 3s, timeout 2s, successes 3, failures 3 | delay 15s, period 20s, timeout 1s, failures 3 |
| Costcenter | 2s / 3s / 30 | period 10s, timeout 3s, failures 6 | period 20s, timeout 3s, failures 3 |
| License | 5s / 5s / 30 | period 5s, timeout 5s, failures 3 | period 20s, timeout 5s, failures 3 |

Startup success enables readiness/liveness checks. Readiness gates Service traffic;
liveness failure restarts the container. Override the objects in the installation
values file or with Helm `-f` / `--set startupProbe.failureThreshold=90`. There are
no new probe environment variables. Keep the path `/healthz` and the container port
3000 unless changing the application's actual listener. `tests.image` defaults to
`busybox:1.36.1`; `tests.path` defaults to `/healthz`. Run `helm test <release> -n
<namespace> --logs` to check the Service endpoint.

Release the application image and updated chart together: older images do not have
`/healthz`. Roll back both image and probe configuration together. Existing Helm
releases with retained values must explicitly update their old probe paths. Desktop's
migration init container does not receive HTTP probes; do not run migrations as part
of health verification.

Run `node --test frontend/scripts/healthz.test.cjs` from the repository root for
isolated endpoint and local-configuration regression tests.
8 changes: 7 additions & 1 deletion frontend/desktop/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@ SEALOS_CLOUD_DOMAIN="cloud.sealos.io"
# costcenter
STRIPE_ENABLED=
STRIPE_PUB=
SEALOS_CLOUD_DOMAIN=
SEALOS_CLOUD_DOMAIN=
# GET /healthz checks local configuration only; it never connects to a database.
# Probe timings/path are Helm startupProbe/readinessProbe/livenessProbe values,
# not environment variables. See frontend/README.md (Health probes).
# Production config: /app/data/config.yaml (mounted by the chart).
# Development config: CONFIG_PATH, default data/config.local.yaml.
# CONFIG_PATH=data/config.local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,35 @@ desktopConfig:
# Team management
maxTeamCount: 0
maxTeamMemberCount: 0

# App-local health checks; override these objects with Helm -f/--set.
# Startup protects initialization; readiness gates traffic; liveness restarts hung containers.
startupProbe:
httpGet:
path: /healthz
port: 3000
periodSeconds: 2
timeoutSeconds: 3
failureThreshold: 60

readinessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 5
periodSeconds: 3
timeoutSeconds: 2
successThreshold: 3
failureThreshold: 3

livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 15
periodSeconds: 20

# helm test uses the same app-local health endpoint.
tests:
image: busybox:1.36.1
path: /healthz
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,13 @@ spec:
REGION_DATABASE_URL=$(cat /app/data/config.yaml | awk '/regionalCockroachdbURI:/ {print $2}' | tr -d '"') \
GLOBAL_DATABASE_URL=$(cat /app/data/config.yaml | awk '/globalCockroachdbURI:/ {print $2}' | tr -d '"') \
node /app/desktop/server.js
image: "{{ .Values.image }}"
startupProbe:
{{- toYaml .Values.startupProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
image: "{{ .Values.image }}"
imagePullPolicy: {{ .Values.imagePullPolicy }}
volumeMounts:
- mountPath: /app/data/config.yaml
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Pod
metadata:
name: {{ include "desktop.fullname" . }}-test-connection
namespace: {{ .Release.Namespace }}
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": before-hook-creation
spec:
restartPolicy: Never
containers:
- name: wget
image: {{ .Values.tests.image | quote }}
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-ec"]
args:
- wget -q -T 10 -O /dev/null "http://{{ include "desktop.fullname" . }}:{{ .Values.service.port }}{{ .Values.tests.path }}"
49 changes: 32 additions & 17 deletions frontend/desktop/deploy/charts/desktop-frontend/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,23 +101,6 @@ desktopConfig:
# End of auto-configured values
# ============================================================================

livenessProbe:
httpGet:
path: /api/platform/getAppConfig
port: 3000
initialDelaySeconds: 15
periodSeconds: 20

readinessProbe:
httpGet:
path: /api/platform/getAppConfig
port: 3000
initialDelaySeconds: 5
periodSeconds: 3
timeoutSeconds: 2
successThreshold: 3
failureThreshold: 3

# Ingress configuration
ingress:
enabled: true
Expand All @@ -134,3 +117,35 @@ nodeSelector: {}
tolerations: []

affinity: {}

# App-local health checks; override these objects with Helm -f/--set.
# Startup protects initialization; readiness gates traffic; liveness restarts hung containers.
startupProbe:
httpGet:
path: /healthz
port: 3000
periodSeconds: 2
timeoutSeconds: 3
failureThreshold: 60

readinessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 5
periodSeconds: 3
timeoutSeconds: 2
successThreshold: 3
failureThreshold: 3

livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 15
periodSeconds: 20

# helm test uses the same app-local health endpoint.
tests:
image: busybox:1.36.1
path: /healthz
56 changes: 56 additions & 0 deletions frontend/desktop/src/pages/healthz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
import { assertReady, HEALTHZ_SERVICE } from '../utils/healthz';

export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
res.setHeader('Cache-Control', 'no-store');

if (req.method !== 'GET') {
if (req.method === 'HEAD') {
return sendHealthz(res, true);
}

res.setHeader('Allow', 'GET, HEAD');
sendJson(res, 405, { error: 'Method Not Allowed' });
return { props: {} };
}

return sendHealthz(res);
};

function sendHealthz(res: GetServerSidePropsContext['res'], head = false) {
try {
assertReady();
} catch (error) {
console.error('[healthz] %s is not ready', HEALTHZ_SERVICE, error);
if (head) {
res.statusCode = 503;
res.end();
} else {
sendJson(res, 503, { status: 'error', service: HEALTHZ_SERVICE });
}
return { props: {} };
}

if (head) {
res.statusCode = 200;
res.end();
} else {
sendJson(res, 200, { status: 'ok', service: HEALTHZ_SERVICE });
}

return { props: {} };
}

function sendJson(
res: GetServerSidePropsContext['res'],
statusCode: number,
body: Record<string, string>
) {
res.statusCode = statusCode;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(body));
}

export default function Healthz() {
return null;
}
56 changes: 56 additions & 0 deletions frontend/desktop/src/utils/healthz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { readFileSync } from 'fs';
import yaml from 'js-yaml';
type ConfigRecord = Record<string, unknown>;

export const HEALTHZ_SERVICE = 'desktop';

export function assertReady() {
const config = loadConfig();
const cloud = readRecord(config, 'cloud');
const database = readRecord(config, 'database');
const desktop = readRecord(config, 'desktop');
const auth = readRecord(desktop, 'auth');
const jwt = readRecord(auth, 'jwt');

readNonEmptyString(cloud, 'domain');
readNonEmptyString(cloud, 'regionUID');
readNonEmptyString(database, 'globalCockroachdbURI');
readNonEmptyString(database, 'regionalCockroachdbURI');
readNonEmptyString(jwt, 'internal');
readNonEmptyString(jwt, 'regional');
readNonEmptyString(jwt, 'global');
}

function loadConfig(): ConfigRecord {
const filename =
process.env.NODE_ENV === 'development'
? process.env.CONFIG_PATH || 'data/config.local.yaml'
: '/app/data/config.yaml';
const config = yaml.load(readFileSync(filename, 'utf-8'));

if (!isRecord(config)) {
throw new Error(`${filename} is not a YAML object`);
}

return config;
}

function readRecord(record: ConfigRecord, key: string): ConfigRecord {
const value = record[key];
if (!isRecord(value)) {
throw new Error(`${key} is not configured`);
}
return value;
}

function readNonEmptyString(record: ConfigRecord, key: string): string {
const value = record[key];
if (typeof value !== 'string' || value.trim() === '') {
throw new Error(`${key} is not configured`);
}
return value;
}

function isRecord(value: unknown): value is ConfigRecord {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,34 @@ costcenterConfig:
components:
accountService:
url: "http://account-service.account-system.svc:2333"

# App-local health checks; override these objects with Helm -f/--set.
# Startup protects initialization; readiness gates traffic; liveness restarts hung containers.
startupProbe:
httpGet:
path: /healthz
port: 3000
periodSeconds: 2
timeoutSeconds: 3
failureThreshold: 30

readinessProbe:
httpGet:
path: /healthz
port: 3000
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6

livenessProbe:
httpGet:
path: /healthz
port: 3000
periodSeconds: 20
timeoutSeconds: 3
failureThreshold: 3

# helm test uses the same app-local health endpoint.
tests:
image: busybox:1.36.1
path: /healthz
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,18 @@ spec:
capabilities:
drop:
- "ALL"
startupProbe:
{{- toYaml .Values.startupProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
image: "{{ .Values.image }}"
imagePullPolicy: {{ .Values.imagePullPolicy }}
volumeMounts:
- name: costcenter-frontend-volume
mountPath: /app/data/config.yaml
subPath: config.yaml
readinessProbe:
httpGet:
path: /api/platform/getAppConfig
port: {{ .Values.service.port }}
initialDelaySeconds: 5
periodSeconds: 3
timeoutSeconds: 2
successThreshold: 3
failureThreshold: 3
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Pod
metadata:
name: {{ include "costcenter-frontend.fullname" . }}-test-connection
namespace: {{ .Release.Namespace }}
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": before-hook-creation
spec:
restartPolicy: Never
containers:
- name: wget
image: {{ .Values.tests.image | quote }}
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-ec"]
args:
- wget -q -T 10 -O /dev/null "http://{{ include "costcenter-frontend.fullname" . }}:{{ .Values.service.port }}{{ .Values.tests.path }}"
Loading
Loading