Merge pull request #2844 from pikasTech/feat/sub2api-nc01-host-docker
将 NC01 Sub2API 迁移到 YAML-first host-Docker
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -682,6 +682,13 @@ function status(server: HostProxyServer, target: HostProxyTarget): Record<string
|
||||
const serverStatus = sourceUsesManagedServer(target.source) ? masterProxyServerStatus(server) : skippedMasterProxyServer(target.source, "status");
|
||||
const result = runTrans(target.route, remoteStatusScript(target), 45);
|
||||
const parsed = parseJson(result.stdout);
|
||||
const failure = result.exitCode !== 0
|
||||
? boundedFailure(result)
|
||||
: parsed === null
|
||||
? "remote status output is not valid JSON"
|
||||
: record(parsed).ok === false
|
||||
? hostProxyFailureDetail(parsed, result)
|
||||
: null;
|
||||
return {
|
||||
ok: serverStatus.ok === true && result.exitCode === 0 && record(parsed).ok !== false,
|
||||
command: "platform-infra egress-proxy host status",
|
||||
@@ -692,6 +699,7 @@ function status(server: HostProxyServer, target: HostProxyTarget): Record<string
|
||||
target: targetSummary(target),
|
||||
remote: parsed ?? { stdoutPreview: result.stdout.slice(0, 2000) },
|
||||
stderrTail: result.stderr.slice(-2000),
|
||||
failure,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
@@ -1034,6 +1042,7 @@ ${podAccessShell}
|
||||
python3 - "$health_rc" "$probe_rc" "$probe_status" "$(exists ${shQuote(target.files.envFile)})" "$(exists ${shQuote(target.files.profile)})" "$(exists ${shQuote(target.files.apt)})" "$(exists ${shQuote(target.files.dockerSystemdDropIn)})" "$(exists ${shQuote(target.files.k3sSystemdDropIn)})" "$(contains ${shQuote(target.files.envFile)} hyueapi.com)" "$(contains ${shQuote(target.files.envFile)} .hyueapi.com)" "$service_active" "$binary_sha" "$archive_sha" ${shQuote(podAccess?.enabled ? "true" : "false")} "$pod_probe_rc" "$pod_probe_status" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
health_rc = int(sys.argv[1])
|
||||
probe_rc = int(sys.argv[2])
|
||||
@@ -1430,6 +1439,7 @@ function renderPlan(result: Record<string, unknown>): 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<string, unknown>): 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.",
|
||||
];
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -158,6 +158,7 @@ export interface Sub2ApiHostDockerConfig {
|
||||
redisPort: number;
|
||||
databaseHost: string;
|
||||
databaseSslMode: string;
|
||||
proxyUrl: string | null;
|
||||
preserveEnvKeys: string[];
|
||||
noProxy: string[];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user