From f3833a9fb0c2d273e2a2c3ba96de2870da70d7d4 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Tue, 6 Jan 2026 16:22:25 +0000 Subject: [PATCH 1/9] feat: add HTTP health check configuration for applications (kubernetes probes) --- .../20260106154856_migration/migration.sql | 42 +++ prisma/schema.prisma | 8 + .../project/app/[appId]/advanced/actions.ts | 41 +++ .../advanced/health-check-settings.tsx | 286 ++++++++++++++++++ .../[appId]/advanced/health-check.model.ts | 17 ++ src/app/project/app/[appId]/app-tabs.tsx | 2 + src/server/services/deployment.service.ts | 22 +- src/shared/model/generated-zod/app.ts | 6 + 8 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20260106154856_migration/migration.sql create mode 100644 src/app/project/app/[appId]/advanced/health-check-settings.tsx create mode 100644 src/app/project/app/[appId]/advanced/health-check.model.ts diff --git a/prisma/migrations/20260106154856_migration/migration.sql b/prisma/migrations/20260106154856_migration/migration.sql new file mode 100644 index 00000000..5032b798 --- /dev/null +++ b/prisma/migrations/20260106154856_migration/migration.sql @@ -0,0 +1,42 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_App" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "appType" TEXT NOT NULL DEFAULT 'APP', + "projectId" TEXT NOT NULL, + "sourceType" TEXT NOT NULL DEFAULT 'GIT', + "containerImageSource" TEXT, + "containerRegistryUsername" TEXT, + "containerRegistryPassword" TEXT, + "gitUrl" TEXT, + "gitBranch" TEXT, + "gitUsername" TEXT, + "gitToken" TEXT, + "dockerfilePath" TEXT NOT NULL DEFAULT './Dockerfile', + "replicas" INTEGER NOT NULL DEFAULT 1, + "envVars" TEXT NOT NULL DEFAULT '', + "memoryReservation" INTEGER, + "memoryLimit" INTEGER, + "cpuReservation" INTEGER, + "cpuLimit" INTEGER, + "webhookId" TEXT, + "ingressNetworkPolicy" TEXT NOT NULL DEFAULT 'ALLOW_ALL', + "egressNetworkPolicy" TEXT NOT NULL DEFAULT 'ALLOW_ALL', + "useNetworkPolicy" BOOLEAN NOT NULL DEFAULT true, + "healthChechHttpGetPath" TEXT, + "healthCheckHttpScheme" TEXT, + "healthCheckHttpHeadersJson" TEXT, + "healthCheckHttpPort" INTEGER, + "healthCheckPeriodSeconds" INTEGER NOT NULL DEFAULT 10, + "healthCheckTimeoutSeconds" INTEGER NOT NULL DEFAULT 5, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "App_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_App" ("appType", "containerImageSource", "containerRegistryPassword", "containerRegistryUsername", "cpuLimit", "cpuReservation", "createdAt", "dockerfilePath", "egressNetworkPolicy", "envVars", "gitBranch", "gitToken", "gitUrl", "gitUsername", "id", "ingressNetworkPolicy", "memoryLimit", "memoryReservation", "name", "projectId", "replicas", "sourceType", "updatedAt", "useNetworkPolicy", "webhookId") SELECT "appType", "containerImageSource", "containerRegistryPassword", "containerRegistryUsername", "cpuLimit", "cpuReservation", "createdAt", "dockerfilePath", "egressNetworkPolicy", "envVars", "gitBranch", "gitToken", "gitUrl", "gitUsername", "id", "ingressNetworkPolicy", "memoryLimit", "memoryReservation", "name", "projectId", "replicas", "sourceType", "updatedAt", "useNetworkPolicy", "webhookId" FROM "App"; +DROP TABLE "App"; +ALTER TABLE "new_App" RENAME TO "App"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e3a1264b..9e5d7728 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -204,6 +204,14 @@ model App { egressNetworkPolicy String @default("ALLOW_ALL") // ALLOW_ALL, NAMESPACE_ONLY, DENY_ALL, INTERNET_ONLY useNetworkPolicy Boolean @default(true) + // healthCheck startupProbe, readinessProbe, livenessProbe + healthChechHttpGetPath String? + healthCheckHttpScheme String? // HTTP, HTTPS + healthCheckHttpHeadersJson String? // JSON stringified key-value pairs + healthCheckHttpPort Int? + healthCheckPeriodSeconds Int @default(10) + healthCheckTimeoutSeconds Int @default(5) + appDomains AppDomain[] appPorts AppPort[] appVolumes AppVolume[] diff --git a/src/app/project/app/[appId]/advanced/actions.ts b/src/app/project/app/[appId]/advanced/actions.ts index 645d5eb0..d0c75957 100644 --- a/src/app/project/app/[appId]/advanced/actions.ts +++ b/src/app/project/app/[appId]/advanced/actions.ts @@ -5,6 +5,7 @@ import appService from "@/server/services/app.service"; import { isAuthorizedWriteForApp, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils"; import { BasicAuthEditModel, basicAuthEditZodModel } from "@/shared/model/basic-auth-edit.model"; import { appNetworkPolicy } from "@/shared/model/network-policy.model"; +import { HealthCheckModel, healthCheckZodModel } from "./health-check.model"; export const saveBasicAuth = async (prevState: any, inputData: BasicAuthEditModel) => @@ -43,3 +44,43 @@ export const saveNetworkPolicy = async (appId: string, ingressPolicy: string, eg }); return new SuccessActionResult(undefined, 'Network policy saved'); }); + +export const saveHealthCheck = async (prevState: any, inputData: HealthCheckModel) => + saveFormAction(inputData, healthCheckZodModel, async (validatedData) => { + await isAuthorizedWriteForApp(validatedData.appId); + + const app = await appService.getById(validatedData.appId); + + // Prepare update data + let updateData: Partial = { + healthCheckPeriodSeconds: validatedData.periodSeconds ?? 10, + healthCheckTimeoutSeconds: validatedData.timeoutSeconds ?? 5, + }; + + if (validatedData.enabled) { + updateData = { + ...updateData, + healthChechHttpGetPath: validatedData.path || null, + healthCheckHttpPort: validatedData.port || null, + healthCheckHttpScheme: validatedData.scheme || null, + healthCheckHttpHeadersJson: validatedData.headers && validatedData.headers.length > 0 + ? JSON.stringify(validatedData.headers) + : null + }; + } else { + updateData = { + ...updateData, + healthChechHttpGetPath: null, + healthCheckHttpPort: null, + healthCheckHttpScheme: null, + healthCheckHttpHeadersJson: null + }; + } + + await appService.save({ + ...app, + ...updateData + }); + + return new SuccessActionResult(undefined, 'Health check settings saved'); + }); diff --git a/src/app/project/app/[appId]/advanced/health-check-settings.tsx b/src/app/project/app/[appId]/advanced/health-check-settings.tsx new file mode 100644 index 00000000..1e2803c9 --- /dev/null +++ b/src/app/project/app/[appId]/advanced/health-check-settings.tsx @@ -0,0 +1,286 @@ +'use client' + +import { AppExtendedModel } from "@/shared/model/app-extended.model"; +import { useForm, useFieldArray } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"; +import { Form, FormField, FormItem, FormControl, FormMessage, FormLabel } from "@/components/ui/form"; +import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Trash, Plus } from "lucide-react"; +import FormLabelWithQuestion from "@/components/custom/form-label-with-question"; +import { useFormState } from "react-dom"; +import { saveHealthCheck } from "./actions"; +import { useEffect } from "react"; +import { toast } from "sonner"; +import { SubmitButton } from "@/components/custom/submit-button"; +import { FormUtils } from "@/frontend/utils/form.utilts"; +import { HealthCheckModel, healthCheckZodModel } from "./health-check.model"; +import { ServerActionResult } from "@/shared/model/server-action-error-return.model"; + +export default function HealthCheckSettings({ app, readonly }: { app: AppExtendedModel, readonly: boolean }) { + + const defaultHeaders = app.healthCheckHttpHeadersJson + ? JSON.parse(app.healthCheckHttpHeadersJson) + : []; + + const isEnabled = !!(app.healthChechHttpGetPath); + + const defaultValues: HealthCheckModel = { + appId: app.id, + enabled: isEnabled, + path: app.healthChechHttpGetPath || undefined, + port: app.healthCheckHttpPort || undefined, + scheme: (app.healthCheckHttpScheme as "HTTP" | "HTTPS") || "HTTP", + periodSeconds: app.healthCheckPeriodSeconds ?? 15, + timeoutSeconds: app.healthCheckTimeoutSeconds ?? 5, + headers: defaultHeaders + }; + + const form = useForm({ + resolver: zodResolver(healthCheckZodModel), + defaultValues, + disabled: readonly, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "headers" + }); + + const enabled = form.watch("enabled"); + + const [state, formAction] = useFormState( + (state: ServerActionResult, payload: HealthCheckModel) => saveHealthCheck(state, payload), + FormUtils.getInitialFormState() + ); + + useEffect(() => { + if (state.status === 'success') { + toast.success('Health Check Settings Saved'); + } + FormUtils.mapValidationErrorsToForm(state, form); + }, [state]); + + return ( + + + Health Check Settings + + Configure healthchecks so that k3s can automatically monitor when your application is fully started up and ready to receive traffic (In kubernetes terms, startup, readiness and liveness probes). + + +
+ form.handleSubmit((data) => { + formAction(data); + })()}> + + ( + +
+ Enable Health Check +
+ + + +
+ )} + /> + + {enabled && ( + <> +
+ ( + + + HTTP Path + + + + + + + )} + /> + ( + + + HTTP Port + + + field.onChange(e.target.value)} /> + + + + )} + /> + ( + + +

Scheme to use for connecting to the container. Defaults to HTTP.

+

Possible enum values:

+
    +
  • "HTTP" means that the scheme used will be http://
  • +
  • "HTTPS" means that the scheme used will be https://
  • +
+
+ }> + HTTP Scheme + + + + + )} + /> + + +
+ +

