fix(sub2api): add JD01 account local proxy
This commit is contained in:
@@ -165,6 +165,8 @@ Codex 启动时反复出现 WebSocket reconnect、HTTPS fallback、`websocket cl
|
||||
|
||||
Sub2API 管理 UI 的账号连接测试使用账号级 `ProxyID` / proxy URL 配置上游 HTTP transport;账号没有绑定 proxy 时会直接出站,即使 Sub2API Pod 已经有 `HTTP_PROXY` / `HTTPS_PROXY` 环境变量。看到 WebUI 账号测试连 `chatgpt.com` 超时、但 Pod 内显式走目标 proxy 可通时,先检查该账号是否属于 `manualAccounts.protected` 并声明了 `proxyBinding`。如果同一账号用 `gpt-5.2-pro` 返回 ChatGPT OAuth 不支持 Codex 的模型能力错误,但默认/受支持模型能完成 `hi` 或 `/v1/responses` smoke,这不是代理失败;按模型映射/账号能力另行处理。
|
||||
|
||||
如果 WebUI 账号连接测试显示 `proxyconnect tcp: dial tcp 127.0.0.1:<port>: connect: connection refused`,先确认该 proxy URL 是账号级 loopback 配置:在 k3s target 内,`127.0.0.1` 是 Sub2API Pod 自己,不是节点或 PK01。不要先改账号凭据、PK01 Caddy、`api.pikapython.com` 或统一 key;应在目标 `config/platform-infra/sub2api.yaml` 声明 `targets[].accountLocalProxy`,由 `platform-infra sub2api apply --target <id>` 渲染同 Pod sidecar 和 Secret,再用 `validate --target <id>` 的 `accountLocalProxy` 探针验证 `http://127.0.0.1:<port>`。输出仍只允许披露 sourceRef、fingerprint、secretName 和 proxyUrl,不打印 proxy 密码或生成配置。
|
||||
|
||||
WebUI 账号连接测试也不经过统一消费 API key 的 pool group 选择器;账号测试正常不代表 PC Codex 客户端能选中该账号。看到 WebUI 账号测试正常、但 `/responses` 或 `/v1/responses` 以 `account-select-failed` / `no available accounts` 返回 503 时,先检查该手动账号是否声明了 `groupBinding.source: pool-group`,并确认 Sub2API `account_groups` join 里存在该账号与当前统一 API key `group_id` 的绑定。对已支持的 k3s target,通过 `sync --confirm` 加入当前 `pool.groupName`;对 PK01 host-Docker target,在 host-Docker codex-pool sync adapter 补齐前,只能用最小 admin API 写入 `group_ids` 做运行面恢复,且必须只输出 account id、group id、presence/fingerprint 和 smoke 状态,不打印密钥。
|
||||
|
||||
受保护手动账号仍由人工在 Sub2API UI 维护 credentials/status 等字段;UniDesk 只允许通过 YAML 做代理和分组窄绑定:
|
||||
|
||||
@@ -333,6 +333,16 @@ targets:
|
||||
- 74.48.78.17
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
accountLocalProxy:
|
||||
enabled: true
|
||||
containerName: account-local-proxy
|
||||
secretName: sub2api-account-local-proxy-config
|
||||
secretKey: config.json
|
||||
image: ghcr.io/sagernet/sing-box:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
listenHost: 127.0.0.1
|
||||
listenPort: 18789
|
||||
sourceConfigRef: config/platform-infra/egress-proxy-sources.yaml#sources.master-shadowsocks
|
||||
runtime:
|
||||
database:
|
||||
mode: external
|
||||
|
||||
@@ -14,7 +14,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 { Sub2ApiEgressProxyConfig, Sub2ApiPublicExposureConfig } from "./entry";
|
||||
import type { Sub2ApiAccountLocalProxyConfig, Sub2ApiEgressProxyConfig, Sub2ApiPublicExposureConfig } from "./entry";
|
||||
import type { ApplyOptions, DisclosureOptions, TargetOptions } from "./options";
|
||||
import { applyScript, dryRunScript } from "./apply-script";
|
||||
import { readSub2ApiConfig } from "./config";
|
||||
@@ -23,7 +23,7 @@ import { hostDockerApplyScript, hostDockerDryRunScript, hostDockerPlanSummary, h
|
||||
import { imageRef, isExternalTarget, isHostDockerTarget, managedResourceCleanupPlan, manifest, resolveTarget, targetDatabase, targetDependencyImages, targetHasSentinel, targetImage } from "./manifest";
|
||||
import { applyPk01PublicExposure } from "./pk01-public-exposure";
|
||||
import { policyChecks } from "./policy";
|
||||
import { prepareEgressProxySecret, prepareExternalActiveSecret, preparePublicExposureSecret } from "./secrets-and-egress";
|
||||
import { prepareAccountLocalProxySecret, prepareEgressProxySecret, prepareExternalActiveSecret, preparePublicExposureSecret } from "./secrets-and-egress";
|
||||
import { statusScript } from "./status-script";
|
||||
import { boolField } from "./utils";
|
||||
|
||||
@@ -92,6 +92,24 @@ export function egressProxySummary(proxy: Sub2ApiEgressProxyConfig): Record<stri
|
||||
};
|
||||
}
|
||||
|
||||
export function accountLocalProxySummary(proxy: Sub2ApiAccountLocalProxyConfig): Record<string, unknown> {
|
||||
return {
|
||||
enabled: proxy.enabled,
|
||||
mode: "sub2api-pod-loopback-account-proxy",
|
||||
containerName: proxy.containerName,
|
||||
secretName: proxy.secretName,
|
||||
image: proxy.image,
|
||||
imagePullPolicy: proxy.imagePullPolicy,
|
||||
listen: `${proxy.listenHost}:${proxy.listenPort}`,
|
||||
sourceConfigRef: proxy.sourceConfigRef,
|
||||
sourceFingerprint: proxy.sourceFingerprint,
|
||||
sourceRef: proxy.sourceRef,
|
||||
sourceType: proxy.sourceType,
|
||||
healthProbeUrl: proxy.healthProbeUrl,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function plan(options: TargetOptions): Record<string, unknown> {
|
||||
const sub2api = readSub2ApiConfig();
|
||||
const target = resolveTarget(sub2api, options.targetId);
|
||||
@@ -131,6 +149,7 @@ export function plan(options: TargetOptions): Record<string, unknown> {
|
||||
hostDocker,
|
||||
publicExposure: target.publicExposure === null ? null : publicExposureSummary(target.publicExposure),
|
||||
egressProxy: target.egressProxy === null ? null : egressProxySummary(target.egressProxy),
|
||||
accountLocalProxy: target.accountLocalProxy === null ? null : accountLocalProxySummary(target.accountLocalProxy),
|
||||
},
|
||||
externalDatabase: isExternalTarget(target) ? {
|
||||
status: target.databaseMode === "external-active" ? "external-db-active" : "pending-external-db",
|
||||
@@ -189,6 +208,15 @@ export function plan(options: TargetOptions): Record<string, unknown> {
|
||||
valuesPrinted: false,
|
||||
}
|
||||
: null,
|
||||
accountLocalProxy: target.accountLocalProxy?.enabled
|
||||
? {
|
||||
mode: `${target.id} Sub2API pod loopback proxy for manually configured account proxy URLs`,
|
||||
listen: `${target.accountLocalProxy.listenHost}:${target.accountLocalProxy.listenPort}`,
|
||||
sourceRef: target.accountLocalProxy.sourceRef,
|
||||
sourceType: target.accountLocalProxy.sourceType,
|
||||
valuesPrinted: false,
|
||||
}
|
||||
: null,
|
||||
dataStores: isExternalTarget(target)
|
||||
? [
|
||||
isHostDockerTarget(target) ? `PK01 local PostgreSQL through ${database.host}:${database.port}` : target.databaseMode === "external-active" ? "External PostgreSQL active from platform-db/PK01" : "External PostgreSQL pending from platform-db/Pika01",
|
||||
@@ -263,7 +291,15 @@ export async function apply(config: UniDeskConfig, options: ApplyOptions): Promi
|
||||
};
|
||||
}
|
||||
if (options.dryRun) {
|
||||
const result = await capture(config, target.route, ["sh"], isHostDockerTarget(target) ? hostDockerDryRunScript(sub2api, target) : dryRunScript(yaml, target));
|
||||
const accountLocalProxySecretMaterial = prepareAccountLocalProxySecret(sub2api, target);
|
||||
const result = await capture(
|
||||
config,
|
||||
target.route,
|
||||
["sh"],
|
||||
isHostDockerTarget(target)
|
||||
? hostDockerDryRunScript(sub2api, target)
|
||||
: dryRunScript(yaml, target, accountLocalProxySecretMaterial),
|
||||
);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -281,13 +317,14 @@ export async function apply(config: UniDeskConfig, options: ApplyOptions): Promi
|
||||
const secretMaterial = target.databaseMode === "external-active" ? prepareExternalActiveSecret(sub2api) : null;
|
||||
const publicExposureSecretMaterial = preparePublicExposureSecret(sub2api, target);
|
||||
const egressProxySecretMaterial = prepareEgressProxySecret(sub2api, target);
|
||||
const accountLocalProxySecretMaterial = prepareAccountLocalProxySecret(sub2api, target);
|
||||
const result = await capture(
|
||||
config,
|
||||
target.route,
|
||||
["sh"],
|
||||
isHostDockerTarget(target)
|
||||
? hostDockerApplyScript(sub2api, target, secretMaterial)
|
||||
: applyScript(sub2api, yaml, target, secretMaterial, publicExposureSecretMaterial, egressProxySecretMaterial),
|
||||
: applyScript(sub2api, yaml, target, secretMaterial, publicExposureSecretMaterial, egressProxySecretMaterial, accountLocalProxySecretMaterial),
|
||||
);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const pk01Exposure = target.publicExposure === null ? null : await applyPk01PublicExposure(config, target);
|
||||
|
||||
@@ -14,7 +14,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 { EgressProxySecretMaterial, ExternalActiveSecretMaterial, PublicExposureSecretMaterial, Sub2ApiConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
|
||||
import type { AccountLocalProxySecretMaterial, EgressProxySecretMaterial, ExternalActiveSecretMaterial, PublicExposureSecretMaterial, Sub2ApiConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
|
||||
import { fieldManager, requiredSecretKeys, sub2apiCaddyManagedMarker } from "./entry";
|
||||
import { managedResourceCleanupPlan } from "./manifest";
|
||||
|
||||
@@ -99,8 +99,30 @@ export function quoteEnv(value: string): string {
|
||||
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
||||
}
|
||||
|
||||
export function dryRunScript(yaml: string, target: Sub2ApiTargetConfig): string {
|
||||
export function dryRunScript(
|
||||
yaml: string,
|
||||
target: Sub2ApiTargetConfig,
|
||||
accountLocalProxySecretMaterial: AccountLocalProxySecretMaterial | null,
|
||||
): string {
|
||||
const encoded = Buffer.from(yaml, "utf8").toString("base64");
|
||||
const accountLocalProxySecretName = accountLocalProxySecretMaterial?.secretName ?? "sub2api-account-local-proxy-config";
|
||||
const accountLocalProxySecretKey = accountLocalProxySecretMaterial?.secretKey ?? "config.json";
|
||||
const accountLocalProxySecretFile = accountLocalProxySecretMaterial === null
|
||||
? ""
|
||||
: `printf '%s' '${Buffer.from(accountLocalProxySecretMaterial.configJson, "utf8").toString("base64")}' | base64 -d >"$tmp/account-local-proxy-config.json"`;
|
||||
const accountLocalProxySecretSummary = accountLocalProxySecretMaterial === null
|
||||
? "None"
|
||||
: `json.loads(${JSON.stringify(JSON.stringify({
|
||||
sourceRef: accountLocalProxySecretMaterial.sourceRef,
|
||||
sourcePath: accountLocalProxySecretMaterial.sourcePath,
|
||||
secretName: accountLocalProxySecretMaterial.secretName,
|
||||
secretKey: accountLocalProxySecretMaterial.secretKey,
|
||||
fingerprint: accountLocalProxySecretMaterial.fingerprint,
|
||||
sourceBytes: accountLocalProxySecretMaterial.sourceBytes,
|
||||
selectedOutbound: accountLocalProxySecretMaterial.selectedOutbound,
|
||||
proxyUrl: accountLocalProxySecretMaterial.proxyUrl,
|
||||
valuesPrinted: false,
|
||||
}))})`;
|
||||
return `
|
||||
set -u
|
||||
tmp="$(mktemp -d)"
|
||||
@@ -111,6 +133,11 @@ client_out="$tmp/client.out"
|
||||
client_err="$tmp/client.err"
|
||||
server_out="$tmp/server.out"
|
||||
server_err="$tmp/server.err"
|
||||
account_local_proxy_secret_yaml="$tmp/account-local-proxy-secret.yaml"
|
||||
account_local_proxy_secret_out="$tmp/account-local-proxy-secret.out"
|
||||
account_local_proxy_secret_err="$tmp/account-local-proxy-secret.err"
|
||||
account_local_proxy_secret_action="not-enabled"
|
||||
account_local_proxy_secret_rc=0
|
||||
kubectl apply --dry-run=client -f "$manifest" >"$client_out" 2>"$client_err"
|
||||
client_rc=$?
|
||||
if kubectl get namespace ${target.namespace} >/dev/null 2>&1; then
|
||||
@@ -123,20 +150,44 @@ else
|
||||
printf '%s\\n' 'server dry-run skipped because namespace does not exist yet; first apply creates it before namespaced resources' >"$server_out"
|
||||
: >"$server_err"
|
||||
fi
|
||||
python3 - "$client_rc" "$server_rc" "$namespace_exists" "$client_out" "$client_err" "$server_out" "$server_err" <<'PY'
|
||||
if [ "${accountLocalProxySecretMaterial === null ? "false" : "true"}" = "true" ]; then
|
||||
${accountLocalProxySecretFile}
|
||||
kubectl -n ${target.namespace} create secret generic ${accountLocalProxySecretName} \\
|
||||
--from-file=${accountLocalProxySecretKey}="$tmp/account-local-proxy-config.json" \\
|
||||
--dry-run=client -o yaml >"$account_local_proxy_secret_yaml" 2>"$account_local_proxy_secret_err"
|
||||
account_local_proxy_secret_rc=$?
|
||||
account_local_proxy_secret_action="client-dry-run"
|
||||
if [ "$account_local_proxy_secret_rc" -eq 0 ]; then
|
||||
if [ "$namespace_exists" = "true" ]; then
|
||||
kubectl apply --server-side --dry-run=server --field-manager=${fieldManager} -f "$account_local_proxy_secret_yaml" >"$account_local_proxy_secret_out" 2>>"$account_local_proxy_secret_err"
|
||||
account_local_proxy_secret_rc=$?
|
||||
account_local_proxy_secret_action="server-dry-run"
|
||||
else
|
||||
printf '%s\\n' 'server dry-run skipped because namespace does not exist yet; client dry-run succeeded' >"$account_local_proxy_secret_out"
|
||||
fi
|
||||
else
|
||||
: >"$account_local_proxy_secret_out"
|
||||
fi
|
||||
else
|
||||
: >"$account_local_proxy_secret_out"
|
||||
: >"$account_local_proxy_secret_err"
|
||||
fi
|
||||
python3 - "$client_rc" "$server_rc" "$account_local_proxy_secret_rc" "$namespace_exists" "$account_local_proxy_secret_action" "$client_out" "$client_err" "$server_out" "$server_err" "$account_local_proxy_secret_out" "$account_local_proxy_secret_err" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
client_rc = int(sys.argv[1])
|
||||
server_rc = int(sys.argv[2])
|
||||
namespace_exists = sys.argv[3] == "true"
|
||||
paths = sys.argv[4:]
|
||||
account_local_proxy_secret_rc = int(sys.argv[3])
|
||||
namespace_exists = sys.argv[4] == "true"
|
||||
account_local_proxy_secret_action = sys.argv[5]
|
||||
paths = sys.argv[6:]
|
||||
def text(path):
|
||||
try:
|
||||
return open(path, encoding="utf-8").read()
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
payload = {
|
||||
"ok": client_rc == 0 and server_rc == 0,
|
||||
"ok": client_rc == 0 and server_rc == 0 and account_local_proxy_secret_rc == 0,
|
||||
"target": "${target.id}",
|
||||
"namespace": "${target.namespace}",
|
||||
"namespaceExistsBeforeDryRun": namespace_exists,
|
||||
@@ -151,6 +202,13 @@ payload = {
|
||||
"stdout": text(paths[2])[-4000:],
|
||||
"stderr": text(paths[3])[-4000:],
|
||||
},
|
||||
"accountLocalProxy": ${accountLocalProxySecretSummary},
|
||||
"accountLocalProxySecretDryRun": {
|
||||
"exitCode": account_local_proxy_secret_rc,
|
||||
"action": account_local_proxy_secret_action,
|
||||
"stdout": text(paths[4])[-4000:],
|
||||
"stderr": text(paths[5])[-4000:],
|
||||
},
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if payload["ok"] else 1)
|
||||
@@ -165,6 +223,7 @@ export function applyScript(
|
||||
secretMaterial: ExternalActiveSecretMaterial | null,
|
||||
publicExposureSecretMaterial: PublicExposureSecretMaterial | null,
|
||||
egressProxySecretMaterial: EgressProxySecretMaterial | null,
|
||||
accountLocalProxySecretMaterial: AccountLocalProxySecretMaterial | null,
|
||||
): string {
|
||||
const encoded = Buffer.from(yaml, "utf8").toString("base64");
|
||||
const cleanupPlan = managedResourceCleanupPlan(sub2api, target);
|
||||
@@ -173,6 +232,8 @@ export function applyScript(
|
||||
const publicExposureSecretKey = publicExposureSecretMaterial?.secretKey ?? "frpc.toml";
|
||||
const egressProxySecretName = egressProxySecretMaterial?.secretName ?? sub2api.defaults.cleanup.egressProxy.secretName;
|
||||
const egressProxySecretKey = egressProxySecretMaterial?.secretKey ?? "config.json";
|
||||
const accountLocalProxySecretName = accountLocalProxySecretMaterial?.secretName ?? "sub2api-account-local-proxy-config";
|
||||
const accountLocalProxySecretKey = accountLocalProxySecretMaterial?.secretKey ?? "config.json";
|
||||
if (target.databaseMode === "external-active" && secretMaterial === null) throw new Error("external-active apply requires secret material");
|
||||
const externalSecretFiles = secretMaterial === null
|
||||
? ""
|
||||
@@ -222,6 +283,22 @@ export function applyScript(
|
||||
noProxy: egressProxySecretMaterial.noProxy,
|
||||
valuesPrinted: false,
|
||||
}))})`;
|
||||
const accountLocalProxySecretFile = accountLocalProxySecretMaterial === null
|
||||
? ""
|
||||
: ` printf '%s' '${Buffer.from(accountLocalProxySecretMaterial.configJson, "utf8").toString("base64")}' | base64 -d >"$tmp/account-local-proxy-config.json"`;
|
||||
const accountLocalProxySecretSummary = accountLocalProxySecretMaterial === null
|
||||
? "None"
|
||||
: `json.loads(${JSON.stringify(JSON.stringify({
|
||||
sourceRef: accountLocalProxySecretMaterial.sourceRef,
|
||||
sourcePath: accountLocalProxySecretMaterial.sourcePath,
|
||||
secretName: accountLocalProxySecretMaterial.secretName,
|
||||
secretKey: accountLocalProxySecretMaterial.secretKey,
|
||||
fingerprint: accountLocalProxySecretMaterial.fingerprint,
|
||||
sourceBytes: accountLocalProxySecretMaterial.sourceBytes,
|
||||
selectedOutbound: accountLocalProxySecretMaterial.selectedOutbound,
|
||||
proxyUrl: accountLocalProxySecretMaterial.proxyUrl,
|
||||
valuesPrinted: false,
|
||||
}))})`;
|
||||
return `
|
||||
set -u
|
||||
tmp="$(mktemp -d)"
|
||||
@@ -236,6 +313,8 @@ exposure_secret_out="$tmp/exposure-secret.out"
|
||||
exposure_secret_err="$tmp/exposure-secret.err"
|
||||
egress_secret_out="$tmp/egress-secret.out"
|
||||
egress_secret_err="$tmp/egress-secret.err"
|
||||
account_local_proxy_secret_out="$tmp/account-local-proxy-secret.out"
|
||||
account_local_proxy_secret_err="$tmp/account-local-proxy-secret.err"
|
||||
apply_out="$tmp/apply.out"
|
||||
apply_err="$tmp/apply.err"
|
||||
cleanup_out="$tmp/cleanup.out"
|
||||
@@ -248,6 +327,8 @@ exposure_secret_action="not-enabled"
|
||||
exposure_secret_rc=0
|
||||
egress_secret_action="not-enabled"
|
||||
egress_secret_rc=0
|
||||
account_local_proxy_secret_action="not-enabled"
|
||||
account_local_proxy_secret_rc=0
|
||||
if [ "$ns_rc" -eq 0 ]; then
|
||||
if [ "${target.databaseMode}" = "external-pending" ]; then
|
||||
secret_action="external-pending-not-managed"
|
||||
@@ -307,14 +388,25 @@ ${egressProxySecretFile}
|
||||
: >"$egress_secret_out"
|
||||
: >"$egress_secret_err"
|
||||
fi
|
||||
if [ "${accountLocalProxySecretMaterial === null ? "false" : "true"}" = "true" ]; then
|
||||
${accountLocalProxySecretFile}
|
||||
kubectl -n ${target.namespace} create secret generic ${accountLocalProxySecretName} \\
|
||||
--from-file=${accountLocalProxySecretKey}="$tmp/account-local-proxy-config.json" \\
|
||||
--dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$account_local_proxy_secret_out" 2>"$account_local_proxy_secret_err"
|
||||
account_local_proxy_secret_rc=$?
|
||||
account_local_proxy_secret_action="synced"
|
||||
else
|
||||
: >"$account_local_proxy_secret_out"
|
||||
: >"$account_local_proxy_secret_err"
|
||||
fi
|
||||
fi
|
||||
apply_rc=1
|
||||
if [ "$ns_rc" -eq 0 ] && [ "$secret_rc" -eq 0 ] && [ "$exposure_secret_rc" -eq 0 ] && [ "$egress_secret_rc" -eq 0 ]; then
|
||||
if [ "$ns_rc" -eq 0 ] && [ "$secret_rc" -eq 0 ] && [ "$exposure_secret_rc" -eq 0 ] && [ "$egress_secret_rc" -eq 0 ] && [ "$account_local_proxy_secret_rc" -eq 0 ]; then
|
||||
kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >"$apply_out" 2>"$apply_err"
|
||||
apply_rc=$?
|
||||
else
|
||||
: >"$apply_out"
|
||||
printf '%s\\n' 'skipped because namespace, app secret, public exposure secret, or egress proxy secret step failed' >"$apply_err"
|
||||
printf '%s\\n' 'skipped because namespace, app secret, public exposure secret, egress proxy secret, or account-local proxy secret step failed' >"$apply_err"
|
||||
fi
|
||||
cleanup_rc=0
|
||||
if [ "$apply_rc" -eq 0 ]; then
|
||||
@@ -414,19 +506,21 @@ else
|
||||
printf '%s\\n' 'skipped because apply failed' >"$cleanup_err"
|
||||
cleanup_rc=1
|
||||
fi
|
||||
python3 - "$ns_rc" "$secret_rc" "$exposure_secret_rc" "$egress_secret_rc" "$apply_rc" "$cleanup_rc" "$secret_action" "$exposure_secret_action" "$egress_secret_action" "$ns_out" "$ns_err" "$secret_out" "$secret_err" "$exposure_secret_out" "$exposure_secret_err" "$egress_secret_out" "$egress_secret_err" "$apply_out" "$apply_err" "$cleanup_out" "$cleanup_err" <<'PY'
|
||||
python3 - "$ns_rc" "$secret_rc" "$exposure_secret_rc" "$egress_secret_rc" "$account_local_proxy_secret_rc" "$apply_rc" "$cleanup_rc" "$secret_action" "$exposure_secret_action" "$egress_secret_action" "$account_local_proxy_secret_action" "$ns_out" "$ns_err" "$secret_out" "$secret_err" "$exposure_secret_out" "$exposure_secret_err" "$egress_secret_out" "$egress_secret_err" "$account_local_proxy_secret_out" "$account_local_proxy_secret_err" "$apply_out" "$apply_err" "$cleanup_out" "$cleanup_err" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
ns_rc = int(sys.argv[1])
|
||||
secret_rc = int(sys.argv[2])
|
||||
exposure_secret_rc = int(sys.argv[3])
|
||||
egress_secret_rc = int(sys.argv[4])
|
||||
apply_rc = int(sys.argv[5])
|
||||
cleanup_rc = int(sys.argv[6])
|
||||
secret_action = sys.argv[7]
|
||||
exposure_secret_action = sys.argv[8]
|
||||
egress_secret_action = sys.argv[9]
|
||||
paths = sys.argv[10:]
|
||||
account_local_proxy_secret_rc = int(sys.argv[5])
|
||||
apply_rc = int(sys.argv[6])
|
||||
cleanup_rc = int(sys.argv[7])
|
||||
secret_action = sys.argv[8]
|
||||
exposure_secret_action = sys.argv[9]
|
||||
egress_secret_action = sys.argv[10]
|
||||
account_local_proxy_secret_action = sys.argv[11]
|
||||
paths = sys.argv[12:]
|
||||
def text(path):
|
||||
try:
|
||||
return open(path, encoding="utf-8").read()
|
||||
@@ -438,7 +532,7 @@ def parsed(path):
|
||||
except Exception:
|
||||
return None
|
||||
payload = {
|
||||
"ok": ns_rc == 0 and secret_rc == 0 and exposure_secret_rc == 0 and egress_secret_rc == 0 and apply_rc == 0 and cleanup_rc == 0,
|
||||
"ok": ns_rc == 0 and secret_rc == 0 and exposure_secret_rc == 0 and egress_secret_rc == 0 and account_local_proxy_secret_rc == 0 and apply_rc == 0 and cleanup_rc == 0,
|
||||
"target": "${target.id}",
|
||||
"namespace": "${target.namespace}",
|
||||
"databaseMode": "${target.databaseMode}",
|
||||
@@ -454,13 +548,15 @@ payload = {
|
||||
},
|
||||
"publicExposure": ${exposureSecretSummary},
|
||||
"egressProxy": ${egressProxySecretSummary},
|
||||
"accountLocalProxy": ${accountLocalProxySecretSummary},
|
||||
"steps": {
|
||||
"namespace": {"exitCode": ns_rc, "stdout": text(paths[0])[-4000:], "stderr": text(paths[1])[-4000:]},
|
||||
"secret": {"exitCode": secret_rc, "stdout": text(paths[2])[-4000:], "stderr": text(paths[3])[-4000:]},
|
||||
"publicExposureSecret": {"exitCode": exposure_secret_rc, "action": exposure_secret_action, "stdout": text(paths[4])[-4000:], "stderr": text(paths[5])[-4000:]},
|
||||
"egressProxySecret": {"exitCode": egress_secret_rc, "action": egress_secret_action, "stdout": text(paths[6])[-4000:], "stderr": text(paths[7])[-4000:]},
|
||||
"apply": {"exitCode": apply_rc, "stdout": text(paths[8])[-8000:], "stderr": text(paths[9])[-4000:]},
|
||||
"cleanup": {"exitCode": cleanup_rc, "summary": parsed(paths[10]), "stdout": text(paths[10])[-8000:], "stderr": text(paths[11])[-4000:]},
|
||||
"accountLocalProxySecret": {"exitCode": account_local_proxy_secret_rc, "action": account_local_proxy_secret_action, "stdout": text(paths[8])[-4000:], "stderr": text(paths[9])[-4000:]},
|
||||
"apply": {"exitCode": apply_rc, "stdout": text(paths[10])[-8000:], "stderr": text(paths[11])[-4000:]},
|
||||
"cleanup": {"exitCode": cleanup_rc, "summary": parsed(paths[12]), "stdout": text(paths[12])[-8000:], "stderr": text(paths[13])[-4000:]},
|
||||
},
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
|
||||
@@ -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, Sub2ApiHostDockerConfig, Sub2ApiMasterShadowsocksConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
|
||||
import type { Sub2ApiAccountLocalProxyConfig, 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";
|
||||
@@ -174,6 +174,7 @@ export function parseTargets(root: Record<string, unknown>, defaultTargetId: str
|
||||
const hostDocker = parseHostDockerConfig(record.hostDocker, path, runtimeMode);
|
||||
const publicExposure = parsePublicExposureConfig(record.publicExposure, path);
|
||||
const egressProxy = parseEgressProxyConfig(record.egressProxy, path);
|
||||
const accountLocalProxy = parseAccountLocalProxyConfig(record.accountLocalProxy, path);
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(id)) throw new Error(`${configPath}.${path}.id must be a simple target id`);
|
||||
if (!/^[A-Za-z0-9:_./-]+$/u.test(route)) throw new Error(`${configPath}.${path}.route has an unsupported format`);
|
||||
if (!isKubernetesName(targetNamespace)) throw new Error(`${configPath}.${path}.namespace must be a Kubernetes namespace name`);
|
||||
@@ -181,8 +182,9 @@ export function parseTargets(root: Record<string, unknown>, defaultTargetId: str
|
||||
if (redisReplicas < 0) throw new Error(`${configPath}.${path}.redisReplicas must be >= 0`);
|
||||
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" && accountLocalProxy?.enabled === true) throw new Error(`${configPath}.${path}.accountLocalProxy 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 };
|
||||
return { id, route, namespace: targetNamespace, runtimeMode, role, enabled, databaseMode, redisMode, appReplicas, redisReplicas, image, dependencyImages, hostDocker, publicExposure, egressProxy, accountLocalProxy };
|
||||
});
|
||||
const ids = new Set<string>();
|
||||
for (const target of targets) {
|
||||
@@ -372,6 +374,36 @@ export function parseEgressProxyConfig(value: unknown, path: string): Sub2ApiEgr
|
||||
return proxy;
|
||||
}
|
||||
|
||||
export function parseAccountLocalProxyConfig(value: unknown, path: string): Sub2ApiAccountLocalProxyConfig | null {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.accountLocalProxy must be an object`);
|
||||
const record = value as Record<string, unknown>;
|
||||
const sourceConfigRef = stringField(record, "sourceConfigRef", `${path}.accountLocalProxy`);
|
||||
const source = resolveEgressProxySourceRef(sourceConfigRef, `${configPath}.${path}.accountLocalProxy.sourceConfigRef`);
|
||||
if (source.sourceType !== "master-shadowsocks" || source.masterShadowsocks === null) {
|
||||
throw new Error(`${configPath}.${path}.accountLocalProxy.sourceConfigRef must reference a master-shadowsocks source`);
|
||||
}
|
||||
const proxy: Sub2ApiAccountLocalProxyConfig = {
|
||||
enabled: booleanField(record, "enabled", `${path}.accountLocalProxy`),
|
||||
containerName: stringField(record, "containerName", `${path}.accountLocalProxy`),
|
||||
secretName: stringField(record, "secretName", `${path}.accountLocalProxy`),
|
||||
secretKey: stringField(record, "secretKey", `${path}.accountLocalProxy`),
|
||||
image: stringField(record, "image", `${path}.accountLocalProxy`),
|
||||
imagePullPolicy: enumField(record, "imagePullPolicy", `${path}.accountLocalProxy`, ["Always", "IfNotPresent", "Never"] as const),
|
||||
listenHost: enumField(record, "listenHost", `${path}.accountLocalProxy`, ["127.0.0.1"] as const),
|
||||
listenPort: integerField(record, "listenPort", `${path}.accountLocalProxy`),
|
||||
sourceConfigRef,
|
||||
sourceFingerprint: source.fingerprint,
|
||||
sourceRef: source.sourceRef,
|
||||
sourceKey: source.sourceKey,
|
||||
sourceType: "master-shadowsocks",
|
||||
masterShadowsocks: source.masterShadowsocks,
|
||||
healthProbeUrl: source.healthProbeUrl,
|
||||
};
|
||||
validateAccountLocalProxyConfig(proxy, path);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
export function parseMasterShadowsocksConfig(value: unknown, path: string): Sub2ApiMasterShadowsocksConfig {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configPath}.${path} must be an object`);
|
||||
const record = value as Record<string, unknown>;
|
||||
@@ -582,3 +614,22 @@ export function validateEgressProxyConfig(config: Sub2ApiEgressProxyConfig, path
|
||||
const url = new URL(config.healthProbeUrl);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${configPath}.${path}.egressProxy.healthProbeUrl must use http:// or https://`);
|
||||
}
|
||||
|
||||
export function validateAccountLocalProxyConfig(config: Sub2ApiAccountLocalProxyConfig, path: string): void {
|
||||
validateKubernetesResourceName(config.containerName, `${path}.accountLocalProxy.containerName`);
|
||||
validateKubernetesResourceName(config.secretName, `${path}.accountLocalProxy.secretName`);
|
||||
if (config.secretKey !== "config.json") throw new Error(`${configPath}.${path}.accountLocalProxy.secretKey must be config.json`);
|
||||
if (!isImageReference(config.image)) throw new Error(`${configPath}.${path}.accountLocalProxy.image has an unsupported format`);
|
||||
if (config.listenHost !== "127.0.0.1") throw new Error(`${configPath}.${path}.accountLocalProxy.listenHost must be 127.0.0.1`);
|
||||
validatePort(config.listenPort, `${path}.accountLocalProxy.listenPort`);
|
||||
if (!config.sourceConfigRef.startsWith("config/") || !config.sourceConfigRef.includes("#sources.") || config.sourceConfigRef.includes("..")) {
|
||||
throw new Error(`${configPath}.${path}.accountLocalProxy.sourceConfigRef must reference config/*.yaml#sources.<id>`);
|
||||
}
|
||||
if (!/^[A-Za-z0-9_./-]+$/u.test(config.sourceRef)) throw new Error(`${configPath}.${path}.accountLocalProxy.sourceRef has an unsupported format`);
|
||||
if (!/^[A-Z0-9_]+$/u.test(config.sourceKey)) throw new Error(`${configPath}.${path}.accountLocalProxy.sourceKey must be an env key`);
|
||||
validateHostOrIp(config.masterShadowsocks.serverHost, `${path}.accountLocalProxy.masterShadowsocks.serverHost`);
|
||||
validatePort(config.masterShadowsocks.serverPort, `${path}.accountLocalProxy.masterShadowsocks.serverPort`);
|
||||
if (!/^[A-Za-z0-9-]+$/u.test(config.masterShadowsocks.method)) throw new Error(`${configPath}.${path}.accountLocalProxy.masterShadowsocks.method has an unsupported format`);
|
||||
const url = new URL(config.healthProbeUrl);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${configPath}.${path}.accountLocalProxy.healthProbeUrl must use http:// or https://`);
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ export interface Sub2ApiTargetConfig {
|
||||
hostDocker: Sub2ApiHostDockerConfig | null;
|
||||
publicExposure: Sub2ApiPublicExposureConfig | null;
|
||||
egressProxy: Sub2ApiEgressProxyConfig | null;
|
||||
accountLocalProxy: Sub2ApiAccountLocalProxyConfig | null;
|
||||
}
|
||||
|
||||
export interface Sub2ApiHostDockerConfig {
|
||||
@@ -206,6 +207,24 @@ export interface Sub2ApiEgressProxyConfig {
|
||||
healthProbeUrl: string;
|
||||
}
|
||||
|
||||
export interface Sub2ApiAccountLocalProxyConfig {
|
||||
enabled: boolean;
|
||||
containerName: string;
|
||||
secretName: string;
|
||||
secretKey: string;
|
||||
image: string;
|
||||
imagePullPolicy: "Always" | "IfNotPresent" | "Never";
|
||||
listenHost: "127.0.0.1";
|
||||
listenPort: number;
|
||||
sourceConfigRef: string;
|
||||
sourceFingerprint: string;
|
||||
sourceRef: string;
|
||||
sourceKey: string;
|
||||
sourceType: "master-shadowsocks";
|
||||
masterShadowsocks: Sub2ApiMasterShadowsocksConfig;
|
||||
healthProbeUrl: string;
|
||||
}
|
||||
|
||||
export interface Sub2ApiMasterShadowsocksConfig {
|
||||
serverHost: string;
|
||||
serverPort: number;
|
||||
@@ -275,6 +294,19 @@ export interface EgressProxySecretMaterial {
|
||||
valuesPrinted: false;
|
||||
}
|
||||
|
||||
export interface AccountLocalProxySecretMaterial {
|
||||
sourceRef: string;
|
||||
sourcePath: string;
|
||||
secretName: string;
|
||||
secretKey: string;
|
||||
fingerprint: string;
|
||||
sourceBytes: number;
|
||||
selectedOutbound: "shadowsocks";
|
||||
configJson: string;
|
||||
proxyUrl: string;
|
||||
valuesPrinted: false;
|
||||
}
|
||||
|
||||
export interface EgressProxySubscriptionCandidateSummary {
|
||||
sourceLine: number;
|
||||
kind: SubscriptionNode["kind"] | "shadowsocks";
|
||||
|
||||
@@ -14,7 +14,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 { ExternalDatabaseConfig, ManagedResourceCleanupPlan, Sub2ApiConfig, Sub2ApiEgressProxyConfig, Sub2ApiTargetConfig } from "./entry";
|
||||
import type { ExternalDatabaseConfig, ManagedResourceCleanupPlan, Sub2ApiAccountLocalProxyConfig, Sub2ApiConfig, Sub2ApiEgressProxyConfig, Sub2ApiTargetConfig } from "./entry";
|
||||
import { isKubernetesName, stringField } from "./config";
|
||||
import { codexPoolConfigPath, configPath, manifestPath } from "./entry";
|
||||
|
||||
@@ -229,6 +229,8 @@ export function externalPendingManifest(sub2api: Sub2ApiConfig, target: Sub2ApiT
|
||||
const publicExposure = renderPublicExposureManifest(target);
|
||||
const egressProxy = renderEgressProxyManifest(target);
|
||||
const proxyEnv = sub2ApiProxyEnv(target);
|
||||
const accountLocalProxyContainer = renderAccountLocalProxyContainer(target.accountLocalProxy);
|
||||
const accountLocalProxyVolume = renderAccountLocalProxyVolume(target.accountLocalProxy);
|
||||
return `apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
@@ -534,9 +536,11 @@ spec:
|
||||
volumeMounts:
|
||||
- name: sub2api-data
|
||||
mountPath: /app/data
|
||||
${accountLocalProxyContainer}
|
||||
volumes:
|
||||
- name: sub2api-data
|
||||
emptyDir: {}
|
||||
${accountLocalProxyVolume}
|
||||
${publicExposure}
|
||||
${egressProxy}
|
||||
`;
|
||||
@@ -551,6 +555,48 @@ export function sub2ApiProxyEnv(target: Sub2ApiTargetConfig): { httpProxy: strin
|
||||
};
|
||||
}
|
||||
|
||||
export function renderAccountLocalProxyContainer(proxy: Sub2ApiAccountLocalProxyConfig | null): string {
|
||||
if (proxy === null || !proxy.enabled) return "";
|
||||
return ` - name: ${proxy.containerName}
|
||||
image: ${proxy.image}
|
||||
imagePullPolicy: ${proxy.imagePullPolicy}
|
||||
args:
|
||||
- run
|
||||
- -c
|
||||
- /etc/sing-box/${proxy.secretKey}
|
||||
ports:
|
||||
- name: account-proxy
|
||||
containerPort: ${proxy.listenPort}
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: account-proxy
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: account-proxy
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
volumeMounts:
|
||||
- name: account-local-proxy-config
|
||||
mountPath: /etc/sing-box/${proxy.secretKey}
|
||||
subPath: ${proxy.secretKey}
|
||||
readOnly: true
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderAccountLocalProxyVolume(proxy: Sub2ApiAccountLocalProxyConfig | null): string {
|
||||
if (proxy === null || !proxy.enabled) return "";
|
||||
return ` - name: account-local-proxy-config
|
||||
secret:
|
||||
secretName: ${proxy.secretName}
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderPublicExposureManifest(target: Sub2ApiTargetConfig): string {
|
||||
const exposure = target.publicExposure;
|
||||
if (exposure === null || !exposure.enabled) return "";
|
||||
|
||||
@@ -14,7 +14,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 { EgressProxySecretMaterial, EgressProxySubscriptionDiagnostics, ExternalActiveSecretMaterial, PublicExposureSecretMaterial, Sub2ApiConfig, Sub2ApiEgressProxyConfig, Sub2ApiTargetConfig } from "./entry";
|
||||
import type { AccountLocalProxySecretMaterial, EgressProxySecretMaterial, EgressProxySubscriptionDiagnostics, ExternalActiveSecretMaterial, PublicExposureSecretMaterial, Sub2ApiAccountLocalProxyConfig, Sub2ApiConfig, Sub2ApiEgressProxyConfig, Sub2ApiTargetConfig } from "./entry";
|
||||
import { secretRoot, writeEnvFile } from "./apply-script";
|
||||
import { configPath, requiredSecretKeys } from "./entry";
|
||||
import { analyzeEgressProxySubscription, renderSingBoxConfig, renderSingBoxMasterShadowsocksConfig } from "./pk01-public-exposure";
|
||||
@@ -166,6 +166,63 @@ export function prepareEgressProxySecret(sub2api: Sub2ApiConfig, target: Sub2Api
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareAccountLocalProxySecret(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): AccountLocalProxySecretMaterial | null {
|
||||
const proxy = target.accountLocalProxy;
|
||||
if (proxy === null || !proxy.enabled) return null;
|
||||
const source = readEnvSourceFile({
|
||||
root: secretRoot(sub2api),
|
||||
sourceRef: proxy.sourceRef,
|
||||
missingMessage: (sourcePath) => `accountLocalProxy requires ${redactRepoPath(sourcePath)} with ${proxy.sourceKey}; materialize the declared proxy source first`,
|
||||
});
|
||||
const password = requiredEnvValue(source.values, proxy.sourceKey, proxy.sourceRef);
|
||||
const configJson = renderAccountLocalProxyConfig(proxy, password);
|
||||
return {
|
||||
sourceRef: proxy.sourceRef,
|
||||
sourcePath: source.sourcePathRedacted,
|
||||
secretName: proxy.secretName,
|
||||
secretKey: proxy.secretKey,
|
||||
fingerprint: fingerprintSecretValues({ password, configJson }, ["password", "configJson"]),
|
||||
sourceBytes: Buffer.byteLength(password, "utf8"),
|
||||
selectedOutbound: "shadowsocks",
|
||||
configJson,
|
||||
proxyUrl: `http://${proxy.listenHost}:${proxy.listenPort}`,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderAccountLocalProxyConfig(proxy: Sub2ApiAccountLocalProxyConfig, password: string): string {
|
||||
return `${JSON.stringify({
|
||||
log: { level: "info", timestamp: true },
|
||||
inbounds: [
|
||||
{
|
||||
type: "mixed",
|
||||
tag: "account-local",
|
||||
listen: proxy.listenHost,
|
||||
listen_port: proxy.listenPort,
|
||||
},
|
||||
],
|
||||
outbounds: [
|
||||
{
|
||||
type: "shadowsocks",
|
||||
tag: "master-vpn",
|
||||
server: proxy.masterShadowsocks.serverHost,
|
||||
server_port: proxy.masterShadowsocks.serverPort,
|
||||
method: proxy.masterShadowsocks.method,
|
||||
password,
|
||||
},
|
||||
{ type: "direct", tag: "direct" },
|
||||
{ type: "block", tag: "block" },
|
||||
],
|
||||
route: {
|
||||
rules: [
|
||||
{ ip_is_private: true, outbound: "direct" },
|
||||
{ domain_suffix: ["cluster.local", "svc"], outbound: "direct" },
|
||||
],
|
||||
final: "master-vpn",
|
||||
},
|
||||
}, null, 2)}\n`;
|
||||
}
|
||||
|
||||
export function fetchSubscription(subscriptionUrl: string): { body: string } {
|
||||
const shellUrl = shQuote(subscriptionUrl);
|
||||
const command = `curl -fsSL --connect-timeout 10 --max-time 30 ${shellUrl}`;
|
||||
|
||||
@@ -175,6 +175,7 @@ export function validateExternalActiveScript(sub2api: Sub2ApiConfig, target: Sub
|
||||
const cleanupPlan = managedResourceCleanupPlan(sub2api, target);
|
||||
const exposure = target.publicExposure?.enabled ? target.publicExposure : null;
|
||||
const proxy = target.egressProxy?.enabled ? target.egressProxy : null;
|
||||
const accountLocalProxy = target.accountLocalProxy?.enabled ? target.accountLocalProxy : null;
|
||||
const proxyUrl = proxy === null ? "" : `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`;
|
||||
const egressSubscriptionDiagnostics = proxy === null ? null : safeEgressProxySubscriptionDiagnostics(sub2api, proxy);
|
||||
const egressSubscriptionJson = egressSubscriptionDiagnostics === null ? "None" : `json.loads(${JSON.stringify(JSON.stringify(egressSubscriptionDiagnostics))})`;
|
||||
@@ -272,6 +273,38 @@ fi
|
||||
},
|
||||
}`;
|
||||
const publicProbeOkExpr = exposure === null ? "True" : "(public_dns_rc == 0 and public_probe_rc == 0)";
|
||||
const accountLocalProxyBlock = accountLocalProxy === null ? `
|
||||
account_local_proxy_probe_rc=0
|
||||
: >"$tmp/account-local-proxy-probe.out"
|
||||
: >"$tmp/account-local-proxy-probe.err"
|
||||
` : `
|
||||
if [ -n "$app_pod" ]; then
|
||||
kubectl -n ${target.namespace} exec "$app_pod" -c sub2api -- sh -c 'HTTP_PROXY="$1" HTTPS_PROXY="$1" ALL_PROXY="$1" NO_PROXY="$2" curl -fsS --max-time 20 -o /dev/null -w "status=%{http_code}\\n" "$3"' sh ${shQuote(`http://${accountLocalProxy.listenHost}:${accountLocalProxy.listenPort}`)} ${shQuote("localhost,127.0.0.1,::1,.svc,.cluster.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16")} ${shQuote(accountLocalProxy.healthProbeUrl)} >"$tmp/account-local-proxy-probe.out" 2>"$tmp/account-local-proxy-probe.err"
|
||||
account_local_proxy_probe_rc=$?
|
||||
else
|
||||
account_local_proxy_probe_rc=1
|
||||
printf '%s\\n' 'sub2api app pod not found' >"$tmp/account-local-proxy-probe.err"
|
||||
fi
|
||||
`;
|
||||
const accountLocalProxyJson = accountLocalProxy === null ? "None" : `{
|
||||
"enabled": True,
|
||||
"mode": "sub2api-pod-loopback-account-proxy",
|
||||
"containerName": "${accountLocalProxy.containerName}",
|
||||
"secretName": "${accountLocalProxy.secretName}",
|
||||
"proxyUrl": "http://${accountLocalProxy.listenHost}:${accountLocalProxy.listenPort}",
|
||||
"healthProbeUrl": "${accountLocalProxy.healthProbeUrl}",
|
||||
"sourceRef": "${accountLocalProxy.sourceRef}",
|
||||
"sourceConfigRef": "${accountLocalProxy.sourceConfigRef}",
|
||||
"sourceFingerprint": "${accountLocalProxy.sourceFingerprint}",
|
||||
"probe": {
|
||||
"exitCode": account_local_proxy_probe_rc,
|
||||
"failureFamily": egress_failure_family(account_local_proxy_probe_rc, text("account-local-proxy-probe.err", 2000), text("account-local-proxy-probe.out", 2000)),
|
||||
"stdout": text("account-local-proxy-probe.out", 2000),
|
||||
"stderr": text("account-local-proxy-probe.err", 2000),
|
||||
},
|
||||
"valuesPrinted": False,
|
||||
}`;
|
||||
const accountLocalProxyOkExpr = accountLocalProxy === null ? "True" : "(account_local_proxy_probe_rc == 0)";
|
||||
return `
|
||||
set -u
|
||||
tmp="$(mktemp -d)"
|
||||
@@ -312,13 +345,14 @@ else
|
||||
fi
|
||||
${publicProbeBlock}
|
||||
${egressProbeBlock}
|
||||
python3 - "$tmp" "$health_rc" "$root_rc" "$redis_rc" "$public_dns_rc" "$public_probe_rc" "$egress_deploy_rc" "$egress_svc_rc" "$egress_probe_rc" <<'PY'
|
||||
${accountLocalProxyBlock}
|
||||
python3 - "$tmp" "$health_rc" "$root_rc" "$redis_rc" "$public_dns_rc" "$public_probe_rc" "$egress_deploy_rc" "$egress_svc_rc" "$egress_probe_rc" "$account_local_proxy_probe_rc" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
tmp = sys.argv[1]
|
||||
health_rc, root_rc, redis_rc, public_dns_rc, public_probe_rc, egress_deploy_rc, egress_svc_rc, egress_probe_rc = [int(value) for value in sys.argv[2:]]
|
||||
health_rc, root_rc, redis_rc, public_dns_rc, public_probe_rc, egress_deploy_rc, egress_svc_rc, egress_probe_rc, account_local_proxy_probe_rc = [int(value) for value in sys.argv[2:]]
|
||||
|
||||
def rc(name):
|
||||
try:
|
||||
@@ -417,7 +451,7 @@ env_aligned = (
|
||||
and env.get("DATABASE_PASSWORD_PRESENT") == "true"
|
||||
)
|
||||
payload = {
|
||||
"ok": health_rc == 0 and root_rc == 0 and redis_rc == 0 and network_policy_ok and len(missing_secret_keys) == 0 and env_aligned and not local_postgres_present and not redis_pvc_present and ${publicProbeOkExpr} and ${egressProbeOkExpr},
|
||||
"ok": health_rc == 0 and root_rc == 0 and redis_rc == 0 and network_policy_ok and len(missing_secret_keys) == 0 and env_aligned and not local_postgres_present and not redis_pvc_present and ${publicProbeOkExpr} and ${egressProbeOkExpr} and ${accountLocalProxyOkExpr},
|
||||
"target": "${target.id}",
|
||||
"namespace": "${target.namespace}",
|
||||
"status": "external-db-active",
|
||||
@@ -434,6 +468,7 @@ payload = {
|
||||
},
|
||||
"publicExposure": ${publicProbeJson},
|
||||
"egressProxy": ${egressProbeJson},
|
||||
"accountLocalProxy": ${accountLocalProxyJson},
|
||||
"checks": {
|
||||
"allowAllNetworkPolicy": {"exitCode": rc("networkpolicy"), "ok": network_policy_ok, "stderr": text("networkpolicy.err", 2000)},
|
||||
"secretReady": {"ok": len(missing_secret_keys) == 0, "missingKeys": missing_secret_keys, "valuesPrinted": False},
|
||||
|
||||
Reference in New Issue
Block a user