Files
pikasTech-unidesk/scripts/src/platform-infra/manifest.ts
T
2026-06-25 16:16:25 +00:00

664 lines
23 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. manifest module for scripts/src/platform-infra.ts.
// Moved mechanically from scripts/src/platform-infra.ts:937-1587 for #903.
import { createHash, randomBytes } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, isAbsolute, join } from "node:path";
import type { UniDeskConfig } from "../config";
import { rootPath } from "../config";
import { startJob } from "../jobs";
import type { RenderedCliResult } from "../output";
import { pk01CaddyMergeManagedBlocksPython, renderCaddyManagedBlock, renderSimpleReverseProxyCaddySiteBlock } from "../pk01-caddy";
import { capture, compactCapture, parseJsonOutput, prepareFrpcSecret, shQuote } from "../platform-infra-public-service";
import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-infra-ops-library";
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
import type { ManagedResourceCleanupPlan, Sub2ApiConfig, Sub2ApiTargetConfig } from "./entry";
import { isKubernetesName, stringField } from "./config";
import { codexPoolConfigPath, configPath, manifestPath } from "./entry";
export function normalizePublicBaseUrl(value: string, path: string): string {
let parsed: URL;
try {
parsed = new URL(value);
} catch {
throw new Error(`${configPath}.${path} must be a valid URL`);
}
parsed.pathname = parsed.pathname.replace(/\/+$/u, "");
if (parsed.search || parsed.hash) throw new Error(`${configPath}.${path} must not include query or hash`);
return parsed.toString().replace(/\/+$/u, "");
}
export function validateHostname(value: string, path: string): void {
if (!/^[A-Za-z0-9.-]+$/u.test(value) || !value.includes(".")) throw new Error(`${configPath}.${path} must be a hostname`);
}
export function validateKubernetesResourceName(value: string, path: string): void {
if (!isKubernetesName(value)) throw new Error(`${configPath}.${path} must be a Kubernetes resource name`);
}
export function validateHostOrIp(value: string, path: string): void {
if (!/^[A-Za-z0-9._:-]+$/u.test(value)) throw new Error(`${configPath}.${path} has an unsupported format`);
}
export function validatePort(value: number, path: string): void {
if (value < 1 || value > 65535) throw new Error(`${configPath}.${path} must be a TCP port`);
}
export function validateProxyName(value: string, path: string): void {
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${configPath}.${path} has an unsupported format`);
}
export function validateProxyUrl(value: string, path: string): void {
let url: URL;
try {
url = new URL(value);
} catch {
throw new Error(`${configPath}.${path} must be a valid proxy URL`);
}
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${configPath}.${path} must use http:// or https://`);
if (url.username || url.password || url.search || url.hash) throw new Error(`${configPath}.${path} must not include credentials, query, or hash`);
}
export function resolveTarget(sub2api: Sub2ApiConfig, targetId: string): Sub2ApiTargetConfig {
const target = sub2api.targets.find((item) => item.id.toLowerCase() === targetId.toLowerCase());
if (target === undefined) {
const known = sub2api.targets.map((item) => item.id).join(", ");
throw new Error(`unknown Sub2API target ${targetId}; known targets: ${known}`);
}
if (!target.enabled) throw new Error(`Sub2API target ${target.id} is disabled in ${configPath}`);
return target;
}
export function targetImage(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): Sub2ApiConfig["image"] {
return {
repository: target.image.repository ?? sub2api.image.repository,
tag: target.image.tag ?? sub2api.image.tag,
pullPolicy: target.image.pullPolicy ?? sub2api.image.pullPolicy,
};
}
export function targetDependencyImages(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): Sub2ApiConfig["dependencyImages"] {
return {
postgres: target.dependencyImages.postgres ?? sub2api.dependencyImages.postgres,
redis: target.dependencyImages.redis ?? sub2api.dependencyImages.redis,
};
}
export function imageRef(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
const image = targetImage(sub2api, target);
return `${image.repository}:${image.tag}`;
}
export function isExternalTarget(target: Sub2ApiTargetConfig): boolean {
return target.databaseMode === "external-pending" || target.databaseMode === "external-active";
}
export function manifest(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
if (isExternalTarget(target)) return externalPendingManifest(sub2api, target);
const template = readFileSync(manifestPath, "utf8");
const urlAllowlist = sub2api.security.urlAllowlist;
const configHash = configHashFor(sub2api, target);
const image = targetImage(sub2api, target);
const dependencyImages = targetDependencyImages(sub2api, target);
return template
.replaceAll("__SUB2API_IMAGE__", imageRef(sub2api, target))
.replaceAll("__SUB2API_IMAGE_PULL_POLICY__", image.pullPolicy)
.replaceAll("postgres:18-alpine", dependencyImages.postgres)
.replaceAll("redis:8-alpine", dependencyImages.redis)
.replaceAll("__SUB2API_CONFIG_HASH__", configHash)
.replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ENABLED__", String(urlAllowlist.enabled))
.replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP__", String(urlAllowlist.allowInsecureHttp))
.replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS__", String(urlAllowlist.allowPrivateHosts))
.replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS__", urlAllowlist.upstreamHosts.join(","));
}
export function configHashFor(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
return createHash("sha256")
.update(JSON.stringify({
image: targetImage(sub2api, target),
dependencyImages: targetDependencyImages(sub2api, target),
security: sub2api.security,
target,
runtime: sub2api.runtime,
}))
.digest("hex")
.slice(0, 16);
}
export function targetHasSentinel(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): boolean {
return sub2api.runtime.sentinel.enabledOnTargets.some((id) => id.toLowerCase() === target.id.toLowerCase());
}
export function managedResourceCleanupPlan(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): ManagedResourceCleanupPlan {
const sentinel = codexPoolSentinelResourceNames();
return {
externalDbState: {
enabled: isExternalTarget(target),
...sub2api.defaults.cleanup.externalDbState,
},
redisPersistentState: {
enabled: target.redisMode === "local-ephemeral",
...sub2api.defaults.cleanup.redisPersistentState,
},
publicExposure: {
enabled: target.publicExposure?.enabled === true,
deploymentName: target.publicExposure?.frpc.deploymentName ?? sub2api.defaults.cleanup.publicExposure.deploymentName,
configMapName: sub2api.defaults.cleanup.publicExposure.configMapName,
secretName: target.publicExposure?.frpc.secretName ?? sub2api.defaults.cleanup.publicExposure.secretName,
},
egressProxy: {
enabled: target.egressProxy?.enabled === true,
deploymentName: target.egressProxy?.deploymentName ?? sub2api.defaults.cleanup.egressProxy.deploymentName,
serviceName: target.egressProxy?.serviceName ?? sub2api.defaults.cleanup.egressProxy.serviceName,
secretName: target.egressProxy?.secretName ?? sub2api.defaults.cleanup.egressProxy.secretName,
},
sentinel: {
...sentinel,
enabled: targetHasSentinel(sub2api, target),
},
};
}
export function codexPoolSentinelResourceNames(): ManagedResourceCleanupPlan["sentinel"] {
if (!existsSync(codexPoolConfigPath)) throw new Error(`${codexPoolConfigPath} is required for Sub2API codex-pool sentinel cleanup resource names`);
const parsed = Bun.YAML.parse(readFileSync(codexPoolConfigPath, "utf8")) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${codexPoolConfigPath} must be an object`);
const sentinel = (parsed as Record<string, unknown>).sentinel;
if (typeof sentinel !== "object" || sentinel === null || Array.isArray(sentinel)) throw new Error(`${codexPoolConfigPath}.sentinel must be an object`);
const record = sentinel as Record<string, unknown>;
const resourceName = (key: string): string => {
const value = stringField(record, key, "sentinel");
validateKubernetesResourceName(value, `sentinel.${key}`);
return value;
};
return {
enabled: false,
cronJobName: resourceName("cronJobName"),
configMapName: resourceName("configMapName"),
credentialsSecretName: resourceName("credentialsSecretName"),
stateConfigMapName: resourceName("stateConfigMapName"),
serviceAccountName: resourceName("serviceAccountName"),
roleName: resourceName("roleName"),
roleBindingName: resourceName("roleBindingName"),
};
}
export function externalPendingManifest(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
const urlAllowlist = sub2api.security.urlAllowlist;
const database = sub2api.runtime.database;
const databaseStatus = target.databaseMode === "external-active" ? "external-db-active" : "pending-external-db";
const redisService = sub2api.runtime.redis.serviceName;
const appReplicas = target.appReplicas;
const redisReplicas = target.redisReplicas;
const image = targetImage(sub2api, target);
const dependencyImages = targetDependencyImages(sub2api, target);
const publicExposure = renderPublicExposureManifest(target);
const egressProxy = renderEgressProxyManifest(target);
const proxyEnv = sub2ApiProxyEnv(target);
return `apiVersion: v1
kind: Namespace
metadata:
name: ${target.namespace}
labels:
app.kubernetes.io/name: platform-infra
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: ${target.id}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: platform-infra
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- {}
egress:
- {}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sub2api-config
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: sub2api
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: ${target.id}
data:
AUTO_SETUP: "true"
SERVER_HOST: "0.0.0.0"
SERVER_PORT: "8080"
SERVER_MODE: "release"
RUN_MODE: "standard"
DATABASE_HOST: "${database.host}"
DATABASE_PORT: "${database.port}"
DATABASE_USER: "${database.user}"
DATABASE_DBNAME: "${database.dbName}"
DATABASE_SSLMODE: "${database.sslMode}"
DATABASE_MAX_OPEN_CONNS: "10"
DATABASE_MAX_IDLE_CONNS: "2"
DATABASE_CONN_MAX_LIFETIME_MINUTES: "30"
DATABASE_CONN_MAX_IDLE_TIME_MINUTES: "5"
REDIS_HOST: "${redisService}"
REDIS_PORT: "6379"
REDIS_PASSWORD: ""
REDIS_DB: "0"
REDIS_POOL_SIZE: "32"
REDIS_MIN_IDLE_CONNS: "2"
REDIS_ENABLE_TLS: "false"
ADMIN_EMAIL: "admin@sub2api.platform-infra.local"
JWT_EXPIRE_HOUR: "24"
TZ: "Asia/Shanghai"
SECURITY_URL_ALLOWLIST_ENABLED: "${urlAllowlist.enabled}"
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP: "${urlAllowlist.allowInsecureHttp}"
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS: "${urlAllowlist.allowPrivateHosts}"
SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS: "${urlAllowlist.upstreamHosts.join(",")}"
UPDATE_PROXY_URL: "${proxyEnv.httpProxy}"
HTTP_PROXY: "${proxyEnv.httpProxy}"
HTTPS_PROXY: "${proxyEnv.httpProxy}"
ALL_PROXY: "${proxyEnv.httpProxy}"
http_proxy: "${proxyEnv.httpProxy}"
https_proxy: "${proxyEnv.httpProxy}"
all_proxy: "${proxyEnv.httpProxy}"
NO_PROXY: "${proxyEnv.noProxy}"
no_proxy: "${proxyEnv.noProxy}"
GATEWAY_OPENAI_RESPONSE_HEADER_TIMEOUT: "0"
GATEWAY_OPENAI_HTTP2_ENABLED: "true"
GATEWAY_OPENAI_HTTP2_ALLOW_PROXY_FALLBACK_TO_HTTP1: "true"
GATEWAY_OPENAI_HTTP2_FALLBACK_ERROR_THRESHOLD: "2"
GATEWAY_OPENAI_HTTP2_FALLBACK_WINDOW_SECONDS: "60"
GATEWAY_OPENAI_HTTP2_FALLBACK_TTL_SECONDS: "600"
GATEWAY_IMAGE_STREAM_DATA_INTERVAL_TIMEOUT: "900"
GATEWAY_IMAGE_STREAM_KEEPALIVE_INTERVAL: "10"
GATEWAY_IMAGE_CONCURRENCY_ENABLED: "false"
GATEWAY_IMAGE_CONCURRENCY_MAX_CONCURRENT_REQUESTS: "0"
GATEWAY_IMAGE_CONCURRENCY_OVERFLOW_MODE: "reject"
GATEWAY_IMAGE_CONCURRENCY_WAIT_TIMEOUT_SECONDS: "30"
GATEWAY_IMAGE_CONCURRENCY_MAX_WAITING_REQUESTS: "100"
---
apiVersion: v1
kind: Service
metadata:
name: ${redisService}
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: sub2api-redis
app.kubernetes.io/component: redis
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: ${target.id}
unidesk.ai/state-mode: ephemeral-cache
spec:
selector:
app.kubernetes.io/name: sub2api-redis
app.kubernetes.io/component: redis
ports:
- name: redis
port: 6379
targetPort: redis
---
apiVersion: v1
kind: Service
metadata:
name: sub2api
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: sub2api
app.kubernetes.io/component: app
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: ${target.id}
spec:
selector:
app.kubernetes.io/name: sub2api
app.kubernetes.io/component: app
ports:
- name: http
port: 8080
targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sub2api-redis
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: sub2api-redis
app.kubernetes.io/component: redis
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: ${target.id}
unidesk.ai/state-mode: ephemeral-cache
spec:
replicas: ${redisReplicas}
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: sub2api-redis
app.kubernetes.io/component: redis
template:
metadata:
labels:
app.kubernetes.io/name: sub2api-redis
app.kubernetes.io/component: redis
app.kubernetes.io/part-of: platform-infra
spec:
securityContext:
fsGroup: 999
containers:
- name: redis
image: ${dependencyImages.redis}
imagePullPolicy: IfNotPresent
command:
- sh
- -c
args:
- redis-server --save "" --appendonly no
ports:
- name: redis
containerPort: 6379
env:
- name: TZ
value: Asia/Shanghai
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
volumeMounts:
- name: redis-data
mountPath: /data
volumes:
- name: redis-data
emptyDir: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sub2api
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: sub2api
app.kubernetes.io/component: app
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: ${target.id}
unidesk.ai/database-status: ${databaseStatus}
spec:
replicas: ${appReplicas}
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: sub2api
app.kubernetes.io/component: app
template:
metadata:
annotations:
unidesk.ai/sub2api-config-hash: "${configHashFor(sub2api, target)}"
unidesk.ai/database-source-ref: "${database.sourceRef}"
unidesk.ai/database-status: "${databaseStatus}"
labels:
app.kubernetes.io/name: sub2api
app.kubernetes.io/component: app
app.kubernetes.io/part-of: platform-infra
spec:
securityContext:
fsGroup: 1000
initContainers:
- name: wait-postgres
image: ${dependencyImages.postgres}
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- until pg_isready -h ${database.host} -p ${database.port} -U ${database.user} -d ${database.dbName}; do sleep 2; done
- name: wait-redis
image: ${dependencyImages.redis}
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- until redis-cli -h ${redisService} ping | grep -q PONG; do sleep 2; done
containers:
- name: sub2api
image: ${imageRef(sub2api, target)}
imagePullPolicy: ${image.pullPolicy}
ports:
- name: http
containerPort: 8080
envFrom:
- configMapRef:
name: sub2api-config
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: ${database.secretName}
key: ${database.passwordKey}
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: ${database.secretName}
key: ADMIN_PASSWORD
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: ${database.secretName}
key: JWT_SECRET
- name: TOTP_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: ${database.secretName}
key: TOTP_ENCRYPTION_KEY
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
startupProbe:
httpGet:
path: /health
port: http
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
volumeMounts:
- name: sub2api-data
mountPath: /app/data
volumes:
- name: sub2api-data
emptyDir: {}
${publicExposure}
${egressProxy}
`;
}
export function sub2ApiProxyEnv(target: Sub2ApiTargetConfig): { httpProxy: string; noProxy: string } {
const proxy = target.egressProxy;
if (proxy === null || !proxy.enabled || !proxy.applyToSub2Api) return { httpProxy: "", noProxy: "" };
return {
httpProxy: `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`,
noProxy: proxy.noProxy.join(","),
};
}
export function renderPublicExposureManifest(target: Sub2ApiTargetConfig): string {
const exposure = target.publicExposure;
if (exposure === null || !exposure.enabled) return "";
return `---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${exposure.frpc.deploymentName}
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: ${exposure.frpc.deploymentName}
app.kubernetes.io/component: tunnel
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: ${target.id}
unidesk.ai/public-hostname: ${exposure.dns.hostname}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ${exposure.frpc.deploymentName}
app.kubernetes.io/component: tunnel
template:
metadata:
labels:
app.kubernetes.io/name: ${exposure.frpc.deploymentName}
app.kubernetes.io/component: tunnel
app.kubernetes.io/part-of: platform-infra
annotations:
unidesk.ai/public-base-url: "${exposure.publicBaseUrl}"
unidesk.ai/frp-server: "${exposure.frpc.serverAddr}:${exposure.frpc.serverPort}"
unidesk.ai/frp-remote-port: "${exposure.frpc.remotePort}"
spec:
containers:
- name: frpc
image: ${exposure.frpc.image}
imagePullPolicy: IfNotPresent
args:
- -c
- /etc/frp/frpc.toml
volumeMounts:
- name: frpc-config
mountPath: /etc/frp/frpc.toml
subPath: ${exposure.frpc.secretKey}
readOnly: true
volumes:
- name: frpc-config
secret:
secretName: ${exposure.frpc.secretName}
`;
}
export function renderEgressProxyManifest(target: Sub2ApiTargetConfig): string {
const proxy = target.egressProxy;
if (proxy === null || !proxy.enabled) return "";
return `---
apiVersion: v1
kind: Service
metadata:
name: ${proxy.serviceName}
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: ${proxy.deploymentName}
app.kubernetes.io/component: egress-proxy
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: ${target.id}
unidesk.ai/proxy-source: ${proxy.sourceType}
spec:
selector:
app.kubernetes.io/name: ${proxy.deploymentName}
app.kubernetes.io/component: egress-proxy
ports:
- name: http
port: ${proxy.listenPort}
targetPort: proxy
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${proxy.deploymentName}
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: ${proxy.deploymentName}
app.kubernetes.io/component: egress-proxy
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: ${target.id}
unidesk.ai/proxy-source: ${proxy.sourceType}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ${proxy.deploymentName}
app.kubernetes.io/component: egress-proxy
template:
metadata:
labels:
app.kubernetes.io/name: ${proxy.deploymentName}
app.kubernetes.io/component: egress-proxy
app.kubernetes.io/part-of: platform-infra
annotations:
unidesk.ai/proxy-source-ref: "${proxy.sourceRef}"
unidesk.ai/proxy-source-type: "${proxy.sourceType}"
unidesk.ai/proxy-selected-outbound: "${proxy.sourceType === "subscription-url" ? proxy.preferredOutbound : "shadowsocks"}"
spec:
containers:
- name: proxy
image: ${proxy.image}
imagePullPolicy: ${proxy.imagePullPolicy}
args:
- run
- -c
- /etc/sing-box/${proxy.secretKey}
ports:
- name: proxy
containerPort: ${proxy.listenPort}
readinessProbe:
tcpSocket:
port: proxy
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
tcpSocket:
port: proxy
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
volumeMounts:
- name: proxy-config
mountPath: /etc/sing-box/${proxy.secretKey}
subPath: ${proxy.secretKey}
readOnly: true
volumes:
- name: proxy-config
secret:
secretName: ${proxy.secretName}
`;
}