Custom headers to set in the request. HTTP allows repeated headers.

+

HTTPHeader describes a custom header to be used in HTTP probes

+
+ }> + HTTP Headers + +
+ {fields.map((item, index) => ( +
+ ( + + {index === 0 &&
+ Name + + {''} + +
} + + + + +
+ )} + /> + ( + + {index === 0 &&
+ Value + + {''} + +
} + + + + +
+ )} + /> + +
+ ))} + +
+ + +
+ ( + + + Check Interval (periodSeconds) + + + field.onChange(e.target.value)} /> + + + + )} + /> + ( + + +

Number of seconds to wait for a HTTP request to complete before timing out. Minimum value is 1.

+
+ }> + Check Timeout (timeoutSeconds) + + + field.onChange(e.target.value)} /> + + + + )} + /> + + + + )} +
+ + Save + +
+ +
+ ); +} diff --git a/src/app/project/app/[appId]/advanced/health-check.model.ts b/src/app/project/app/[appId]/advanced/health-check.model.ts new file mode 100644 index 00000000..5be06e56 --- /dev/null +++ b/src/app/project/app/[appId]/advanced/health-check.model.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const healthCheckZodModel = z.object({ + appId: z.string(), + enabled: z.boolean(), + path: z.string().optional(), // If enabled is true, this might be required in reality, but we'll let user save empty + port: z.coerce.number().int().min(1).max(65535).optional(), + scheme: z.enum(["HTTP", "HTTPS"]).optional(), + periodSeconds: z.coerce.number().int().min(1).default(10), + timeoutSeconds: z.coerce.number().int().min(1).default(5), + headers: z.array(z.object({ + name: z.string().min(1, "Name is required"), + value: z.string().min(1, "Value is required") + })).optional() +}); + +export type HealthCheckModel = z.infer; diff --git a/src/app/project/app/[appId]/app-tabs.tsx b/src/app/project/app/[appId]/app-tabs.tsx index 4ab0d0dc..a65dbcae 100644 --- a/src/app/project/app/[appId]/app-tabs.tsx +++ b/src/app/project/app/[appId]/app-tabs.tsx @@ -20,6 +20,7 @@ import VolumeBackupList from "./volumes/volume-backup"; import { VolumeBackupExtendedModel } from "@/shared/model/volume-backup-extended.model"; import BasicAuth from "./advanced/basic-auth"; import NetworkPolicy from "./advanced/network-policy"; +import HealthCheckSettings from "./advanced/health-check-settings"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import DbToolsCard from "./credentials/db-tools"; import { RolePermissionEnum } from "@/shared/model/role-extended.model.ts"; @@ -94,6 +95,7 @@ export default function AppTabs({ + ) diff --git a/src/server/services/deployment.service.ts b/src/server/services/deployment.service.ts index f72144c1..ec949405 100644 --- a/src/server/services/deployment.service.ts +++ b/src/server/services/deployment.service.ts @@ -1,6 +1,6 @@ import { AppExtendedModel } from "@/shared/model/app-extended.model"; import k3s from "../adapter/kubernetes-api.adapter"; -import { V1Deployment, V1ReplicaSet } from "@kubernetes/client-node"; +import { V1Deployment, V1ReplicaSet, V1Probe } from "@kubernetes/client-node"; import buildService from "./build.service"; import { ListUtils } from "../../shared/utils/list.utils"; import { DeploymentInfoModel, DeploymentStatus } from "@/shared/model/deployment-info.model"; @@ -182,6 +182,26 @@ class DeploymentService { } } + if (app.healthChechHttpGetPath) { + const probe: V1Probe = { + httpGet: { + path: app.healthChechHttpGetPath, + port: app.healthCheckHttpPort ?? 80, + scheme: app.healthCheckHttpScheme ?? undefined, + ...(app.healthCheckHttpHeadersJson ? { httpHeaders: JSON.parse(app.healthCheckHttpHeadersJson) } : {}) + }, + periodSeconds: app.healthCheckPeriodSeconds, + timeoutSeconds: app.healthCheckTimeoutSeconds + }; + // waits until pod is started and before that the other probes are not startet + body.spec!.template!.spec!.containers[0].startupProbe = { ...probe, failureThreshold: 20 }; // allow failures before marking pod as failed --> back off + // checks if traffic can be routed to this pod or not + body.spec!.template!.spec!.containers[0].readinessProbe = { ...probe }; + // checks if pod is still alive and if not restarts it + body.spec!.template!.spec!.containers[0].livenessProbe = { ...probe }; + dlog(deploymentId, `Configured Health Checks.`); + } + const dockerPullSecretName = await secretService.createOrUpdateDockerPullSecret(app); if (dockerPullSecretName) { dlog(deploymentId, `Configured credentials to pull Docker Image (${dockerPullSecretName})`); diff --git a/src/shared/model/generated-zod/app.ts b/src/shared/model/generated-zod/app.ts index b5cac275..8e60ddfd 100644 --- a/src/shared/model/generated-zod/app.ts +++ b/src/shared/model/generated-zod/app.ts @@ -26,6 +26,12 @@ export const AppModel = z.object({ ingressNetworkPolicy: z.string(), egressNetworkPolicy: z.string(), useNetworkPolicy: z.boolean(), + healthChechHttpGetPath: z.string().nullish(), + healthCheckHttpScheme: z.string().nullish(), + healthCheckHttpHeadersJson: z.string().nullish(), + healthCheckHttpPort: z.number().int().nullish(), + healthCheckPeriodSeconds: z.number().int(), + healthCheckTimeoutSeconds: z.number().int(), createdAt: z.date(), updatedAt: z.date(), }) From 02f84eb7a86c121b9a4d3de94d86edaf61f477f1 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Tue, 6 Jan 2026 16:30:06 +0000 Subject: [PATCH 2/9] fix: add health check configuration templates --- src/shared/templates/apps/wordpress.template.ts | 4 ++++ src/shared/templates/databases/mariadb.template.ts | 2 ++ src/shared/templates/databases/mongodb.template.ts | 2 ++ src/shared/templates/databases/mysql.template.ts | 2 ++ src/shared/templates/databases/postgres.template.ts | 2 ++ 5 files changed, 12 insertions(+) diff --git a/src/shared/templates/apps/wordpress.template.ts b/src/shared/templates/apps/wordpress.template.ts index 36120407..70ae98ef 100644 --- a/src/shared/templates/apps/wordpress.template.ts +++ b/src/shared/templates/apps/wordpress.template.ts @@ -42,6 +42,8 @@ export const wordpressAppTemplate: AppTemplateModel = { MYSQL_USER=wordpress `, useNetworkPolicy: true, + healthCheckPeriodSeconds: 15, + healthCheckTimeoutSeconds: 5, }, appDomains: [], appVolumes: [{ @@ -81,6 +83,8 @@ WORDPRESS_DB_PASSWORD={password} WORDPRESS_TABLE_PREFIX=wp_ `, useNetworkPolicy: true, + healthCheckPeriodSeconds: 15, + healthCheckTimeoutSeconds: 5, }, appDomains: [], appVolumes: [{ diff --git a/src/shared/templates/databases/mariadb.template.ts b/src/shared/templates/databases/mariadb.template.ts index f5dd8ded..9f21e623 100644 --- a/src/shared/templates/databases/mariadb.template.ts +++ b/src/shared/templates/databases/mariadb.template.ts @@ -52,6 +52,8 @@ export const mariadbAppTemplate: AppTemplateModel = { replicas: 1, envVars: ``, useNetworkPolicy: true, + healthCheckPeriodSeconds: 15, + healthCheckTimeoutSeconds: 5, }, appDomains: [], appVolumes: [{ diff --git a/src/shared/templates/databases/mongodb.template.ts b/src/shared/templates/databases/mongodb.template.ts index 4449aa32..c4c11615 100644 --- a/src/shared/templates/databases/mongodb.template.ts +++ b/src/shared/templates/databases/mongodb.template.ts @@ -45,6 +45,8 @@ export const mongodbAppTemplate: AppTemplateModel = { replicas: 1, envVars: ``, useNetworkPolicy: true, + healthCheckPeriodSeconds: 15, + healthCheckTimeoutSeconds: 5, }, appDomains: [], appVolumes: [{ diff --git a/src/shared/templates/databases/mysql.template.ts b/src/shared/templates/databases/mysql.template.ts index a2830d9a..c863c87e 100644 --- a/src/shared/templates/databases/mysql.template.ts +++ b/src/shared/templates/databases/mysql.template.ts @@ -52,6 +52,8 @@ export const mysqlAppTemplate: AppTemplateModel = { ingressNetworkPolicy: Constants.DEFAULT_INGRESS_NETWORK_POLICY_DATABASES, egressNetworkPolicy: Constants.DEFAULT_EGRESS_NETWORK_POLICY_DATABASES, useNetworkPolicy: true, + healthCheckPeriodSeconds: 15, + healthCheckTimeoutSeconds: 5, }, appDomains: [], appVolumes: [{ diff --git a/src/shared/templates/databases/postgres.template.ts b/src/shared/templates/databases/postgres.template.ts index b300b9ea..ac20f851 100644 --- a/src/shared/templates/databases/postgres.template.ts +++ b/src/shared/templates/databases/postgres.template.ts @@ -46,6 +46,8 @@ export const postgreAppTemplate: AppTemplateModel = { envVars: `PGDATA=/var/lib/qs-postgres/data `, useNetworkPolicy: true, + healthCheckPeriodSeconds: 15, + healthCheckTimeoutSeconds: 5, }, appDomains: [], appVolumes: [{ From 82077d3b96cdcf8c70dc2871fcaa129757e6f093 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Wed, 7 Jan 2026 07:13:55 +0000 Subject: [PATCH 3/9] fix: enhance startup probe configuration for deployments --- src/server/services/deployment.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/server/services/deployment.service.ts b/src/server/services/deployment.service.ts index ec949405..425dda84 100644 --- a/src/server/services/deployment.service.ts +++ b/src/server/services/deployment.service.ts @@ -194,7 +194,13 @@ class DeploymentService { timeoutSeconds: app.healthCheckTimeoutSeconds }; // waits until pod is started and before that the other probes are not startet - body.spec!.template!.spec!.containers[0].startupProbe = { ...probe, failureThreshold: 20 }; // allow failures before marking pod as failed --> back off + body.spec!.template!.spec!.containers[0].startupProbe = { + ...probe, + periodSeconds: 10, + failureThreshold: 30, + timeoutSeconds: 3, + }; // checking 5 minutes long if app is starting, after 5 minutes --> restart + // checks if traffic can be routed to this pod or not body.spec!.template!.spec!.containers[0].readinessProbe = { ...probe }; // checks if pod is still alive and if not restarts it From e8069e874ae70a69b1311ad8e35a916c77f44382 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Thu, 8 Jan 2026 16:08:56 +0100 Subject: [PATCH 4/9] refactor: getFirstMasterNode sorts nodes by name asc --- src/server/services/node.service.ts | 5 +++-- src/server/services/registry.service.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/server/services/node.service.ts b/src/server/services/node.service.ts index a5932729..0dbf4875 100644 --- a/src/server/services/node.service.ts +++ b/src/server/services/node.service.ts @@ -43,9 +43,10 @@ class ClusterService { })(); } - async getMasterNode(): Promise { + async getFirstMasterNode(): Promise { const nodes = await this.getNodeInfo(); - return nodes.find(node => node.isMasterNode)!; + nodes.sort((a, b) => a.name.localeCompare(b.name)); + return nodes.find(node => node.isMasterNode)!; // even on HA Cluster, only one node is returned } async setNodeStatus(nodeName: string, schedulable: boolean) { diff --git a/src/server/services/registry.service.ts b/src/server/services/registry.service.ts index 689ef8c8..d623ef7e 100644 --- a/src/server/services/registry.service.ts +++ b/src/server/services/registry.service.ts @@ -174,7 +174,7 @@ class RegistryService { const deploymentName = 'registry'; - const masterNode = await clusterService.getMasterNode(); + const masterNode = await clusterService.getFirstMasterNode(); if (useLocalStorage && !masterNode) { throw new ServiceException("Cannot deploy registry with local storage, because could not evaluate master node."); } From fc0efbf388d25b64afdace2db75dd760d5f322b3 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Thu, 8 Jan 2026 15:34:07 +0000 Subject: [PATCH 5/9] fix: update next dependency to version 14.2.35 --- package.json | 2 +- yarn.lock | 124 +++++++++++++++++++++++++-------------------------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index 64273ab7..31451281 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "dotenv": "^17.2.3", "lucide-react": "^0.465.0", "moment": "^2.30.1", - "next": "14.2.15", + "next": "14.2.35", "next-auth": "^4.24.8", "next-themes": "^0.3.0", "node-schedule": "^2.1.1", diff --git a/yarn.lock b/yarn.lock index 88bc0c86..2d0613d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1399,10 +1399,10 @@ resolved "https://registry.yarnpkg.com/@next-auth/prisma-adapter/-/prisma-adapter-1.0.7.tgz#86195e9cc5a23a9e71370c7e8745b630388143b1" integrity sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw== -"@next/env@14.2.15": - version "14.2.15" - resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.15.tgz#06d984e37e670d93ddd6790af1844aeb935f332f" - integrity sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ== +"@next/env@14.2.35": + version "14.2.35" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.35.tgz#e979016d0ca8500a47d41ffd02625fe29b8df35a" + integrity sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ== "@next/eslint-plugin-next@14.2.15": version "14.2.15" @@ -1411,50 +1411,50 @@ dependencies: glob "10.3.10" -"@next/swc-darwin-arm64@14.2.15": - version "14.2.15" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz#6386d585f39a1c490c60b72b1f76612ba4434347" - integrity sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA== - -"@next/swc-darwin-x64@14.2.15": - version "14.2.15" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz#b7baeedc6a28f7545ad2bc55adbab25f7b45cb89" - integrity sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg== - -"@next/swc-linux-arm64-gnu@14.2.15": - version "14.2.15" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz#fa13c59d3222f70fb4cb3544ac750db2c6e34d02" - integrity sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw== - -"@next/swc-linux-arm64-musl@14.2.15": - version "14.2.15" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz#30e45b71831d9a6d6d18d7ac7d611a8d646a17f9" - integrity sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ== - -"@next/swc-linux-x64-gnu@14.2.15": - version "14.2.15" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz#5065db17fc86f935ad117483f21f812dc1b39254" - integrity sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA== - -"@next/swc-linux-x64-musl@14.2.15": - version "14.2.15" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz#3c4a4568d8be7373a820f7576cf33388b5dab47e" - integrity sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ== - -"@next/swc-win32-arm64-msvc@14.2.15": - version "14.2.15" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz#fb812cc4ca0042868e32a6a021da91943bb08b98" - integrity sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g== - -"@next/swc-win32-ia32-msvc@14.2.15": - version "14.2.15" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz#ec26e6169354f8ced240c1427be7fd485c5df898" - integrity sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ== - -"@next/swc-win32-x64-msvc@14.2.15": - version "14.2.15" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz#18d68697002b282006771f8d92d79ade9efd35c4" - integrity sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g== +"@next/swc-darwin-arm64@14.2.33": + version "14.2.33" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz#9e74a4223f1e5e39ca4f9f85709e0d95b869b298" + integrity sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA== + +"@next/swc-darwin-x64@14.2.33": + version "14.2.33" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz#fcf0c45938da9b0cc2ec86357d6aefca90bd17f3" + integrity sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA== + +"@next/swc-linux-arm64-gnu@14.2.33": + version "14.2.33" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz#837f91a740eb4420c06f34c4677645315479d9be" + integrity sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw== + +"@next/swc-linux-arm64-musl@14.2.33": + version "14.2.33" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz#dc8903469e5c887b25e3c2217a048bd30c58d3d4" + integrity sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg== + +"@next/swc-linux-x64-gnu@14.2.33": + version "14.2.33" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz#344438be592b6b28cc540194274561e41f9933e5" + integrity sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg== + +"@next/swc-linux-x64-musl@14.2.33": + version "14.2.33" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz#3379fad5e0181000b2a4fac0b80f7ca4ffe795c8" + integrity sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA== + +"@next/swc-win32-arm64-msvc@14.2.33": + version "14.2.33" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz#bca8f4dde34656aef8e99f1e5696de255c2f00e5" + integrity sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ== + +"@next/swc-win32-ia32-msvc@14.2.33": + version "14.2.33" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz#a69c581483ea51dd3b8907ce33bb101fe07ec1df" + integrity sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q== + +"@next/swc-win32-x64-msvc@14.2.33": + version "14.2.33" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz#f1a40062530c17c35a86d8c430b3ae465eb7cea1" + integrity sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg== "@noble/hashes@1.5.0": version "1.5.0" @@ -6898,12 +6898,12 @@ next-themes@^0.3.0: resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a" integrity sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w== -next@14.2.15: - version "14.2.15" - resolved "https://registry.yarnpkg.com/next/-/next-14.2.15.tgz#348e5603e22649775d19c785c09a89c9acb5189a" - integrity sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw== +next@14.2.35: + version "14.2.35" + resolved "https://registry.yarnpkg.com/next/-/next-14.2.35.tgz#7c68873a15fe5a19401f2f993fea535be3366ee9" + integrity sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig== dependencies: - "@next/env" "14.2.15" + "@next/env" "14.2.35" "@swc/helpers" "0.5.5" busboy "1.6.0" caniuse-lite "^1.0.30001579" @@ -6911,15 +6911,15 @@ next@14.2.15: postcss "8.4.31" styled-jsx "5.1.1" optionalDependencies: - "@next/swc-darwin-arm64" "14.2.15" - "@next/swc-darwin-x64" "14.2.15" - "@next/swc-linux-arm64-gnu" "14.2.15" - "@next/swc-linux-arm64-musl" "14.2.15" - "@next/swc-linux-x64-gnu" "14.2.15" - "@next/swc-linux-x64-musl" "14.2.15" - "@next/swc-win32-arm64-msvc" "14.2.15" - "@next/swc-win32-ia32-msvc" "14.2.15" - "@next/swc-win32-x64-msvc" "14.2.15" + "@next/swc-darwin-arm64" "14.2.33" + "@next/swc-darwin-x64" "14.2.33" + "@next/swc-linux-arm64-gnu" "14.2.33" + "@next/swc-linux-arm64-musl" "14.2.33" + "@next/swc-linux-x64-gnu" "14.2.33" + "@next/swc-linux-x64-musl" "14.2.33" + "@next/swc-win32-arm64-msvc" "14.2.33" + "@next/swc-win32-ia32-msvc" "14.2.33" + "@next/swc-win32-x64-msvc" "14.2.33" node-abi@^3.3.0: version "3.85.0" From 4e44574a62b0ac54bed9a472f1da7c2d2f5f437f Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Thu, 8 Jan 2026 16:41:45 +0000 Subject: [PATCH 6/9] feat: add health check icon in project-network-graph --- .../[projectId]/project-network-graph.tsx | 24 ++++++++++++++++--- .../advanced/health-check-settings.tsx | 2 +- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/app/project/[projectId]/project-network-graph.tsx b/src/app/project/[projectId]/project-network-graph.tsx index 97943de6..497fce2b 100644 --- a/src/app/project/[projectId]/project-network-graph.tsx +++ b/src/app/project/[projectId]/project-network-graph.tsx @@ -4,9 +4,10 @@ import React, { useMemo } from 'react'; import { ReactFlow, Background, Controls, Node, Edge, MarkerType, Handle, Position } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { App, AppDomain, AppPort } from '@prisma/client'; -import { Globe, Network, Lock, Cloud, Shield, ArrowDown } from 'lucide-react'; +import { Globe, Network, Lock, Cloud, Shield, ArrowDown, HeartPulse } from 'lucide-react'; import PodStatusIndicator from '@/components/custom/pod-status-indicator'; import { useRouter } from 'next/navigation'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; interface AppWithRelations extends App { appPorts: AppPort[]; @@ -58,7 +59,14 @@ const PolicyIcon = ({ policy, type, ports, useNetworkPolicy }: { policy: string, ); }; -const AppNode = ({ data }: { data: any }) => { +const AppNode = ({ data }: { data: { + label: string; + ingressPolicy: string; + egressPolicy: string; + appId: string; + app: AppWithRelations; + ports: string; +} }) => { return (
@@ -67,8 +75,18 @@ const AppNode = ({ data }: { data: any }) => {
-
+

{data.label}

+ {data.app.healthChechHttpGetPath && + + + + + +

Healthchecks enabled for this App

+
+
+
}
diff --git a/src/app/project/app/[appId]/advanced/health-check-settings.tsx b/src/app/project/app/[appId]/advanced/health-check-settings.tsx index 1e2803c9..f2c9c8aa 100644 --- a/src/app/project/app/[appId]/advanced/health-check-settings.tsx +++ b/src/app/project/app/[appId]/advanced/health-check-settings.tsx @@ -242,7 +242,7 @@ export default function HealthCheckSettings({ app, readonly }: { app: AppExtende name="periodSeconds" render={({ field }) => ( - + Check Interval (periodSeconds) From ca4a25d35bfaaa79e8d763b289b5f1ca0bc5ef0c Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Thu, 8 Jan 2026 17:04:41 +0000 Subject: [PATCH 7/9] feat: add TCP health check configuration for applications --- .../20260108164941_migration/migration.sql | 2 + prisma/schema.prisma | 2 + .../project/app/[appId]/advanced/actions.ts | 35 ++- .../advanced/health-check-settings.tsx | 296 ++++++++++-------- .../[appId]/advanced/health-check.model.ts | 15 +- src/server/services/deployment.service.ts | 45 ++- src/shared/model/generated-zod/app.ts | 1 + 7 files changed, 237 insertions(+), 159 deletions(-) create mode 100644 prisma/migrations/20260108164941_migration/migration.sql diff --git a/prisma/migrations/20260108164941_migration/migration.sql b/prisma/migrations/20260108164941_migration/migration.sql new file mode 100644 index 00000000..90c92f46 --- /dev/null +++ b/prisma/migrations/20260108164941_migration/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "App" ADD COLUMN "healthCheckTcpPort" INTEGER; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9e5d7728..784490b7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -212,6 +212,8 @@ model App { healthCheckPeriodSeconds Int @default(10) healthCheckTimeoutSeconds Int @default(5) + healthCheckTcpPort Int? + appDomains AppDomain[] appPorts AppPort[] appVolumes AppVolume[] diff --git a/src/app/project/app/[appId]/advanced/actions.ts b/src/app/project/app/[appId]/advanced/actions.ts index d0c75957..bd753d85 100644 --- a/src/app/project/app/[appId]/advanced/actions.ts +++ b/src/app/project/app/[appId]/advanced/actions.ts @@ -58,22 +58,37 @@ export const saveHealthCheck = async (prevState: any, inputData: HealthCheckMode }; if (validatedData.enabled) { - updateData = { - ...updateData, - healthChechHttpGetPath: validatedData.path || null, - healthCheckHttpPort: validatedData.port || null, - healthCheckHttpScheme: validatedData.scheme || null, - healthCheckHttpHeadersJson: validatedData.headers && validatedData.headers.length > 0 - ? JSON.stringify(validatedData.headers) - : null - }; + if (validatedData.probeType === 'HTTP') { + updateData = { + ...updateData, + healthChechHttpGetPath: validatedData.path || null, + healthCheckHttpPort: validatedData.httpPort || null, + healthCheckHttpScheme: validatedData.scheme || null, + healthCheckHttpHeadersJson: validatedData.headers && validatedData.headers.length > 0 + ? JSON.stringify(validatedData.headers) + : null, + healthCheckTcpPort: null // Clear TCP when using HTTP + }; + } else if (validatedData.probeType === 'TCP') { + updateData = { + ...updateData, + healthCheckTcpPort: validatedData.tcpPort || null, + // Clear HTTP fields when using TCP + healthChechHttpGetPath: null, + healthCheckHttpPort: null, + healthCheckHttpScheme: null, + healthCheckHttpHeadersJson: null + }; + } } else { + // Clear all probe fields when disabled updateData = { ...updateData, healthChechHttpGetPath: null, healthCheckHttpPort: null, healthCheckHttpScheme: null, - healthCheckHttpHeadersJson: null + healthCheckHttpHeadersJson: null, + healthCheckTcpPort: null }; } diff --git a/src/app/project/app/[appId]/advanced/health-check-settings.tsx b/src/app/project/app/[appId]/advanced/health-check-settings.tsx index f2c9c8aa..6c4caf1d 100644 --- a/src/app/project/app/[appId]/advanced/health-check-settings.tsx +++ b/src/app/project/app/[appId]/advanced/health-check-settings.tsx @@ -9,6 +9,7 @@ import { Switch } from "@/components/ui/switch"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Trash, Plus } from "lucide-react"; import FormLabelWithQuestion from "@/components/custom/form-label-with-question"; import { useFormState } from "react-dom"; @@ -26,17 +27,20 @@ export default function HealthCheckSettings({ app, readonly }: { app: AppExtende ? JSON.parse(app.healthCheckHttpHeadersJson) : []; - const isEnabled = !!(app.healthChechHttpGetPath); + const isEnabled = !!(app.healthChechHttpGetPath || app.healthCheckTcpPort); + const probeType = app.healthChechHttpGetPath ? "HTTP" : app.healthCheckTcpPort ? "TCP" : "HTTP"; const defaultValues: HealthCheckModel = { appId: app.id, enabled: isEnabled, + probeType: probeType as "HTTP" | "TCP", path: app.healthChechHttpGetPath || undefined, - port: app.healthCheckHttpPort || undefined, + httpPort: app.healthCheckHttpPort || undefined, scheme: (app.healthCheckHttpScheme as "HTTP" | "HTTPS") || "HTTP", periodSeconds: app.healthCheckPeriodSeconds ?? 15, timeoutSeconds: app.healthCheckTimeoutSeconds ?? 5, - headers: defaultHeaders + headers: defaultHeaders, + tcpPort: app.healthCheckTcpPort || undefined, }; const form = useForm({ @@ -51,6 +55,7 @@ export default function HealthCheckSettings({ app, readonly }: { app: AppExtende }); const enabled = form.watch("enabled"); + const probeTypeWatch = form.watch("probeType"); const [state, formAction] = useFormState( (state: ServerActionResult, payload: HealthCheckModel) => saveHealthCheck(state, payload), @@ -98,143 +103,169 @@ export default function HealthCheckSettings({ app, readonly }: { app: AppExtende {enabled && ( <> -
- ( - - - HTTP Path - - - - - - - )} - /> - ( - - - HTTP Port - - - field.onChange(e.target.value)} /> - - - - )} - /> - ( - - -

Scheme to use for connecting to the container. Defaults to HTTP.

-

Possible enum values:

-
    -
  • "HTTP" means that the scheme used will be http://
  • -
  • "HTTPS" means that the scheme used will be https://
  • -
-
- }> - HTTP Scheme -
- - -
- )} - /> -
+ form.setValue('probeType', value as "HTTP" | "TCP")} className="w-full"> + + HTTP Probe + TCP Probe + -
- -

Custom headers to set in the request. HTTP allows repeated headers.

-

HTTPHeader describes a custom header to be used in HTTP probes

-
- }> - HTTP Headers - -
- {fields.map((item, index) => ( -
- ( - - {index === 0 &&
- Name - - {''} - -
} - - - - -
- )} - /> - ( - - {index === 0 &&
- Value - - {''} - -
} + +
+ ( + + + HTTP Path + + + + + + + )} + /> + ( + + + HTTP Port + + + field.onChange(e.target.value)} /> + + + + )} + /> + ( + + +

Scheme to use for connecting to the container. Defaults to HTTP.

+

Possible enum values:

+
    +
  • "HTTP" means that the scheme used will be http://
  • +
  • "HTTPS" means that the scheme used will be https://
  • +
+
+ }> + HTTP Scheme + + + + + - -
- )} - /> + + HTTP + HTTPS + + + + + )} + /> +
+ +
+ +

