fix: move sub2api runtime to PK01 host docker
This commit is contained in:
@@ -15,7 +15,7 @@ import { capture, compactCapture, parseJsonOutput, prepareFrpcSecret, shQuote }
|
||||
import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-infra-ops-library";
|
||||
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
|
||||
|
||||
import type { Sub2ApiConfig, Sub2ApiDefaults, Sub2ApiEgressProxyConfig, Sub2ApiMasterShadowsocksConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
|
||||
import type { Sub2ApiConfig, Sub2ApiDefaults, Sub2ApiEgressProxyConfig, Sub2ApiHostDockerConfig, Sub2ApiMasterShadowsocksConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
|
||||
import { secretRoot } from "./apply-script";
|
||||
import { configPath } from "./entry";
|
||||
import { normalizePublicBaseUrl, validateHostOrIp, validateHostname, validateKubernetesResourceName, validatePort, validateProxyName, validateProxyUrl } from "./manifest";
|
||||
@@ -162,6 +162,7 @@ export function parseTargets(root: Record<string, unknown>, defaultTargetId: str
|
||||
const id = stringField(record, "id", path);
|
||||
const route = stringField(record, "route", path);
|
||||
const targetNamespace = stringField(record, "namespace", path);
|
||||
const runtimeMode = record.runtimeMode === undefined ? "k3s" : enumField(record, "runtimeMode", path, ["k3s", "host-docker"] as const);
|
||||
const role = stringField(record, "role", path);
|
||||
const enabled = booleanField(record, "enabled", path);
|
||||
const databaseMode = enumField(record, "databaseMode", path, ["bundled", "external-pending", "external-active"] as const);
|
||||
@@ -170,6 +171,7 @@ export function parseTargets(root: Record<string, unknown>, defaultTargetId: str
|
||||
const redisReplicas = integerField(record, "redisReplicas", path);
|
||||
const image = targetImageOverride(record, path);
|
||||
const dependencyImages = targetDependencyImageOverride(record, path);
|
||||
const hostDocker = parseHostDockerConfig(record.hostDocker, path, runtimeMode);
|
||||
const publicExposure = parsePublicExposureConfig(record.publicExposure, path);
|
||||
const egressProxy = parseEgressProxyConfig(record.egressProxy, path);
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(id)) throw new Error(`${configPath}.${path}.id must be a simple target id`);
|
||||
@@ -177,7 +179,10 @@ export function parseTargets(root: Record<string, unknown>, defaultTargetId: str
|
||||
if (!isKubernetesName(targetNamespace)) throw new Error(`${configPath}.${path}.namespace must be a Kubernetes namespace name`);
|
||||
if (appReplicas < 0) throw new Error(`${configPath}.${path}.appReplicas must be >= 0`);
|
||||
if (redisReplicas < 0) throw new Error(`${configPath}.${path}.redisReplicas must be >= 0`);
|
||||
return { id, route, namespace: targetNamespace, role, enabled, databaseMode, redisMode, appReplicas, redisReplicas, image, dependencyImages, publicExposure, egressProxy };
|
||||
if (runtimeMode === "host-docker" && databaseMode !== "external-active") throw new Error(`${configPath}.${path}.databaseMode must be external-active for runtimeMode=host-docker`);
|
||||
if (runtimeMode === "host-docker" && egressProxy?.enabled === true) throw new Error(`${configPath}.${path}.egressProxy must be disabled or omitted for runtimeMode=host-docker`);
|
||||
if (runtimeMode === "host-docker" && publicExposure?.enabled === true && publicExposure.mode !== "pk01-local") throw new Error(`${configPath}.${path}.publicExposure.mode must be pk01-local for runtimeMode=host-docker`);
|
||||
return { id, route, namespace: targetNamespace, runtimeMode, role, enabled, databaseMode, redisMode, appReplicas, redisReplicas, image, dependencyImages, hostDocker, publicExposure, egressProxy };
|
||||
});
|
||||
const ids = new Set<string>();
|
||||
for (const target of targets) {
|
||||
@@ -188,6 +193,37 @@ export function parseTargets(root: Record<string, unknown>, defaultTargetId: str
|
||||
return targets;
|
||||
}
|
||||
|
||||
export function parseHostDockerConfig(value: unknown, path: string, runtimeMode: Sub2ApiTargetConfig["runtimeMode"]): Sub2ApiHostDockerConfig | null {
|
||||
if (value === undefined || value === null) {
|
||||
if (runtimeMode === "host-docker") throw new Error(`${configPath}.${path}.hostDocker is required when runtimeMode=host-docker`);
|
||||
return null;
|
||||
}
|
||||
if (runtimeMode !== "host-docker") throw new Error(`${configPath}.${path}.hostDocker is only supported when runtimeMode=host-docker`);
|
||||
if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.hostDocker must be an object`);
|
||||
const record = value as Record<string, unknown>;
|
||||
const hostDocker: Sub2ApiHostDockerConfig = {
|
||||
projectName: stringField(record, "projectName", `${path}.hostDocker`),
|
||||
workDir: stringField(record, "workDir", `${path}.hostDocker`),
|
||||
composePath: stringField(record, "composePath", `${path}.hostDocker`),
|
||||
envPath: stringField(record, "envPath", `${path}.hostDocker`),
|
||||
appDataDir: stringField(record, "appDataDir", `${path}.hostDocker`),
|
||||
appPort: integerField(record, "appPort", `${path}.hostDocker`),
|
||||
redisPort: integerField(record, "redisPort", `${path}.hostDocker`),
|
||||
databaseHost: stringField(record, "databaseHost", `${path}.hostDocker`),
|
||||
databaseSslMode: stringField(record, "databaseSslMode", `${path}.hostDocker`),
|
||||
noProxy: stringArrayField(record, "noProxy", `${path}.hostDocker`),
|
||||
};
|
||||
validateProxyName(hostDocker.projectName, `${path}.hostDocker.projectName`);
|
||||
for (const key of ["workDir", "composePath", "envPath", "appDataDir"] as const) {
|
||||
if (!hostDocker[key].startsWith("/")) throw new Error(`${configPath}.${path}.hostDocker.${key} must be absolute`);
|
||||
}
|
||||
validatePort(hostDocker.appPort, `${path}.hostDocker.appPort`);
|
||||
validatePort(hostDocker.redisPort, `${path}.hostDocker.redisPort`);
|
||||
validateHostOrIp(hostDocker.databaseHost, `${path}.hostDocker.databaseHost`);
|
||||
if (!/^[A-Za-z0-9_-]+$/u.test(hostDocker.databaseSslMode)) throw new Error(`${configPath}.${path}.hostDocker.databaseSslMode has an unsupported format`);
|
||||
return hostDocker;
|
||||
}
|
||||
|
||||
export function targetImageOverride(record: Record<string, unknown>, path: string): Partial<Sub2ApiConfig["image"]> {
|
||||
if (record.image === undefined) return {};
|
||||
const image = objectField(record, "image", path);
|
||||
@@ -230,22 +266,31 @@ export function parsePublicExposureConfig(value: unknown, path: string): Sub2Api
|
||||
if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.publicExposure must be an object`);
|
||||
const record = value as Record<string, unknown>;
|
||||
const enabled = booleanField(record, "enabled", `${path}.publicExposure`);
|
||||
const mode = record.mode === undefined ? "frp" : enumField(record, "mode", `${path}.publicExposure`, ["frp", "pk01-local"] as const);
|
||||
const publicBaseUrl = stringField(record, "publicBaseUrl", `${path}.publicExposure`);
|
||||
const dnsRaw = objectField(record, "dns", `${path}.publicExposure`);
|
||||
const frpcRaw = objectField(record, "frpc", `${path}.publicExposure`);
|
||||
const localRaw = record.local === undefined || record.local === null ? null : objectField(record, "local", `${path}.publicExposure`);
|
||||
const frpcRaw = record.frpc === undefined || record.frpc === null ? null : objectField(record, "frpc", `${path}.publicExposure`);
|
||||
const pk01Raw = objectField(record, "pk01", `${path}.publicExposure`);
|
||||
const hostname = stringField(dnsRaw, "hostname", `${path}.publicExposure.dns`);
|
||||
const expectedA = stringField(dnsRaw, "expectedA", `${path}.publicExposure.dns`);
|
||||
const resolvers = stringArrayField(dnsRaw, "resolvers", `${path}.publicExposure.dns`);
|
||||
if (mode === "frp" && frpcRaw === null) throw new Error(`${configPath}.${path}.publicExposure.frpc is required when mode=frp`);
|
||||
if (mode === "pk01-local" && localRaw === null) throw new Error(`${configPath}.${path}.publicExposure.local is required when mode=pk01-local`);
|
||||
const exposure: Sub2ApiPublicExposureConfig = {
|
||||
enabled,
|
||||
mode,
|
||||
publicBaseUrl: normalizePublicBaseUrl(publicBaseUrl, `${path}.publicExposure.publicBaseUrl`),
|
||||
dns: {
|
||||
hostname,
|
||||
expectedA,
|
||||
resolvers,
|
||||
},
|
||||
frpc: {
|
||||
local: localRaw === null ? null : {
|
||||
upstreamHost: stringField(localRaw, "upstreamHost", `${path}.publicExposure.local`),
|
||||
upstreamPort: integerField(localRaw, "upstreamPort", `${path}.publicExposure.local`),
|
||||
},
|
||||
frpc: frpcRaw === null ? null : {
|
||||
deploymentName: stringField(frpcRaw, "deploymentName", `${path}.publicExposure.frpc`),
|
||||
secretName: stringField(frpcRaw, "secretName", `${path}.publicExposure.frpc`),
|
||||
secretKey: stringField(frpcRaw, "secretKey", `${path}.publicExposure.frpc`),
|
||||
@@ -475,18 +520,26 @@ export function validatePublicExposureConfig(config: Sub2ApiPublicExposureConfig
|
||||
for (const resolver of config.dns.resolvers) {
|
||||
if (!/^[0-9.]+$/u.test(resolver)) throw new Error(`${configPath}.${path}.publicExposure.dns.resolvers entries must be IPv4 resolvers`);
|
||||
}
|
||||
validateKubernetesResourceName(config.frpc.deploymentName, `${path}.publicExposure.frpc.deploymentName`);
|
||||
validateKubernetesResourceName(config.frpc.secretName, `${path}.publicExposure.frpc.secretName`);
|
||||
if (config.frpc.secretKey !== "frpc.toml") throw new Error(`${configPath}.${path}.publicExposure.frpc.secretKey must be frpc.toml`);
|
||||
if (!isImageReference(config.frpc.image)) throw new Error(`${configPath}.${path}.publicExposure.frpc.image has an unsupported format`);
|
||||
validateHostOrIp(config.frpc.serverAddr, `${path}.publicExposure.frpc.serverAddr`);
|
||||
validatePort(config.frpc.serverPort, `${path}.publicExposure.frpc.serverPort`);
|
||||
validateProxyName(config.frpc.proxyName, `${path}.publicExposure.frpc.proxyName`);
|
||||
validatePort(config.frpc.remotePort, `${path}.publicExposure.frpc.remotePort`);
|
||||
validateHostOrIp(config.frpc.localIP, `${path}.publicExposure.frpc.localIP`);
|
||||
validatePort(config.frpc.localPort, `${path}.publicExposure.frpc.localPort`);
|
||||
if (!/^[A-Za-z0-9_./-]+$/u.test(config.frpc.tokenSourceRef)) throw new Error(`${configPath}.${path}.publicExposure.frpc.tokenSourceRef has an unsupported format`);
|
||||
if (!/^[A-Z0-9_]+$/u.test(config.frpc.tokenSourceKey)) throw new Error(`${configPath}.${path}.publicExposure.frpc.tokenSourceKey must be an env key`);
|
||||
if (config.mode === "frp") {
|
||||
if (config.frpc === null) throw new Error(`${configPath}.${path}.publicExposure.frpc is required when mode=frp`);
|
||||
validateKubernetesResourceName(config.frpc.deploymentName, `${path}.publicExposure.frpc.deploymentName`);
|
||||
validateKubernetesResourceName(config.frpc.secretName, `${path}.publicExposure.frpc.secretName`);
|
||||
if (config.frpc.secretKey !== "frpc.toml") throw new Error(`${configPath}.${path}.publicExposure.frpc.secretKey must be frpc.toml`);
|
||||
if (!isImageReference(config.frpc.image)) throw new Error(`${configPath}.${path}.publicExposure.frpc.image has an unsupported format`);
|
||||
validateHostOrIp(config.frpc.serverAddr, `${path}.publicExposure.frpc.serverAddr`);
|
||||
validatePort(config.frpc.serverPort, `${path}.publicExposure.frpc.serverPort`);
|
||||
validateProxyName(config.frpc.proxyName, `${path}.publicExposure.frpc.proxyName`);
|
||||
validatePort(config.frpc.remotePort, `${path}.publicExposure.frpc.remotePort`);
|
||||
validateHostOrIp(config.frpc.localIP, `${path}.publicExposure.frpc.localIP`);
|
||||
validatePort(config.frpc.localPort, `${path}.publicExposure.frpc.localPort`);
|
||||
if (!/^[A-Za-z0-9_./-]+$/u.test(config.frpc.tokenSourceRef)) throw new Error(`${configPath}.${path}.publicExposure.frpc.tokenSourceRef has an unsupported format`);
|
||||
if (!/^[A-Z0-9_]+$/u.test(config.frpc.tokenSourceKey)) throw new Error(`${configPath}.${path}.publicExposure.frpc.tokenSourceKey must be an env key`);
|
||||
}
|
||||
if (config.mode === "pk01-local") {
|
||||
if (config.local === null) throw new Error(`${configPath}.${path}.publicExposure.local is required when mode=pk01-local`);
|
||||
validateHostOrIp(config.local.upstreamHost, `${path}.publicExposure.local.upstreamHost`);
|
||||
validatePort(config.local.upstreamPort, `${path}.publicExposure.local.upstreamPort`);
|
||||
}
|
||||
if (!/^[A-Za-z0-9:_./-]+$/u.test(config.pk01.route)) throw new Error(`${configPath}.${path}.publicExposure.pk01.route has an unsupported format`);
|
||||
if (!config.pk01.caddyBinaryPath.startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyBinaryPath must be absolute`);
|
||||
if (!/^https:\/\//u.test(config.pk01.caddyDownloadUrl)) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyDownloadUrl must use https://`);
|
||||
|
||||
Reference in New Issue
Block a user