feat: declare D601 v03 public exposure
This commit is contained in:
@@ -133,8 +133,38 @@ lanes:
|
||||
- source: fatedier/frpc:v0.68.1
|
||||
target: 127.0.0.1:5000/hwlab/frpc:v0.68.1
|
||||
public:
|
||||
webUrl: http://74.48.78.17:20666
|
||||
apiUrl: http://74.48.78.17:20667
|
||||
webUrl: https://v03.hwpod.com
|
||||
apiUrl: https://v03.hwpod.com
|
||||
publicExposure:
|
||||
mode: pk01-caddy-frp
|
||||
publicBaseUrl: https://v03.hwpod.com
|
||||
hostname: v03.hwpod.com
|
||||
expectedA: 82.156.23.220
|
||||
frpc:
|
||||
serverAddr: 82.156.23.220
|
||||
serverPort: 22000
|
||||
tokenSourceRef: platform-infra/pk01-frp.env
|
||||
tokenSourceKey: FRP_TOKEN
|
||||
secretName: hwlab-v03-frpc-secrets
|
||||
secretKey: frpc.toml
|
||||
tokenKey: token
|
||||
webProxy:
|
||||
name: hwlab-d601-v03-cloud-web
|
||||
remotePort: 22096
|
||||
localIP: hwlab-cloud-web.hwlab-v03.svc.cluster.local
|
||||
localPort: 8080
|
||||
apiProxy:
|
||||
name: hwlab-d601-v03-edge-proxy
|
||||
remotePort: 22095
|
||||
localIP: hwlab-edge-proxy.hwlab-v03.svc.cluster.local
|
||||
localPort: 6667
|
||||
caddy:
|
||||
route: PK01
|
||||
configPath: /etc/caddy/Caddyfile
|
||||
serviceName: caddy
|
||||
email: ops@pikapython.com
|
||||
tls: internal
|
||||
responseHeaderTimeoutSeconds: 600
|
||||
externalPostgres:
|
||||
provider: PK01
|
||||
configRef: config/platform-db/postgres-pk01.yaml
|
||||
|
||||
@@ -90,6 +90,36 @@ export interface HwlabRuntimeImageRewriteSpec {
|
||||
readonly target: string;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimePublicExposureFrpcProxySpec {
|
||||
readonly name: string;
|
||||
readonly localIP: string;
|
||||
readonly localPort: number;
|
||||
readonly remotePort: number;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimePublicExposureSpec {
|
||||
readonly enabled: boolean;
|
||||
readonly mode: "pk01-caddy-frp";
|
||||
readonly publicBaseUrl: string;
|
||||
readonly hostname: string;
|
||||
readonly expectedA: string;
|
||||
readonly serverAddr: string;
|
||||
readonly serverPort: number;
|
||||
readonly tokenSourceRef: string;
|
||||
readonly tokenSourceKey: string;
|
||||
readonly secretName: string;
|
||||
readonly secretKey: string;
|
||||
readonly tokenKey: string;
|
||||
readonly caddyRoute: string;
|
||||
readonly caddyConfigPath: string;
|
||||
readonly caddyServiceName: string;
|
||||
readonly caddyEmail: string;
|
||||
readonly caddyTls: "auto" | "internal";
|
||||
readonly responseHeaderTimeoutSeconds: number;
|
||||
readonly webProxy: HwlabRuntimePublicExposureFrpcProxySpec;
|
||||
readonly apiProxy: HwlabRuntimePublicExposureFrpcProxySpec;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeLaneSpec {
|
||||
readonly lane: HwlabRuntimeLane;
|
||||
readonly nodeId: string;
|
||||
@@ -127,6 +157,7 @@ export interface HwlabRuntimeLaneSpec {
|
||||
readonly stepEnv: Record<string, string>;
|
||||
readonly buildkit?: HwlabRuntimeBuildkitSpec;
|
||||
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
||||
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
|
||||
readonly observability: HwlabRuntimeObservabilitySpec;
|
||||
readonly runtimeImageRewrites: readonly HwlabRuntimeImageRewriteSpec[];
|
||||
readonly networkProfileId: string;
|
||||
@@ -166,6 +197,7 @@ interface HwlabLaneConfig {
|
||||
readonly stepEnv: Record<string, string>;
|
||||
readonly buildkit?: HwlabRuntimeBuildkitSpec;
|
||||
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
||||
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
|
||||
readonly observability: HwlabRuntimeObservabilitySpec;
|
||||
readonly runtimeImageRewrites: readonly HwlabRuntimeImageRewriteSpec[];
|
||||
}
|
||||
@@ -365,6 +397,7 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
|
||||
stepEnv: optionalStringRecord(raw.stepEnv, `lanes.${id}.stepEnv`),
|
||||
buildkit: buildkitConfig(raw.buildkit, `lanes.${id}.buildkit`),
|
||||
externalPostgres: externalPostgresConfig(raw.externalPostgres, `lanes.${id}.externalPostgres`),
|
||||
publicExposure: publicExposureConfig(raw.publicExposure, `lanes.${id}.publicExposure`),
|
||||
observability: observabilityConfig(raw.observability, `lanes.${id}.observability`),
|
||||
runtimeImageRewrites: runtimeImageRewritesConfig(raw.runtimeImageRewrites, `lanes.${id}.runtimeImageRewrites`),
|
||||
};
|
||||
@@ -382,6 +415,7 @@ function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record<
|
||||
stepEnv: mergeOptionalRecord(baseRaw.stepEnv, targetRaw.stepEnv) ?? {},
|
||||
buildkit: mergeOptionalRecord(baseRaw.buildkit, targetRaw.buildkit),
|
||||
externalPostgres: mergeOptionalRecord(baseRaw.externalPostgres, targetRaw.externalPostgres),
|
||||
publicExposure: mergeOptionalRecord(baseRaw.publicExposure, targetRaw.publicExposure),
|
||||
observability: mergeOptionalRecord(baseRaw.observability, targetRaw.observability),
|
||||
};
|
||||
delete merged.targets;
|
||||
@@ -427,6 +461,49 @@ function externalPostgresConfig(value: unknown, path: string): HwlabRuntimeExter
|
||||
};
|
||||
}
|
||||
|
||||
function publicExposureProxyConfig(value: unknown, path: string): HwlabRuntimePublicExposureFrpcProxySpec {
|
||||
const raw = asRecord(value, path);
|
||||
return {
|
||||
name: stringField(raw, "name", path),
|
||||
localIP: stringField(raw, "localIP", path),
|
||||
localPort: numberField(raw, "localPort", path),
|
||||
remotePort: numberField(raw, "remotePort", path),
|
||||
};
|
||||
}
|
||||
|
||||
function publicExposureConfig(value: unknown, path: string): HwlabRuntimePublicExposureSpec | null {
|
||||
if (value === undefined) return null;
|
||||
const raw = asRecord(value, path);
|
||||
const mode = stringField(raw, "mode", path);
|
||||
if (mode !== "pk01-caddy-frp") throw new Error(`${path}.mode must be pk01-caddy-frp`);
|
||||
const frpc = asRecord(raw.frpc, `${path}.frpc`);
|
||||
const caddy = asRecord(raw.caddy, `${path}.caddy`);
|
||||
const caddyTls = optionalStringField(caddy, "tls", `${path}.caddy`) ?? "auto";
|
||||
if (caddyTls !== "auto" && caddyTls !== "internal") throw new Error(`${path}.caddy.tls must be auto or internal`);
|
||||
return {
|
||||
enabled: true,
|
||||
mode,
|
||||
publicBaseUrl: stringField(raw, "publicBaseUrl", path).replace(/\/+$/u, ""),
|
||||
hostname: stringField(raw, "hostname", path),
|
||||
expectedA: stringField(raw, "expectedA", path),
|
||||
serverAddr: stringField(frpc, "serverAddr", `${path}.frpc`),
|
||||
serverPort: numberField(frpc, "serverPort", `${path}.frpc`),
|
||||
tokenSourceRef: stringField(frpc, "tokenSourceRef", `${path}.frpc`),
|
||||
tokenSourceKey: stringField(frpc, "tokenSourceKey", `${path}.frpc`),
|
||||
secretName: stringField(frpc, "secretName", `${path}.frpc`),
|
||||
secretKey: optionalStringField(frpc, "secretKey", `${path}.frpc`) ?? "frpc.toml",
|
||||
tokenKey: optionalStringField(frpc, "tokenKey", `${path}.frpc`) ?? "token",
|
||||
caddyRoute: stringField(caddy, "route", `${path}.caddy`),
|
||||
caddyConfigPath: stringField(caddy, "configPath", `${path}.caddy`),
|
||||
caddyServiceName: stringField(caddy, "serviceName", `${path}.caddy`),
|
||||
caddyEmail: stringField(caddy, "email", `${path}.caddy`),
|
||||
caddyTls,
|
||||
responseHeaderTimeoutSeconds: numberField(caddy, "responseHeaderTimeoutSeconds", `${path}.caddy`),
|
||||
webProxy: publicExposureProxyConfig(frpc.webProxy, `${path}.frpc.webProxy`),
|
||||
apiProxy: publicExposureProxyConfig(frpc.apiProxy, `${path}.frpc.apiProxy`),
|
||||
};
|
||||
}
|
||||
|
||||
function observabilityConfig(value: unknown, path: string): HwlabRuntimeObservabilitySpec {
|
||||
const raw = asRecord(value, path);
|
||||
return {
|
||||
@@ -527,6 +604,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
|
||||
stepEnv: config.stepEnv,
|
||||
...(config.buildkit === undefined ? {} : { buildkit: config.buildkit }),
|
||||
...(config.externalPostgres === undefined ? {} : { externalPostgres: config.externalPostgres }),
|
||||
publicExposure: config.publicExposure,
|
||||
observability: config.observability,
|
||||
runtimeImageRewrites: config.runtimeImageRewrites,
|
||||
networkProfileId: networkProfile.id,
|
||||
|
||||
+482
-2
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { repoRoot, rootPath, type Config } from "./config";
|
||||
@@ -5,7 +6,7 @@ import { runCommand, type CommandResult } from "./command";
|
||||
import { startJob } from "./jobs";
|
||||
import { runHwlabG14Command } from "./hwlab-g14";
|
||||
import { hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "./hwlab-node-control-plane";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimePublicExposureSpec } from "./hwlab-node-lanes";
|
||||
|
||||
type SecretAction = "status" | "ensure" | "cleanup-owned-postgres" | "cleanup-obsolete";
|
||||
type SecretPreset = "openfga" | "master-server-admin-api-key" | "bootstrap-admin" | "code-agent-provider" | "cloud-api-db" | "owned-postgres-cleanup" | "obsolete-secret-cleanup";
|
||||
@@ -31,6 +32,16 @@ interface NodeSecretOptions {
|
||||
timeoutSeconds: number;
|
||||
}
|
||||
|
||||
interface NodePublicExposureOptions {
|
||||
action: "public-exposure";
|
||||
node: string;
|
||||
lane: HwlabRuntimeLane;
|
||||
dryRun: boolean;
|
||||
confirm: boolean;
|
||||
timeoutSeconds: number;
|
||||
spec: HwlabRuntimeLaneSpec;
|
||||
}
|
||||
|
||||
interface RuntimeSecretSpec {
|
||||
node: string;
|
||||
lane: string;
|
||||
@@ -116,6 +127,8 @@ export function hwlabNodeHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts hwlab nodes control-plane apply --node G14 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane refresh --node G14 --lane v03 --confirm",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane sync --node D601 --lane v03 --confirm",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane public-exposure --node D601 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane public-exposure --node D601 --lane v03 --confirm",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane trigger-current --node G14 --lane v03 --confirm",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane runtime-migration --node D601 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane runtime-migration --node D601 --lane v03 --confirm",
|
||||
@@ -139,6 +152,17 @@ export function hwlabNodeHelp(): Record<string, unknown> {
|
||||
async function runNodeDelegatedDomain(config: Config, domain: DelegatedNodeDomain, args: string[]): Promise<Record<string, unknown>> {
|
||||
const scoped = parseNodeScopedDelegatedOptions(domain, args);
|
||||
const defaultSpec = hwlabRuntimeLaneSpec(scoped.lane);
|
||||
if (domain === "control-plane" && scoped.action === "public-exposure") {
|
||||
return runNodePublicExposure({
|
||||
action: "public-exposure",
|
||||
node: scoped.node,
|
||||
lane: scoped.lane,
|
||||
dryRun: scoped.dryRun || !scoped.confirm,
|
||||
confirm: scoped.confirm,
|
||||
timeoutSeconds: scoped.timeoutSeconds,
|
||||
spec: scoped.spec,
|
||||
});
|
||||
}
|
||||
if (domain === "control-plane" && scoped.action === "plan") {
|
||||
return nodeRuntimeControlPlanePlan(scoped);
|
||||
}
|
||||
@@ -262,6 +286,7 @@ function nodeRuntimeExpected(spec: HwlabRuntimeLaneSpec): Record<string, unknown
|
||||
webUrl: spec.publicWebUrl,
|
||||
apiUrl: spec.publicApiUrl,
|
||||
},
|
||||
publicExposure: spec.publicExposure === null ? null : publicExposureSummary(spec.publicExposure),
|
||||
downloadProfile: {
|
||||
id: spec.downloadProfileId,
|
||||
git: spec.downloadProfile.git,
|
||||
@@ -317,6 +342,7 @@ function nodeRuntimeControlPlanePlan(scoped: ReturnType<typeof parseNodeScopedDe
|
||||
secretValuesPrinted: false,
|
||||
runtimeNamespace: scoped.spec.runtimeNamespace,
|
||||
localPostgresExpectedAbsent: scoped.spec.externalPostgres !== undefined,
|
||||
publicExposureDeclared: scoped.spec.publicExposure !== null,
|
||||
},
|
||||
next: {
|
||||
infraStatus: `bun scripts/cli.ts hwlab nodes control-plane infra status --node ${scoped.node} --lane ${scoped.lane}`,
|
||||
@@ -324,6 +350,7 @@ function nodeRuntimeControlPlanePlan(scoped: ReturnType<typeof parseNodeScopedDe
|
||||
platformDbStatus: scoped.spec.externalPostgres === undefined
|
||||
? null
|
||||
: `bun scripts/cli.ts platform-db postgres status --config ${scoped.spec.externalPostgres.configRef}`,
|
||||
publicExposure: scoped.spec.publicExposure === null ? null : `bun scripts/cli.ts hwlab nodes control-plane public-exposure --node ${scoped.node} --lane ${scoped.lane} --confirm`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1519,6 +1546,7 @@ function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" runtimeRenderDir: overlay.runtimeRenderDir,",
|
||||
" runtimeNamespace: overlay.runtimeNamespace,",
|
||||
" externalPostgres: overlay.externalPostgres,",
|
||||
" publicExposure: overlay.publicExposure,",
|
||||
" observability: overlay.observability,",
|
||||
" runtimeImageRewrites: overlay.runtimeImageRewrites,",
|
||||
" gitReadUrl: overlay.gitReadUrl,",
|
||||
@@ -1528,6 +1556,7 @@ function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" return `node - <<'NODE_UNIDESK_RUNTIME_GITOPS_POSTPROCESS'",
|
||||
"const fs = require('fs');",
|
||||
"const path = require('path');",
|
||||
"const crypto = require('crypto');",
|
||||
"const YAML = require('yaml');",
|
||||
"const overlay = ${runtimeOverlay};",
|
||||
"const runtimePath = String(overlay.runtimePath || '');",
|
||||
@@ -1818,11 +1847,90 @@ function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" if (changed) writeYaml(file, normalizeList(items));",
|
||||
" return changed;",
|
||||
"}",
|
||||
"function renderPublicExposureFrpcToml(exposure) {",
|
||||
" return [",
|
||||
" 'serverAddr = ' + JSON.stringify(String(exposure.serverAddr)),",
|
||||
" 'serverPort = ' + Number(exposure.serverPort),",
|
||||
" 'loginFailExit = true',",
|
||||
" 'auth.token = \"{{ .Envs.HWLAB_FRP_TOKEN }}\"',",
|
||||
" '',",
|
||||
" '[[proxies]]',",
|
||||
" 'name = ' + JSON.stringify(String(exposure.webProxy.name)),",
|
||||
" 'type = \"tcp\"',",
|
||||
" 'localIP = ' + JSON.stringify(String(exposure.webProxy.localIP)),",
|
||||
" 'localPort = ' + Number(exposure.webProxy.localPort),",
|
||||
" 'remotePort = ' + Number(exposure.webProxy.remotePort),",
|
||||
" '',",
|
||||
" '[[proxies]]',",
|
||||
" 'name = ' + JSON.stringify(String(exposure.apiProxy.name)),",
|
||||
" 'type = \"tcp\"',",
|
||||
" 'localIP = ' + JSON.stringify(String(exposure.apiProxy.localIP)),",
|
||||
" 'localPort = ' + Number(exposure.apiProxy.localPort),",
|
||||
" 'remotePort = ' + Number(exposure.apiProxy.remotePort),",
|
||||
" '',",
|
||||
" ].join('\\\\n');",
|
||||
"}",
|
||||
"function setEnvFromSecret(container, name, secretName, secretKey) {",
|
||||
" if (!isObject(container)) return false;",
|
||||
" container.env = Array.isArray(container.env) ? container.env : [];",
|
||||
" let item = container.env.find((env) => env && env.name === name);",
|
||||
" if (!item) { item = { name }; container.env.push(item); }",
|
||||
" const nextValueFrom = { secretKeyRef: { name: secretName, key: secretKey } };",
|
||||
" const changed = item.value !== undefined || JSON.stringify(item.valueFrom || {}) !== JSON.stringify(nextValueFrom);",
|
||||
" delete item.value;",
|
||||
" item.valueFrom = nextValueFrom;",
|
||||
" return changed;",
|
||||
"}",
|
||||
"function patchPublicExposure() {",
|
||||
" const exposure = overlay.publicExposure;",
|
||||
" if (!exposure || !exposure.enabled) return { configured: false, changed: false };",
|
||||
" const file = path.join(runtimePath, 'node-frpc.yaml');",
|
||||
" if (!fs.existsSync(file)) {",
|
||||
" console.error(JSON.stringify({ event: 'unidesk-public-exposure-postprocess', ok: false, reason: 'node-frpc-yaml-missing', filePath: file, hostname: exposure.hostname }));",
|
||||
" process.exit(49);",
|
||||
" }",
|
||||
" const docs = readYamlDocuments(file);",
|
||||
" const configName = String(overlay.runtimeNamespace) + '-frpc-config';",
|
||||
" const deploymentName = String(overlay.runtimeNamespace) + '-frpc';",
|
||||
" const configKey = String(exposure.secretKey || 'frpc.toml');",
|
||||
" const tokenKey = String(exposure.tokenKey || 'token');",
|
||||
" const toml = renderPublicExposureFrpcToml(exposure);",
|
||||
" let changed = false;",
|
||||
" let foundConfigMap = false;",
|
||||
" let foundDeployment = false;",
|
||||
" for (const doc of docs) {",
|
||||
" for (const item of listItems(doc).filter(Boolean)) {",
|
||||
" if (!isObject(item)) continue;",
|
||||
" item.metadata = item.metadata || {};",
|
||||
" if (item.kind === 'ConfigMap' && item.metadata.name === configName) {",
|
||||
" foundConfigMap = true;",
|
||||
" item.data = item.data || {};",
|
||||
" if (item.data[configKey] !== toml) { item.data[configKey] = toml; changed = true; }",
|
||||
" }",
|
||||
" if (item.kind === 'Deployment' && item.metadata.name === deploymentName) {",
|
||||
" foundDeployment = true;",
|
||||
" const podSpec = podSpecFor(item);",
|
||||
" for (const container of Array.isArray(podSpec && podSpec.containers) ? podSpec.containers : []) {",
|
||||
" if (!isObject(container)) continue;",
|
||||
" if (container.name === 'frpc' || String(container.image || '').includes('frpc')) changed = setEnvFromSecret(container, 'HWLAB_FRP_TOKEN', exposure.secretName, tokenKey) || changed;",
|
||||
" }",
|
||||
" }",
|
||||
" }",
|
||||
" }",
|
||||
" if (!foundConfigMap || !foundDeployment) {",
|
||||
" console.error(JSON.stringify({ event: 'unidesk-public-exposure-postprocess', ok: false, reason: 'frpc-resource-missing', filePath: file, configName, deploymentName, foundConfigMap, foundDeployment }));",
|
||||
" process.exit(50);",
|
||||
" }",
|
||||
" if (changed) writeYamlDocuments(file, docs);",
|
||||
" console.error(JSON.stringify({ event: 'unidesk-public-exposure-postprocess', ok: true, applied: true, changed, filePath: file, hostname: exposure.hostname, serverAddr: exposure.serverAddr, serverPort: exposure.serverPort, webProxy: exposure.webProxy.name, apiProxy: exposure.apiProxy.name, configSha256: crypto.createHash('sha256').update(toml).digest('hex') }));",
|
||||
" return { configured: true, changed, foundConfigMap, foundDeployment };",
|
||||
"}",
|
||||
"const kustomizationChanged = patchKustomization();",
|
||||
"const runtimeWorkloadsChanged = patchRuntimeWorkloads();",
|
||||
"const externalPostgresChanged = patchExternalPostgres();",
|
||||
"const healthContractChanged = patchHealthContract();",
|
||||
"console.error(JSON.stringify({ event: 'unidesk-runtime-gitops-postprocess', ok: true, runtimePath, sourcePath, pathRelocated: sourcePath !== runtimePath, observabilityPrometheusOperator: overlay.observability ? overlay.observability.prometheusOperator : null, runtimeImageRewriteCount: (overlay.runtimeImageRewrites || []).length, kustomizationChanged, observabilityWorkloadsChanged: runtimeWorkloadsChanged.observabilityChanged, startupProbeChanged: runtimeWorkloadsChanged.startupProbeChanged, runtimeImageRewriteChanged: runtimeWorkloadsChanged.imageRewriteChanged, gitReadUrlChanged: runtimeWorkloadsChanged.gitReadUrlChanged, publicEndpointChanged: runtimeWorkloadsChanged.publicEndpointChanged, dbSslModeChanged: runtimeWorkloadsChanged.dbSslModeChanged, externalPostgresChanged, healthContractChanged }));",
|
||||
"const publicExposureChanged = patchPublicExposure();",
|
||||
"console.error(JSON.stringify({ event: 'unidesk-runtime-gitops-postprocess', ok: true, runtimePath, sourcePath, pathRelocated: sourcePath !== runtimePath, observabilityPrometheusOperator: overlay.observability ? overlay.observability.prometheusOperator : null, runtimeImageRewriteCount: (overlay.runtimeImageRewrites || []).length, kustomizationChanged, observabilityWorkloadsChanged: runtimeWorkloadsChanged.observabilityChanged, startupProbeChanged: runtimeWorkloadsChanged.startupProbeChanged, runtimeImageRewriteChanged: runtimeWorkloadsChanged.imageRewriteChanged, gitReadUrlChanged: runtimeWorkloadsChanged.gitReadUrlChanged, publicEndpointChanged: runtimeWorkloadsChanged.publicEndpointChanged, dbSslModeChanged: runtimeWorkloadsChanged.dbSslModeChanged, externalPostgresChanged, healthContractChanged, publicExposureChanged }));",
|
||||
"NODE_UNIDESK_RUNTIME_GITOPS_POSTPROCESS`;",
|
||||
"}",
|
||||
"function runtimeGitopsVerifyScript() {",
|
||||
@@ -1830,6 +1938,7 @@ function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" runtimePath: overlay.runtimePath,",
|
||||
" runtimeNamespace: overlay.runtimeNamespace,",
|
||||
" externalPostgres: overlay.externalPostgres,",
|
||||
" publicExposure: overlay.publicExposure,",
|
||||
" observability: overlay.observability,",
|
||||
" runtimeImageRewrites: overlay.runtimeImageRewrites,",
|
||||
" gitReadUrl: overlay.gitReadUrl,",
|
||||
@@ -1860,6 +1969,7 @@ function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" return files;",
|
||||
"}",
|
||||
"function readYamlDocuments(file) { return YAML.parseAllDocuments(fs.readFileSync(file, 'utf8')).map((document) => document.toJS()).filter((doc) => doc !== null); }",
|
||||
"function allItemsFromFile(file) { return readYamlDocuments(file).flatMap((doc) => listItems(doc).filter(Boolean)); }",
|
||||
"function podSpecFor(item) {",
|
||||
" if (!isObject(item) || !isObject(item.spec)) return null;",
|
||||
" if (item.kind === 'Pod') return item.spec;",
|
||||
@@ -1944,6 +2054,32 @@ function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" if (String(endpointAddress) !== String(pg.endpointAddress)) fail('external-postgres-address-mismatch', { endpointAddress, expectedEndpointAddress: pg.endpointAddress });",
|
||||
" checks.push('external-postgres-bridge');",
|
||||
"}",
|
||||
"const exposure = overlay.publicExposure;",
|
||||
"if (exposure && exposure.enabled) {",
|
||||
" const file = path.join(runtimePath, 'node-frpc.yaml');",
|
||||
" if (!fs.existsSync(file)) fail('public-exposure-frpc-missing');",
|
||||
" const items = allItemsFromFile(file);",
|
||||
" const configName = String(overlay.runtimeNamespace) + '-frpc-config';",
|
||||
" const deploymentName = String(overlay.runtimeNamespace) + '-frpc';",
|
||||
" const configKey = String(exposure.secretKey || 'frpc.toml');",
|
||||
" const tokenKey = String(exposure.tokenKey || 'token');",
|
||||
" const configMap = items.find((item) => item && item.kind === 'ConfigMap' && item.metadata && item.metadata.name === configName);",
|
||||
" const deployment = items.find((item) => item && item.kind === 'Deployment' && item.metadata && item.metadata.name === deploymentName);",
|
||||
" if (!configMap) fail('public-exposure-frpc-configmap-missing', { expected: configName });",
|
||||
" if (!deployment) fail('public-exposure-frpc-deployment-missing', { expected: deploymentName });",
|
||||
" const toml = configMap.data && configMap.data[configKey];",
|
||||
" if (typeof toml !== 'string') fail('public-exposure-frpc-config-missing', { expectedKey: configKey });",
|
||||
" for (const expected of [String(exposure.serverAddr), String(exposure.serverPort), String(exposure.webProxy.name), String(exposure.webProxy.remotePort), String(exposure.apiProxy.name), String(exposure.apiProxy.remotePort), 'HWLAB_FRP_TOKEN']) {",
|
||||
" if (!toml.includes(expected)) fail('public-exposure-frpc-config-mismatch', { expected });",
|
||||
" }",
|
||||
" const podSpec = podSpecFor(deployment);",
|
||||
" const containers = Array.isArray(podSpec && podSpec.containers) ? podSpec.containers : [];",
|
||||
" const frpc = containers.find((container) => container && (container.name === 'frpc' || String(container.image || '').includes('frpc')));",
|
||||
" const env = frpc && Array.isArray(frpc.env) ? frpc.env.find((item) => item && item.name === 'HWLAB_FRP_TOKEN') : null;",
|
||||
" const secretRef = env && env.valueFrom && env.valueFrom.secretKeyRef;",
|
||||
" if (!secretRef || secretRef.name !== exposure.secretName || secretRef.key !== tokenKey) fail('public-exposure-frpc-token-env-mismatch', { expectedSecret: exposure.secretName, expectedKey: tokenKey });",
|
||||
" checks.push('public-exposure-frpc');",
|
||||
"}",
|
||||
"console.error(JSON.stringify({ event: 'unidesk-runtime-gitops-verify', ok: true, runtimePath, checks }));",
|
||||
"NODE_UNIDESK_RUNTIME_GITOPS_VERIFY`;",
|
||||
"}",
|
||||
@@ -2176,6 +2312,20 @@ function nodeRuntimeRenderOverlay(spec: HwlabRuntimeLaneSpec): Record<string, un
|
||||
buildkitSidecarImage: spec.buildkit?.sidecarImage,
|
||||
publicWebUrl: spec.publicWebUrl,
|
||||
publicApiUrl: spec.publicApiUrl,
|
||||
publicExposure: spec.publicExposure === null ? undefined : {
|
||||
enabled: spec.publicExposure.enabled,
|
||||
mode: spec.publicExposure.mode,
|
||||
publicBaseUrl: spec.publicExposure.publicBaseUrl,
|
||||
hostname: spec.publicExposure.hostname,
|
||||
expectedA: spec.publicExposure.expectedA,
|
||||
serverAddr: spec.publicExposure.serverAddr,
|
||||
serverPort: spec.publicExposure.serverPort,
|
||||
secretName: spec.publicExposure.secretName,
|
||||
secretKey: spec.publicExposure.secretKey,
|
||||
tokenKey: spec.publicExposure.tokenKey,
|
||||
webProxy: spec.publicExposure.webProxy,
|
||||
apiProxy: spec.publicExposure.apiProxy,
|
||||
},
|
||||
externalPostgres: spec.externalPostgres === undefined ? undefined : {
|
||||
enabled: true,
|
||||
serviceName: spec.externalPostgres.serviceName,
|
||||
@@ -2938,6 +3088,336 @@ function nextSecretCommand(options: NodeSecretOptions, spec: RuntimeSecretSpec):
|
||||
return { ensure: `bun scripts/cli.ts hwlab nodes secret ensure --node ${options.node} --lane ${options.lane} --name ${options.name}${options.key ? ` --key ${options.key}` : ""} --confirm` };
|
||||
}
|
||||
|
||||
function publicExposureSummary(exposure: HwlabRuntimePublicExposureSpec): Record<string, unknown> {
|
||||
return {
|
||||
mode: exposure.mode,
|
||||
publicBaseUrl: exposure.publicBaseUrl,
|
||||
hostname: exposure.hostname,
|
||||
expectedA: exposure.expectedA,
|
||||
frpc: {
|
||||
serverAddr: exposure.serverAddr,
|
||||
serverPort: exposure.serverPort,
|
||||
tokenSourceRef: exposure.tokenSourceRef,
|
||||
tokenSourceKey: exposure.tokenSourceKey,
|
||||
secretName: exposure.secretName,
|
||||
secretKey: exposure.secretKey,
|
||||
tokenKey: exposure.tokenKey,
|
||||
webProxy: exposure.webProxy,
|
||||
apiProxy: exposure.apiProxy,
|
||||
},
|
||||
caddy: {
|
||||
route: exposure.caddyRoute,
|
||||
configPath: exposure.caddyConfigPath,
|
||||
serviceName: exposure.caddyServiceName,
|
||||
tls: exposure.caddyTls,
|
||||
responseHeaderTimeoutSeconds: exposure.responseHeaderTimeoutSeconds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function runNodePublicExposure(options: NodePublicExposureOptions): Record<string, unknown> {
|
||||
const exposure = options.spec.publicExposure;
|
||||
if (exposure === null || !exposure.enabled) {
|
||||
return {
|
||||
ok: false,
|
||||
command: "hwlab nodes control-plane public-exposure",
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
configPath: hwlabRuntimeLaneConfigPath(),
|
||||
error: "publicExposure is not configured for this node/lane target",
|
||||
mutation: false,
|
||||
};
|
||||
}
|
||||
const source = readPublicExposureTokenSource(exposure);
|
||||
if (!source.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
command: "hwlab nodes control-plane public-exposure",
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
mode: options.dryRun ? "dry-run" : "confirmed",
|
||||
configPath: hwlabRuntimeLaneConfigPath(),
|
||||
publicExposure: publicExposureSummary(exposure),
|
||||
source,
|
||||
mutation: false,
|
||||
valuesRedacted: true,
|
||||
next: { fixSecretSource: `create .state/secrets/${exposure.tokenSourceRef} with ${exposure.tokenSourceKey}=<redacted>` },
|
||||
};
|
||||
}
|
||||
const secretResult = runTransScript(options.node, publicExposureSecretScript(options, exposure), source.value ?? "", options.timeoutSeconds);
|
||||
const secretFields = keyValueLinesFromText(statusText(secretResult));
|
||||
const caddyResult = runTransHostScript(exposure.caddyRoute, publicExposureCaddyScript(options, exposure), "", options.timeoutSeconds);
|
||||
const caddyFields = keyValueLinesFromText(statusText(caddyResult));
|
||||
const secretOk = options.dryRun ? secretResult.exitCode === 0 : secretResult.exitCode === 0 && secretFields.afterSecretExists === "yes";
|
||||
const ok = secretOk
|
||||
&& caddyResult.exitCode === 0
|
||||
&& caddyFields.afterBlockPresent === "yes"
|
||||
&& caddyFields.validateExitCode === "0"
|
||||
&& (options.dryRun || caddyFields.active === "active");
|
||||
return {
|
||||
ok,
|
||||
command: "hwlab nodes control-plane public-exposure",
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
mode: options.dryRun ? "dry-run" : "confirmed",
|
||||
mutation: !options.dryRun && (secretFields.mutation === "true" || caddyFields.mutation === "true"),
|
||||
configPath: hwlabRuntimeLaneConfigPath(),
|
||||
publicExposure: publicExposureSummary(exposure),
|
||||
source: {
|
||||
ok: source.ok,
|
||||
path: source.path,
|
||||
key: exposure.tokenSourceKey,
|
||||
bytes: source.value?.length ?? 0,
|
||||
fingerprint: source.fingerprint,
|
||||
valueRedacted: true,
|
||||
},
|
||||
secret: publicExposureSecretStatus(secretFields, secretResult),
|
||||
caddy: publicExposureCaddyStatus(caddyFields, caddyResult),
|
||||
valuesRedacted: true,
|
||||
next: ok
|
||||
? {
|
||||
triggerCurrent: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${options.node} --lane ${options.lane} --confirm`,
|
||||
status: `bun scripts/cli.ts hwlab nodes control-plane status --node ${options.node} --lane ${options.lane}`,
|
||||
}
|
||||
: { retry: `bun scripts/cli.ts hwlab nodes control-plane public-exposure --node ${options.node} --lane ${options.lane} --confirm` },
|
||||
};
|
||||
}
|
||||
|
||||
function readPublicExposureTokenSource(exposure: HwlabRuntimePublicExposureSpec): { ok: boolean; path: string; checkedPaths: string[]; key: string; value: string | null; fingerprint: string | null; error?: string } {
|
||||
const checkedPaths = publicExposureTokenSourcePaths(exposure);
|
||||
const path = checkedPaths.find((candidate) => existsSync(candidate)) ?? checkedPaths[0] ?? join(repoRoot, ".state", "secrets", exposure.tokenSourceRef);
|
||||
if (!existsSync(path)) return { ok: false, path, checkedPaths, key: exposure.tokenSourceKey, value: null, fingerprint: null, error: "secret-source-missing" };
|
||||
const values = parseEnvFile(readFileSync(path, "utf8"));
|
||||
const value = values[exposure.tokenSourceKey];
|
||||
if (value === undefined || value.length === 0) return { ok: false, path, checkedPaths, key: exposure.tokenSourceKey, value: null, fingerprint: null, error: "secret-key-missing" };
|
||||
return {
|
||||
ok: true,
|
||||
path,
|
||||
checkedPaths,
|
||||
key: exposure.tokenSourceKey,
|
||||
value,
|
||||
fingerprint: `sha256:${createHash("sha256").update(value).digest("hex").slice(0, 16)}`,
|
||||
};
|
||||
}
|
||||
|
||||
function publicExposureTokenSourcePaths(exposure: HwlabRuntimePublicExposureSpec): string[] {
|
||||
const paths = [join(repoRoot, ".state", "secrets", exposure.tokenSourceRef)];
|
||||
const marker = "/.worktree/";
|
||||
const index = repoRoot.indexOf(marker);
|
||||
if (index >= 0) paths.push(join(repoRoot.slice(0, index), ".state", "secrets", exposure.tokenSourceRef));
|
||||
return [...new Set(paths)];
|
||||
}
|
||||
|
||||
function publicExposureSecretStatus(fields: Record<string, string>, result: CommandResult): Record<string, unknown> {
|
||||
const dryRun = fields.dryRun === "true";
|
||||
return {
|
||||
ok: result.exitCode === 0 && (dryRun || fields.afterSecretExists === "yes"),
|
||||
dryRun,
|
||||
mutation: fields.mutation === "true",
|
||||
namespace: fields.namespace || null,
|
||||
secret: fields.secret || null,
|
||||
beforeExists: fields.beforeSecretExists === "yes",
|
||||
afterExists: fields.afterSecretExists === "yes",
|
||||
tokenBytes: numericField(fields.tokenBytes),
|
||||
applyExitCode: numericField(fields.applyExitCode),
|
||||
rolloutRestartExitCode: numericField(fields.rolloutRestartExitCode),
|
||||
rolloutStatusExitCode: numericField(fields.rolloutStatusExitCode),
|
||||
exitCode: result.exitCode,
|
||||
stderr: result.exitCode === 0 ? "" : result.stderr.trim().slice(0, 2000),
|
||||
};
|
||||
}
|
||||
|
||||
function publicExposureCaddyStatus(fields: Record<string, string>, result: CommandResult): Record<string, unknown> {
|
||||
return {
|
||||
ok: result.exitCode === 0 && fields.afterBlockPresent === "yes" && fields.validateExitCode === "0",
|
||||
dryRun: fields.dryRun === "true",
|
||||
mutation: fields.mutation === "true",
|
||||
hostname: fields.hostname || null,
|
||||
configPath: fields.configPath || null,
|
||||
beforeBlockPresent: fields.beforeBlockPresent === "yes",
|
||||
afterBlockPresent: fields.afterBlockPresent === "yes",
|
||||
validateMode: fields.validateMode || null,
|
||||
validateExitCode: numericField(fields.validateExitCode),
|
||||
reloadExitCode: numericField(fields.reloadExitCode),
|
||||
active: fields.active || null,
|
||||
localWebStatus: fields.localWebStatus || null,
|
||||
localApiStatus: fields.localApiStatus || null,
|
||||
validateErrorPreview: fields.validateErrorPreview || null,
|
||||
localWebErrorPreview: fields.localWebErrorPreview || null,
|
||||
localApiErrorPreview: fields.localApiErrorPreview || null,
|
||||
exitCode: result.exitCode,
|
||||
stderr: result.exitCode === 0 ? "" : result.stderr.trim().slice(0, 2000),
|
||||
};
|
||||
}
|
||||
|
||||
function publicExposureSecretScript(options: NodePublicExposureOptions, exposure: HwlabRuntimePublicExposureSpec): string {
|
||||
return [
|
||||
"set +e",
|
||||
`namespace=${shellQuote(options.spec.runtimeNamespace)}`,
|
||||
`secret=${shellQuote(exposure.secretName)}`,
|
||||
`token_key=${shellQuote(exposure.tokenKey)}`,
|
||||
`dry_run=${shellQuote(options.dryRun ? "true" : "false")}`,
|
||||
"token=$(cat)",
|
||||
"before_secret_exists=no",
|
||||
"kubectl -n \"$namespace\" get secret \"$secret\" >/dev/null 2>&1 && before_secret_exists=yes",
|
||||
"token_bytes=$(printf '%s' \"$token\" | wc -c | tr -d ' ')",
|
||||
"apply_exit=",
|
||||
"rollout_restart_exit=",
|
||||
"rollout_status_exit=",
|
||||
"mutation=false",
|
||||
"if [ \"$dry_run\" = false ]; then",
|
||||
" tmp=$(mktemp /tmp/hwlab-public-frpc-secret.XXXXXX.yaml)",
|
||||
" token_b64=$(printf '%s' \"$token\" | base64 | tr -d '\\n')",
|
||||
" cat >\"$tmp\" <<EOF",
|
||||
"apiVersion: v1",
|
||||
"kind: Secret",
|
||||
"metadata:",
|
||||
" name: $secret",
|
||||
" namespace: $namespace",
|
||||
" labels:",
|
||||
" app.kubernetes.io/part-of: hwlab",
|
||||
" app.kubernetes.io/managed-by: unidesk",
|
||||
"type: Opaque",
|
||||
"data:",
|
||||
" $token_key: $token_b64",
|
||||
"EOF",
|
||||
" kubectl apply --server-side --force-conflicts --field-manager=unidesk-hwlab-node-public-exposure -f \"$tmp\" >/tmp/hwlab-public-frpc-secret.apply.out 2>/tmp/hwlab-public-frpc-secret.apply.err",
|
||||
" apply_exit=$?",
|
||||
" rm -f \"$tmp\"",
|
||||
" if [ \"$apply_exit\" = 0 ]; then mutation=true; fi",
|
||||
" kubectl -n \"$namespace\" rollout restart deploy/hwlab-v03-frpc >/tmp/hwlab-public-frpc-rollout-restart.out 2>/tmp/hwlab-public-frpc-rollout-restart.err",
|
||||
" rollout_restart_exit=$?",
|
||||
" if [ \"$rollout_restart_exit\" = 0 ]; then",
|
||||
" kubectl -n \"$namespace\" rollout status deploy/hwlab-v03-frpc --timeout=60s >/tmp/hwlab-public-frpc-rollout-status.out 2>/tmp/hwlab-public-frpc-rollout-status.err",
|
||||
" rollout_status_exit=$?",
|
||||
" fi",
|
||||
"fi",
|
||||
"after_secret_exists=no",
|
||||
"kubectl -n \"$namespace\" get secret \"$secret\" >/dev/null 2>&1 && after_secret_exists=yes",
|
||||
"printf 'namespace\\t%s\\n' \"$namespace\"",
|
||||
"printf 'secret\\t%s\\n' \"$secret\"",
|
||||
"printf 'dryRun\\t%s\\n' \"$dry_run\"",
|
||||
"printf 'mutation\\t%s\\n' \"$mutation\"",
|
||||
"printf 'beforeSecretExists\\t%s\\n' \"$before_secret_exists\"",
|
||||
"printf 'afterSecretExists\\t%s\\n' \"$after_secret_exists\"",
|
||||
"printf 'tokenBytes\\t%s\\n' \"$token_bytes\"",
|
||||
"printf 'applyExitCode\\t%s\\n' \"$apply_exit\"",
|
||||
"printf 'rolloutRestartExitCode\\t%s\\n' \"$rollout_restart_exit\"",
|
||||
"printf 'rolloutStatusExitCode\\t%s\\n' \"$rollout_status_exit\"",
|
||||
"if [ \"$dry_run\" = true ]; then exit 0; fi",
|
||||
"[ \"$after_secret_exists\" = yes ]",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function publicExposureCaddyScript(options: NodePublicExposureOptions, exposure: HwlabRuntimePublicExposureSpec): string {
|
||||
const blockB64 = Buffer.from(publicExposureCaddyBlock(exposure), "utf8").toString("base64");
|
||||
const marker = `unidesk managed ${exposure.hostname}`;
|
||||
return [
|
||||
"set +e",
|
||||
`dry_run=${shellQuote(options.dryRun ? "true" : "false")}`,
|
||||
`hostname=${shellQuote(exposure.hostname)}`,
|
||||
`config_path=${shellQuote(exposure.caddyConfigPath)}`,
|
||||
`service=${shellQuote(exposure.caddyServiceName)}`,
|
||||
`marker=${shellQuote(marker)}`,
|
||||
`block_b64=${shellQuote(blockB64)}`,
|
||||
`expected_a=${shellQuote(exposure.expectedA)}`,
|
||||
"rm -f /tmp/hwlab-public-caddy-validate.out /tmp/hwlab-public-caddy-validate.err /tmp/hwlab-public-caddy-web.err /tmp/hwlab-public-caddy-api.err",
|
||||
"tmp=$(mktemp -d)",
|
||||
"block=\"$tmp/block\"",
|
||||
"next=\"$tmp/Caddyfile\"",
|
||||
"printf '%s' \"$block_b64\" | base64 -d >\"$block\"",
|
||||
"before_present=no",
|
||||
"if [ -f \"$config_path\" ] && grep -Fq \"# BEGIN $marker\" \"$config_path\"; then before_present=yes; fi",
|
||||
"if [ -f \"$config_path\" ]; then cp \"$config_path\" \"$next\"; else : >\"$next\"; fi",
|
||||
"python3 - \"$next\" \"$block\" \"$marker\" <<'PY'",
|
||||
"import pathlib, sys",
|
||||
"config = pathlib.Path(sys.argv[1])",
|
||||
"block = pathlib.Path(sys.argv[2]).read_text(encoding='utf-8')",
|
||||
"marker = sys.argv[3]",
|
||||
"text = config.read_text(encoding='utf-8') if config.exists() else ''",
|
||||
"begin = f'# BEGIN {marker}'",
|
||||
"end = f'# END {marker}'",
|
||||
"managed = f'{begin}\\n{block.rstrip()}\\n{end}\\n'",
|
||||
"if begin in text and end in text:",
|
||||
" start = text.index(begin)",
|
||||
" stop = text.index(end, start) + len(end)",
|
||||
" text = text[:start].rstrip() + '\\n\\n' + managed.rstrip() + '\\n' + text[stop:].lstrip('\\n')",
|
||||
"else:",
|
||||
" text = text.rstrip() + '\\n\\n' + managed",
|
||||
"config.write_text(text, encoding='utf-8')",
|
||||
"PY",
|
||||
"validate_exit=1",
|
||||
"validate_mode=validate",
|
||||
"if [ \"$dry_run\" = true ]; then",
|
||||
" validate_mode=adapt",
|
||||
" caddy adapt --config \"$next\" --adapter caddyfile >/tmp/hwlab-public-caddy-validate.out 2>/tmp/hwlab-public-caddy-validate.err",
|
||||
"else",
|
||||
" sudo caddy validate --config \"$next\" --adapter caddyfile >/tmp/hwlab-public-caddy-validate.out 2>/tmp/hwlab-public-caddy-validate.err",
|
||||
"fi",
|
||||
"validate_exit=$?",
|
||||
"reload_exit=",
|
||||
"mutation=false",
|
||||
"if [ \"$dry_run\" = false ] && [ \"$validate_exit\" = 0 ]; then",
|
||||
" sudo install -m 0644 \"$next\" \"$config_path\" >/tmp/hwlab-public-caddy-install.out 2>/tmp/hwlab-public-caddy-install.err",
|
||||
" install_exit=$?",
|
||||
" if [ \"$install_exit\" = 0 ]; then",
|
||||
" mutation=true",
|
||||
" sudo systemctl reload \"$service\" >/tmp/hwlab-public-caddy-reload.out 2>/tmp/hwlab-public-caddy-reload.err || sudo systemctl restart \"$service\" >>/tmp/hwlab-public-caddy-reload.out 2>>/tmp/hwlab-public-caddy-reload.err",
|
||||
" reload_exit=$?",
|
||||
" else",
|
||||
" reload_exit=$install_exit",
|
||||
" fi",
|
||||
"fi",
|
||||
"active=$(systemctl is-active \"$service\" 2>/dev/null || true)",
|
||||
"after_present=no",
|
||||
"if [ \"$dry_run\" = true ]; then grep -Fq \"# BEGIN $marker\" \"$next\" && after_present=yes; else grep -Fq \"# BEGIN $marker\" \"$config_path\" && after_present=yes; fi",
|
||||
"local_web_status=",
|
||||
"local_api_status=",
|
||||
"if [ \"$dry_run\" = false ]; then",
|
||||
" local_web_status=$(curl -kfsS --max-time 15 --resolve \"$hostname:443:127.0.0.1\" \"https://$hostname/\" -o /dev/null -w '%{http_code}' 2>/tmp/hwlab-public-caddy-web.err || true)",
|
||||
" local_api_status=$(curl -kfsS --max-time 15 --resolve \"$hostname:443:127.0.0.1\" \"https://$hostname/health/live\" -o /dev/null -w '%{http_code}' 2>/tmp/hwlab-public-caddy-api.err || true)",
|
||||
"fi",
|
||||
"validate_err_preview=$([ -f /tmp/hwlab-public-caddy-validate.err ] && tr '\\n' ';' </tmp/hwlab-public-caddy-validate.err 2>/dev/null | cut -c1-1000 || true)",
|
||||
"local_web_err_preview=$([ -f /tmp/hwlab-public-caddy-web.err ] && tr '\\n' ';' </tmp/hwlab-public-caddy-web.err 2>/dev/null | cut -c1-1000 || true)",
|
||||
"local_api_err_preview=$([ -f /tmp/hwlab-public-caddy-api.err ] && tr '\\n' ';' </tmp/hwlab-public-caddy-api.err 2>/dev/null | cut -c1-1000 || true)",
|
||||
"printf 'hostname\\t%s\\n' \"$hostname\"",
|
||||
"printf 'expectedA\\t%s\\n' \"$expected_a\"",
|
||||
"printf 'configPath\\t%s\\n' \"$config_path\"",
|
||||
"printf 'dryRun\\t%s\\n' \"$dry_run\"",
|
||||
"printf 'mutation\\t%s\\n' \"$mutation\"",
|
||||
"printf 'beforeBlockPresent\\t%s\\n' \"$before_present\"",
|
||||
"printf 'afterBlockPresent\\t%s\\n' \"$after_present\"",
|
||||
"printf 'validateMode\\t%s\\n' \"$validate_mode\"",
|
||||
"printf 'validateExitCode\\t%s\\n' \"$validate_exit\"",
|
||||
"printf 'reloadExitCode\\t%s\\n' \"$reload_exit\"",
|
||||
"printf 'active\\t%s\\n' \"$active\"",
|
||||
"printf 'localWebStatus\\t%s\\n' \"$local_web_status\"",
|
||||
"printf 'localApiStatus\\t%s\\n' \"$local_api_status\"",
|
||||
"printf 'validateErrorPreview\\t%s\\n' \"$validate_err_preview\"",
|
||||
"printf 'localWebErrorPreview\\t%s\\n' \"$local_web_err_preview\"",
|
||||
"printf 'localApiErrorPreview\\t%s\\n' \"$local_api_err_preview\"",
|
||||
"rm -rf \"$tmp\"",
|
||||
"[ \"$validate_exit\" = 0 ] && [ \"$after_present\" = yes ]",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function publicExposureCaddyBlock(exposure: HwlabRuntimePublicExposureSpec): string {
|
||||
const tlsLines = exposure.caddyTls === "internal" ? " tls internal\n" : "";
|
||||
return `${exposure.hostname} {
|
||||
${tlsLines} @api path /health* /api* /v1* /openapi* /docs* /swagger*
|
||||
reverse_proxy @api 127.0.0.1:${exposure.apiProxy.remotePort} {
|
||||
transport http {
|
||||
response_header_timeout ${exposure.responseHeaderTimeoutSeconds}s
|
||||
}
|
||||
}
|
||||
reverse_proxy 127.0.0.1:${exposure.webProxy.remotePort} {
|
||||
transport http {
|
||||
response_header_timeout ${exposure.responseHeaderTimeoutSeconds}s
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
function runTransScript(node: string, script: string, input: string, timeoutSeconds: number): CommandResult {
|
||||
return runCommand([transPath(), `${node}:k3s`, "script", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user