diff --git a/config/platform-infra/host-proxy.yaml b/config/platform-infra/host-proxy.yaml index 5f528732..ab40a346 100644 --- a/config/platform-infra/host-proxy.yaml +++ b/config/platform-infra/host-proxy.yaml @@ -46,6 +46,19 @@ sources: nc01-vpn-server-shadowsocks: extends: templates.sources.singBoxHostClient benchmarkRef: local-vpn-server + nc01-sub2api-docker-shadowsocks: + extends: templates.sources.singBoxHostClient + benchmarkRef: local-vpn-server + client: + configPath: /etc/unidesk/sub2api-docker-egress/sing-box.json + unitPath: /etc/systemd/system/unidesk-sub2api-docker-egress.service + serviceName: unidesk-sub2api-docker-egress + listenPort: 18089 + podAccess: + enabled: false + clashApiListen: 127.0.0.1:19091 + healthUrl: http://127.0.0.1:19091/connections + proxyUrl: http://127.0.0.1:18089 d601-vpn-server-shadowsocks: extends: templates.sources.singBoxHostClient benchmarkRef: local-vpn-server @@ -119,6 +132,27 @@ targets: noProxyRefs: common: noProxy.common extras: noProxy.nodeExtras.NC01 + NC01-SUB2API-DOCKER: + extends: templates.targets.hostProxyClient + route: NC01 + sourceRef: sources.nc01-sub2api-docker-shadowsocks + env: + httpProxy: http://127.0.0.1:18089 + httpsProxy: http://127.0.0.1:18089 + allProxy: http://127.0.0.1:18089 + noProxyRefs: + common: noProxy.common + extras: noProxy.nodeExtras.NC01 + files: + envFile: /etc/unidesk/sub2api-docker-egress/proxy.env + profile: /etc/profile.d/unidesk-sub2api-docker-egress.sh + apt: /etc/apt/apt.conf.d/91-unidesk-sub2api-docker-egress + dockerSystemdDropIn: /etc/systemd/system/docker.service.d/11-unidesk-sub2api-docker-egress.conf + k3sSystemdDropIn: /etc/systemd/system/k3s.service.d/11-unidesk-sub2api-docker-egress.conf + apply: + reloadSystemd: true + restartDocker: false + restartK3s: false D601-VM: extends: templates.targets.hostProxyClient route: D601-VM-HOST diff --git a/config/platform-infra/sub2api.yaml b/config/platform-infra/sub2api.yaml index 5cfc3a9f..60c6893e 100644 --- a/config/platform-infra/sub2api.yaml +++ b/config/platform-infra/sub2api.yaml @@ -240,6 +240,46 @@ targets: - 74.48.78.17 - hyueapi.com - .hyueapi.com + - id: NC01-DOCKER + route: NC01 + namespace: platform-infra + runtimeMode: host-docker + role: active-candidate + enabled: true + databaseMode: external-active + redisMode: local-ephemeral + appReplicas: 1 + redisReplicas: 1 + runtime: + autoSetup: false + image: + repository: docker.io/weishaw/sub2api + tag: 0.1.162 + pullPolicy: Always + dependencyImages: + redis: docker.m.daocloud.io/library/redis:8-alpine + hostDocker: + projectName: sub2api-nc01 + workDir: /opt/unidesk/platform-infra/sub2api-nc01 + composePath: /opt/unidesk/platform-infra/sub2api-nc01/docker-compose.yml + envPath: /opt/unidesk/platform-infra/sub2api-nc01/sub2api.env + appDataDir: /var/lib/unidesk/platform-infra/sub2api-nc01/data + appPort: 28080 + redisPort: 16380 + databaseHost: 82.156.23.220 + databaseSslMode: require + proxyUrl: http://127.0.0.1:18089 + preserveEnvKeys: + - API_KEY + noProxy: + - localhost + - 127.0.0.1 + - ::1 + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - hyueapi.com + - .hyueapi.com - id: NC01 route: NC01:k3s namespace: platform-infra @@ -280,7 +320,7 @@ targets: publicEdge: enabled: true hostname: api.hwpod.com - upstream: sub2api.platform-infra.svc.cluster.local:8080 + upstream: 127.0.0.1:28080 healthPath: /health expectedA: 152.53.229.148 publicExposure: diff --git a/scripts/src/platform-infra-host-proxy.ts b/scripts/src/platform-infra-host-proxy.ts index 4d94b67a..ee3dfe65 100644 --- a/scripts/src/platform-infra-host-proxy.ts +++ b/scripts/src/platform-infra-host-proxy.ts @@ -682,6 +682,13 @@ function status(server: HostProxyServer, target: HostProxyTarget): Record): RenderedCliResult { ...table(["PROXY", "NO_PROXY_COUNT", "HYUEAPI", "VALUES"], [[text(env.proxyUrl), text(env.noProxyCount), "preserved", "printed=false"]]), "", ...transHostProxyEnvTable(target), + ...(result.failure === null || result.failure === undefined ? [] : ["", `FAILURE ${text(result.failure)}`]), "", "NEXT", ` dry-run: ${text(next.dryRun, "")}`, @@ -1504,6 +1514,7 @@ function renderStatus(result: Record): RenderedCliResult { ...table(["NO_PROXY_HYUEAPI", "NO_PROXY_WILDCARD", "VALUES"], [[text(noProxy.hyueapi), text(noProxy.wildcardHyueapi), "printed=false"]]), "", ...transHostProxyEnvTable(target), + ...(result.failure === null || result.failure === undefined ? [] : ["", `FAILURE ${text(result.failure)}`]), "", "Disclosure: file content, Secret values and generated proxy config are not printed.", ]; diff --git a/scripts/src/platform-infra/config.ts b/scripts/src/platform-infra/config.ts index 54904512..e6156c18 100644 --- a/scripts/src/platform-infra/config.ts +++ b/scripts/src/platform-infra/config.ts @@ -288,6 +288,7 @@ export function parseHostDockerConfig(value: unknown, path: string, runtimeMode: redisPort: integerField(record, "redisPort", `${path}.hostDocker`), databaseHost: stringField(record, "databaseHost", `${path}.hostDocker`), databaseSslMode: stringField(record, "databaseSslMode", `${path}.hostDocker`), + proxyUrl: record.proxyUrl === undefined ? null : stringField(record, "proxyUrl", `${path}.hostDocker`), preserveEnvKeys: record.preserveEnvKeys === undefined ? [] : stringArrayField(record, "preserveEnvKeys", `${path}.hostDocker`), noProxy: stringArrayField(record, "noProxy", `${path}.hostDocker`), }; @@ -298,6 +299,7 @@ export function parseHostDockerConfig(value: unknown, path: string, runtimeMode: validatePort(hostDocker.appPort, `${path}.hostDocker.appPort`); validatePort(hostDocker.redisPort, `${path}.hostDocker.redisPort`); validateHostOrIp(hostDocker.databaseHost, `${path}.hostDocker.databaseHost`); + if (hostDocker.proxyUrl !== null) validateProxyUrl(hostDocker.proxyUrl, `${path}.hostDocker.proxyUrl`); if (!/^[A-Za-z0-9_-]+$/u.test(hostDocker.databaseSslMode)) throw new Error(`${configPath}.${path}.hostDocker.databaseSslMode has an unsupported format`); for (const key of hostDocker.preserveEnvKeys) { if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`${configPath}.${path}.hostDocker.preserveEnvKeys contains an unsupported env key`); diff --git a/scripts/src/platform-infra/entry.ts b/scripts/src/platform-infra/entry.ts index 4bef9377..99992bc8 100644 --- a/scripts/src/platform-infra/entry.ts +++ b/scripts/src/platform-infra/entry.ts @@ -158,6 +158,7 @@ export interface Sub2ApiHostDockerConfig { redisPort: number; databaseHost: string; databaseSslMode: string; + proxyUrl: string | null; preserveEnvKeys: string[]; noProxy: string[]; } diff --git a/scripts/src/platform-infra/host-docker.ts b/scripts/src/platform-infra/host-docker.ts index 08b4dfe8..e62aae62 100644 --- a/scripts/src/platform-infra/host-docker.ts +++ b/scripts/src/platform-infra/host-docker.ts @@ -25,6 +25,7 @@ export function hostDockerPlanSummary(sub2api: Sub2ApiConfig, target: Sub2ApiTar databaseHost: database.host, databasePort: database.port, databaseSslMode: database.sslMode, + proxyUrl: host.proxyUrl, preserveEnvKeys: host.preserveEnvKeys, noProxy: host.noProxy, valuesPrinted: false, @@ -101,13 +102,13 @@ export function hostDockerEnvFile(sub2api: Sub2ApiConfig, target: Sub2ApiTargetC SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP: String(urlAllowlist.allowInsecureHttp), SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS: String(urlAllowlist.allowPrivateHosts), SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS: urlAllowlist.upstreamHosts.join(","), - UPDATE_PROXY_URL: "", - HTTP_PROXY: "", - HTTPS_PROXY: "", - ALL_PROXY: "", - http_proxy: "", - https_proxy: "", - all_proxy: "", + UPDATE_PROXY_URL: host.proxyUrl ?? "", + HTTP_PROXY: host.proxyUrl ?? "", + HTTPS_PROXY: host.proxyUrl ?? "", + ALL_PROXY: host.proxyUrl ?? "", + http_proxy: host.proxyUrl ?? "", + https_proxy: host.proxyUrl ?? "", + all_proxy: host.proxyUrl ?? "", NO_PROXY: host.noProxy.join(","), no_proxy: host.noProxy.join(","), GATEWAY_OPENAI_RESPONSE_HEADER_TIMEOUT: "0", @@ -127,6 +128,27 @@ export function hostDockerEnvFile(sub2api: Sub2ApiConfig, target: Sub2ApiTargetC return `${Object.entries(values).map(([key, value]) => `${key}=${envFileValue(value)}`).join("\n")}\n`; } +export function hostDockerConfigFile(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig, secretMaterial: ExternalActiveSecretMaterial): string { + const host = requireHostDockerConfig(target); + const database = targetDatabase(sub2api, target); + return `${JSON.stringify({ + server: { host: "127.0.0.1", port: host.appPort, mode: "release" }, + database: { + host: database.host, + port: database.port, + user: database.user, + password: secretMaterial.values.POSTGRES_PASSWORD, + dbname: database.dbName, + sslmode: database.sslMode, + }, + redis: { host: "127.0.0.1", port: host.redisPort, password: "", db: 0, enable_tls: false }, + jwt: { secret: secretMaterial.values.JWT_SECRET, expire_hour: 24 }, + default: { user_concurrency: 5, user_balance: 0, api_key_prefix: "sk-", rate_multiplier: 1 }, + rate_limit: { requests_per_minute: 60, burst_size: 10 }, + timezone: "Asia/Shanghai", + }, null, 2)}\n`; +} + export function hostDockerDryRunScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string { const host = requireHostDockerConfig(target); const compose = hostDockerComposeYaml(sub2api, target); @@ -225,8 +247,10 @@ export function hostDockerApplyScript(sub2api: Sub2ApiConfig, target: Sub2ApiTar const host = requireHostDockerConfig(target); const compose = hostDockerComposeYaml(sub2api, target); const env = hostDockerEnvFile(sub2api, target, secretMaterial); + const appConfig = hostDockerConfigFile(sub2api, target, secretMaterial); const composeB64 = Buffer.from(compose, "utf8").toString("base64"); const envB64 = Buffer.from(env, "utf8").toString("base64"); + const appConfigB64 = Buffer.from(appConfig, "utf8").toString("base64"); const preserveEnvKeysB64 = Buffer.from(host.preserveEnvKeys.join("\n"), "utf8").toString("base64"); const image = targetImage(sub2api, target); const dependencyImages = targetDependencyImages(sub2api, target); @@ -249,10 +273,12 @@ tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT compose_tmp="$tmp/docker-compose.yml" env_tmp="$tmp/sub2api.env" +app_config_tmp="$tmp/config.yaml" preserve_keys="$tmp/preserve-env-keys.txt" existing_env="$tmp/existing.env" printf '%s' '${composeB64}' | base64 -d >"$compose_tmp" printf '%s' '${envB64}' | base64 -d >"$env_tmp" +printf '%s' '${appConfigB64}' | base64 -d >"$app_config_tmp" printf '%s' '${preserveEnvKeysB64}' | base64 -d >"$preserve_keys" docker_out="$tmp/docker.out" docker_err="$tmp/docker.err" @@ -382,6 +408,10 @@ PY sudo install -m 0600 -o "$current_uid" -g "$current_gid" "$env_tmp" ${shQuote(host.envPath)} >>"$install_out" 2>>"$install_err" install_rc=$? fi + if [ "$install_rc" -eq 0 ]; then + sudo install -m 0600 "$app_config_tmp" ${shQuote(`${host.appDataDir}/config.yaml`)} >>"$install_out" 2>>"$install_err" + install_rc=$? + fi else : >"$install_out" printf '%s\\n' 'skipped because preserve env failed' >"$install_err" @@ -465,10 +495,11 @@ health_rc=1 if [ "$up_rc" -eq 0 ]; then deadline=$(( $(date +%s) + 180 )) while [ "$(date +%s)" -lt "$deadline" ]; do - if curl -fsS --max-time 5 http://127.0.0.1:${host.appPort}/health >"$health_out" 2>"$health_err"; then - health_rc=0 - break - fi + if curl -fsS --max-time 5 http://127.0.0.1:${host.appPort}/health >"$health_out" 2>"$health_err" \ + && grep -Eq '"status"[[:space:]]*:[[:space:]]*"ok"' "$health_out"; then + health_rc=0 + break + fi sleep 3 done else diff --git a/scripts/src/platform-infra/policy.ts b/scripts/src/platform-infra/policy.ts index c9f570e0..3648bf47 100644 --- a/scripts/src/platform-infra/policy.ts +++ b/scripts/src/platform-infra/policy.ts @@ -68,27 +68,27 @@ export function policyChecks(sub2api: Sub2ApiConfig, yaml: string, target: Sub2A { name: "host-docker-no-kubernetes-manifest", ok: yaml.trim().length === 0, - detail: "PK01 host-Docker Sub2API must not render or apply Kubernetes manifests.", + detail: `${target.id} host-Docker Sub2API must not render or apply Kubernetes manifests.`, }, { name: "host-docker-config-present", ok: target.hostDocker !== null, - detail: "PK01 host-Docker Sub2API must be fully declared in config/platform-infra/sub2api.yaml.", + detail: `${target.id} host-Docker Sub2API must be fully declared in config/platform-infra/sub2api.yaml.`, }, { name: "host-docker-no-egress-proxy", ok: target.egressProxy === null || !target.egressProxy.enabled, - detail: "PK01 Sub2API exits directly from PK01/api.pikapython.com and does not deploy the D601 egress proxy.", + detail: `${target.id} host-Docker Sub2API does not deploy a Kubernetes egress proxy.`, }, { name: "host-docker-no-sentinel", ok: !targetHasSentinel(sub2api, target), - detail: "PK01 bare Docker deployment must not deploy the codex-pool sentinel.", + detail: `${target.id} host-Docker deployment must not deploy the Kubernetes codex-pool sentinel.`, }, { name: "host-docker-public-exposure-local", - ok: target.publicExposure?.enabled === true && target.publicExposure.mode === "pk01-local", - detail: "PK01 public exposure must be Caddy to local Docker, not FRP.", + ok: target.publicExposure === null || !target.publicExposure.enabled || target.publicExposure.mode === "pk01-local", + detail: `${target.id} host-Docker public exposure may be disabled for a staged cutover; when enabled it must use local Docker exposure.`, }, ]; }