Custom headers to set in the request. HTTP allows repeated headers.

+
+ }> + HTTP Headers + +
+ {fields.map((item, index) => ( +
+ ( + + {index === 0 &&
+ Name + + {''} + +
} + + + + +
+ )} + /> + ( + + {index === 0 &&
+ Value + + {''} + +
} + + + + +
+ )} + /> + +
+ ))}
- ))} - -
-
+ + + + + ( + + + TCP Port + + + field.onChange(e.target.value)} /> + + + + )} + /> + +
-

Number of seconds to wait for a HTTP request to complete before timing out. Minimum value is 1.

+

Number of seconds to wait for a connection to complete before timing out. Minimum value is 1.

}> Check Timeout (timeoutSeconds) @@ -272,7 +303,6 @@ export default function HealthCheckSettings({ app, readonly }: { app: AppExtende )} /> - )} diff --git a/src/app/project/app/[appId]/advanced/health-check.model.ts b/src/app/project/app/[appId]/advanced/health-check.model.ts index 5be06e56..59b4e8e6 100644 --- a/src/app/project/app/[appId]/advanced/health-check.model.ts +++ b/src/app/project/app/[appId]/advanced/health-check.model.ts @@ -3,15 +3,20 @@ import { z } from "zod"; export const healthCheckZodModel = z.object({ appId: z.string(), enabled: z.boolean(), - path: z.string().optional(), // If enabled is true, this might be required in reality, but we'll let user save empty - port: z.coerce.number().int().min(1).max(65535).optional(), + probeType: z.enum(["HTTP", "TCP"]).default("HTTP"), + // HTTP probe fields + path: z.string().optional(), + httpPort: z.coerce.number().int().min(1).max(65535).optional(), scheme: z.enum(["HTTP", "HTTPS"]).optional(), - periodSeconds: z.coerce.number().int().min(1).default(10), - timeoutSeconds: z.coerce.number().int().min(1).default(5), headers: z.array(z.object({ name: z.string().min(1, "Name is required"), value: z.string().min(1, "Value is required") - })).optional() + })).optional(), + // TCP probe fields + tcpPort: z.coerce.number().int().min(1).max(65535).optional(), + // Common fields + periodSeconds: z.coerce.number().int().min(1).default(10), + timeoutSeconds: z.coerce.number().int().min(1).default(5), }); export type HealthCheckModel = z.infer; diff --git a/src/server/services/deployment.service.ts b/src/server/services/deployment.service.ts index 425dda84..0d3c768d 100644 --- a/src/server/services/deployment.service.ts +++ b/src/server/services/deployment.service.ts @@ -182,17 +182,40 @@ class DeploymentService { } } - if (app.healthChechHttpGetPath) { - const probe: V1Probe = { - httpGet: { - path: app.healthChechHttpGetPath, - port: app.healthCheckHttpPort ?? 80, - scheme: app.healthCheckHttpScheme ?? undefined, - ...(app.healthCheckHttpHeadersJson ? { httpHeaders: JSON.parse(app.healthCheckHttpHeadersJson) } : {}) - }, - periodSeconds: app.healthCheckPeriodSeconds, - timeoutSeconds: app.healthCheckTimeoutSeconds - }; + if (!!app.healthChechHttpGetPath || !!app.healthCheckTcpPort) { + let probe: V1Probe; + + // check if both probes are configured --> should not happen, but just in case + if (!!app.healthChechHttpGetPath && !!app.healthCheckTcpPort) { + dlog(deploymentId, `Warning: Both HTTP and TCP health checks are configured. Defaulting to HTTP health check.`); + throw new ServiceException("Both HTTP and TCP health checks are configured. Please configure only one type of health check."); + } + + if (app.healthChechHttpGetPath) { + // HTTP probe + probe = { + httpGet: { + path: app.healthChechHttpGetPath, + port: app.healthCheckHttpPort ?? 80, + scheme: app.healthCheckHttpScheme ?? undefined, + ...(app.healthCheckHttpHeadersJson ? { httpHeaders: JSON.parse(app.healthCheckHttpHeadersJson) } : {}) + }, + periodSeconds: app.healthCheckPeriodSeconds, + timeoutSeconds: app.healthCheckTimeoutSeconds + }; + dlog(deploymentId, `Configured HTTP Health Checks.`); + } else { + // TCP probe + probe = { + tcpSocket: { + port: app.healthCheckTcpPort! + }, + periodSeconds: app.healthCheckPeriodSeconds, + timeoutSeconds: app.healthCheckTimeoutSeconds + }; + dlog(deploymentId, `Configured TCP Health Checks.`); + } + // waits until pod is started and before that the other probes are not startet body.spec!.template!.spec!.containers[0].startupProbe = { ...probe, diff --git a/src/shared/model/generated-zod/app.ts b/src/shared/model/generated-zod/app.ts index 8e60ddfd..26485fb3 100644 --- a/src/shared/model/generated-zod/app.ts +++ b/src/shared/model/generated-zod/app.ts @@ -32,6 +32,7 @@ export const AppModel = z.object({ healthCheckHttpPort: z.number().int().nullish(), healthCheckPeriodSeconds: z.number().int(), healthCheckTimeoutSeconds: z.number().int(), + healthCheckTcpPort: z.number().int().nullish(), createdAt: z.date(), updatedAt: z.date(), }) From 4ec36e18dee515c6300c6c0eaa9dc19b75a6db9b Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Thu, 8 Jan 2026 19:00:22 +0000 Subject: [PATCH 8/9] fix: show healthcheck icon in graph view also for configured tcp health checks --- .../[projectId]/project-network-graph.tsx | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/app/project/[projectId]/project-network-graph.tsx b/src/app/project/[projectId]/project-network-graph.tsx index 497fce2b..50443ce2 100644 --- a/src/app/project/[projectId]/project-network-graph.tsx +++ b/src/app/project/[projectId]/project-network-graph.tsx @@ -59,14 +59,16 @@ const PolicyIcon = ({ policy, type, ports, useNetworkPolicy }: { policy: string, ); }; -const AppNode = ({ data }: { data: { - label: string; - ingressPolicy: string; - egressPolicy: string; - appId: string; - app: AppWithRelations; - ports: string; -} }) => { +const AppNode = ({ data }: { + data: { + label: string; + ingressPolicy: string; + egressPolicy: string; + appId: string; + app: AppWithRelations; + ports: string; + } +}) => { return (
@@ -77,16 +79,16 @@ const AppNode = ({ data }: { data: {

{data.label}

- {data.app.healthChechHttpGetPath && - - - - - -

Healthchecks enabled for this App

-
-
-
} + {(!!data.app.healthChechHttpGetPath || !!data.app.healthCheckTcpPort) && + + + + + +

Healthchecks enabled for this App

+
+
+
}
From ff1165a2754b3298bbdb4d4882371ca0ae7eca91 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Fri, 30 Jan 2026 08:00:27 +0000 Subject: [PATCH 9/9] feat: add health check failureThreshold --- .../20260130074309_migration/migration.sql | 44 +++++++++++++++++++ prisma/schema.prisma | 13 +++--- .../project/app/[appId]/advanced/actions.ts | 5 ++- .../advanced/health-check-settings.tsx | 16 +++++++ .../[appId]/advanced/health-check.model.ts | 3 +- src/server/services/deployment.service.ts | 6 ++- src/shared/model/generated-zod/app.ts | 1 + .../templates/apps/wordpress.template.ts | 6 ++- .../templates/databases/mariadb.template.ts | 1 + .../templates/databases/mongodb.template.ts | 1 + .../templates/databases/mysql.template.ts | 1 + .../templates/databases/postgres.template.ts | 1 + 12 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 prisma/migrations/20260130074309_migration/migration.sql diff --git a/prisma/migrations/20260130074309_migration/migration.sql b/prisma/migrations/20260130074309_migration/migration.sql new file mode 100644 index 00000000..ca56b106 --- /dev/null +++ b/prisma/migrations/20260130074309_migration/migration.sql @@ -0,0 +1,44 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_App" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "appType" TEXT NOT NULL DEFAULT 'APP', + "projectId" TEXT NOT NULL, + "sourceType" TEXT NOT NULL DEFAULT 'GIT', + "containerImageSource" TEXT, + "containerRegistryUsername" TEXT, + "containerRegistryPassword" TEXT, + "gitUrl" TEXT, + "gitBranch" TEXT, + "gitUsername" TEXT, + "gitToken" TEXT, + "dockerfilePath" TEXT NOT NULL DEFAULT './Dockerfile', + "replicas" INTEGER NOT NULL DEFAULT 1, + "envVars" TEXT NOT NULL DEFAULT '', + "memoryReservation" INTEGER, + "memoryLimit" INTEGER, + "cpuReservation" INTEGER, + "cpuLimit" INTEGER, + "webhookId" TEXT, + "ingressNetworkPolicy" TEXT NOT NULL DEFAULT 'ALLOW_ALL', + "egressNetworkPolicy" TEXT NOT NULL DEFAULT 'ALLOW_ALL', + "useNetworkPolicy" BOOLEAN NOT NULL DEFAULT true, + "healthChechHttpGetPath" TEXT, + "healthCheckHttpScheme" TEXT, + "healthCheckHttpHeadersJson" TEXT, + "healthCheckHttpPort" INTEGER, + "healthCheckPeriodSeconds" INTEGER NOT NULL DEFAULT 15, + "healthCheckTimeoutSeconds" INTEGER NOT NULL DEFAULT 5, + "healthCheckFailureThreshold" INTEGER NOT NULL DEFAULT 3, + "healthCheckTcpPort" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "App_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_App" ("appType", "containerImageSource", "containerRegistryPassword", "containerRegistryUsername", "cpuLimit", "cpuReservation", "createdAt", "dockerfilePath", "egressNetworkPolicy", "envVars", "gitBranch", "gitToken", "gitUrl", "gitUsername", "healthChechHttpGetPath", "healthCheckHttpHeadersJson", "healthCheckHttpPort", "healthCheckHttpScheme", "healthCheckPeriodSeconds", "healthCheckTcpPort", "healthCheckTimeoutSeconds", "id", "ingressNetworkPolicy", "memoryLimit", "memoryReservation", "name", "projectId", "replicas", "sourceType", "updatedAt", "useNetworkPolicy", "webhookId") SELECT "appType", "containerImageSource", "containerRegistryPassword", "containerRegistryUsername", "cpuLimit", "cpuReservation", "createdAt", "dockerfilePath", "egressNetworkPolicy", "envVars", "gitBranch", "gitToken", "gitUrl", "gitUsername", "healthChechHttpGetPath", "healthCheckHttpHeadersJson", "healthCheckHttpPort", "healthCheckHttpScheme", "healthCheckPeriodSeconds", "healthCheckTcpPort", "healthCheckTimeoutSeconds", "id", "ingressNetworkPolicy", "memoryLimit", "memoryReservation", "name", "projectId", "replicas", "sourceType", "updatedAt", "useNetworkPolicy", "webhookId" FROM "App"; +DROP TABLE "App"; +ALTER TABLE "new_App" RENAME TO "App"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 784490b7..e61b10bf 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -205,12 +205,13 @@ model App { useNetworkPolicy Boolean @default(true) // healthCheck startupProbe, readinessProbe, livenessProbe - healthChechHttpGetPath String? - healthCheckHttpScheme String? // HTTP, HTTPS - healthCheckHttpHeadersJson String? // JSON stringified key-value pairs - healthCheckHttpPort Int? - healthCheckPeriodSeconds Int @default(10) - healthCheckTimeoutSeconds Int @default(5) + healthChechHttpGetPath String? + healthCheckHttpScheme String? // HTTP, HTTPS + healthCheckHttpHeadersJson String? // JSON stringified key-value pairs + healthCheckHttpPort Int? + healthCheckPeriodSeconds Int @default(15) + healthCheckTimeoutSeconds Int @default(5) + healthCheckFailureThreshold Int @default(3) healthCheckTcpPort Int? diff --git a/src/app/project/app/[appId]/advanced/actions.ts b/src/app/project/app/[appId]/advanced/actions.ts index bd753d85..f6fbe243 100644 --- a/src/app/project/app/[appId]/advanced/actions.ts +++ b/src/app/project/app/[appId]/advanced/actions.ts @@ -53,8 +53,9 @@ export const saveHealthCheck = async (prevState: any, inputData: HealthCheckMode // Prepare update data let updateData: Partial = { - healthCheckPeriodSeconds: validatedData.periodSeconds ?? 10, - healthCheckTimeoutSeconds: validatedData.timeoutSeconds ?? 5, + healthCheckPeriodSeconds: validatedData.periodSeconds, + healthCheckTimeoutSeconds: validatedData.timeoutSeconds, + healthCheckFailureThreshold: validatedData.failureThreshold, }; if (validatedData.enabled) { diff --git a/src/app/project/app/[appId]/advanced/health-check-settings.tsx b/src/app/project/app/[appId]/advanced/health-check-settings.tsx index 6c4caf1d..03ba43de 100644 --- a/src/app/project/app/[appId]/advanced/health-check-settings.tsx +++ b/src/app/project/app/[appId]/advanced/health-check-settings.tsx @@ -39,6 +39,7 @@ export default function HealthCheckSettings({ app, readonly }: { app: AppExtende scheme: (app.healthCheckHttpScheme as "HTTP" | "HTTPS") || "HTTP", periodSeconds: app.healthCheckPeriodSeconds ?? 15, timeoutSeconds: app.healthCheckTimeoutSeconds ?? 5, + failureThreshold: app.healthCheckFailureThreshold ?? 3, headers: defaultHeaders, tcpPort: app.healthCheckTcpPort || undefined, }; @@ -302,6 +303,21 @@ export default function HealthCheckSettings({ app, readonly }: { app: AppExtende )} /> + ( + + + Failure Threshold + + + field.onChange(e.target.value)} /> + + + + )} + />
)} diff --git a/src/app/project/app/[appId]/advanced/health-check.model.ts b/src/app/project/app/[appId]/advanced/health-check.model.ts index 59b4e8e6..81dcad95 100644 --- a/src/app/project/app/[appId]/advanced/health-check.model.ts +++ b/src/app/project/app/[appId]/advanced/health-check.model.ts @@ -15,8 +15,9 @@ export const healthCheckZodModel = z.object({ // TCP probe fields tcpPort: z.coerce.number().int().min(1).max(65535).optional(), // Common fields - periodSeconds: z.coerce.number().int().min(1).default(10), + periodSeconds: z.coerce.number().int().min(1).default(15), timeoutSeconds: z.coerce.number().int().min(1).default(5), + failureThreshold: z.coerce.number().int().min(1).default(3), }); export type HealthCheckModel = z.infer; diff --git a/src/server/services/deployment.service.ts b/src/server/services/deployment.service.ts index 0d3c768d..a7098516 100644 --- a/src/server/services/deployment.service.ts +++ b/src/server/services/deployment.service.ts @@ -201,7 +201,8 @@ class DeploymentService { ...(app.healthCheckHttpHeadersJson ? { httpHeaders: JSON.parse(app.healthCheckHttpHeadersJson) } : {}) }, periodSeconds: app.healthCheckPeriodSeconds, - timeoutSeconds: app.healthCheckTimeoutSeconds + timeoutSeconds: app.healthCheckTimeoutSeconds, + failureThreshold: app.healthCheckFailureThreshold }; dlog(deploymentId, `Configured HTTP Health Checks.`); } else { @@ -211,7 +212,8 @@ class DeploymentService { port: app.healthCheckTcpPort! }, periodSeconds: app.healthCheckPeriodSeconds, - timeoutSeconds: app.healthCheckTimeoutSeconds + timeoutSeconds: app.healthCheckTimeoutSeconds, + failureThreshold: app.healthCheckFailureThreshold }; dlog(deploymentId, `Configured TCP Health Checks.`); } diff --git a/src/shared/model/generated-zod/app.ts b/src/shared/model/generated-zod/app.ts index 26485fb3..c4872e78 100644 --- a/src/shared/model/generated-zod/app.ts +++ b/src/shared/model/generated-zod/app.ts @@ -32,6 +32,7 @@ export const AppModel = z.object({ healthCheckHttpPort: z.number().int().nullish(), healthCheckPeriodSeconds: z.number().int(), healthCheckTimeoutSeconds: z.number().int(), + healthCheckFailureThreshold: z.number().int(), healthCheckTcpPort: z.number().int().nullish(), createdAt: z.date(), updatedAt: z.date(), diff --git a/src/shared/templates/apps/wordpress.template.ts b/src/shared/templates/apps/wordpress.template.ts index 70ae98ef..bc5d25c9 100644 --- a/src/shared/templates/apps/wordpress.template.ts +++ b/src/shared/templates/apps/wordpress.template.ts @@ -44,6 +44,7 @@ MYSQL_USER=wordpress useNetworkPolicy: true, healthCheckPeriodSeconds: 15, healthCheckTimeoutSeconds: 5, + healthCheckFailureThreshold: 3, }, appDomains: [], appVolumes: [{ @@ -83,8 +84,9 @@ WORDPRESS_DB_PASSWORD={password} WORDPRESS_TABLE_PREFIX=wp_ `, useNetworkPolicy: true, - healthCheckPeriodSeconds: 15, - healthCheckTimeoutSeconds: 5, + healthCheckPeriodSeconds: 30, + healthCheckTimeoutSeconds: 10, + healthCheckFailureThreshold: 3, }, appDomains: [], appVolumes: [{ diff --git a/src/shared/templates/databases/mariadb.template.ts b/src/shared/templates/databases/mariadb.template.ts index 9f21e623..a1e135b9 100644 --- a/src/shared/templates/databases/mariadb.template.ts +++ b/src/shared/templates/databases/mariadb.template.ts @@ -54,6 +54,7 @@ export const mariadbAppTemplate: AppTemplateModel = { useNetworkPolicy: true, healthCheckPeriodSeconds: 15, healthCheckTimeoutSeconds: 5, + healthCheckFailureThreshold: 3, }, appDomains: [], appVolumes: [{ diff --git a/src/shared/templates/databases/mongodb.template.ts b/src/shared/templates/databases/mongodb.template.ts index c4c11615..3d13379e 100644 --- a/src/shared/templates/databases/mongodb.template.ts +++ b/src/shared/templates/databases/mongodb.template.ts @@ -47,6 +47,7 @@ export const mongodbAppTemplate: AppTemplateModel = { useNetworkPolicy: true, healthCheckPeriodSeconds: 15, healthCheckTimeoutSeconds: 5, + healthCheckFailureThreshold: 3, }, appDomains: [], appVolumes: [{ diff --git a/src/shared/templates/databases/mysql.template.ts b/src/shared/templates/databases/mysql.template.ts index c863c87e..72721e99 100644 --- a/src/shared/templates/databases/mysql.template.ts +++ b/src/shared/templates/databases/mysql.template.ts @@ -54,6 +54,7 @@ export const mysqlAppTemplate: AppTemplateModel = { useNetworkPolicy: true, healthCheckPeriodSeconds: 15, healthCheckTimeoutSeconds: 5, + healthCheckFailureThreshold: 3, }, appDomains: [], appVolumes: [{ diff --git a/src/shared/templates/databases/postgres.template.ts b/src/shared/templates/databases/postgres.template.ts index ac20f851..06c7d78a 100644 --- a/src/shared/templates/databases/postgres.template.ts +++ b/src/shared/templates/databases/postgres.template.ts @@ -48,6 +48,7 @@ export const postgreAppTemplate: AppTemplateModel = { useNetworkPolicy: true, healthCheckPeriodSeconds: 15, healthCheckTimeoutSeconds: 5, + healthCheckFailureThreshold: 3, }, appDomains: [], appVolumes: [{