This commit is contained in:
@@ -15,10 +15,11 @@ import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-
|
||||
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
|
||||
|
||||
import type { AccountLocalProxySecretMaterial, EgressProxySecretMaterial, ExternalActiveSecretMaterial, PublicExposureSecretMaterial, Sub2ApiConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
|
||||
import { fieldManager, requiredSecretKeys, sub2apiCaddyManagedMarker } from "./entry";
|
||||
import { fieldManager, requiredSecretKeys, serviceName, sub2apiCaddyManagedMarker } from "./entry";
|
||||
import { managedResourceCleanupPlan } from "./manifest";
|
||||
|
||||
export function renderPk01Caddyfile(target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): string {
|
||||
if (exposure.pk01 === null) throw new Error(`publicExposure.pk01 is required for target ${target.id} when rendering PK01 Caddy`);
|
||||
const apexHost = baseDomain(exposure.dns.hostname);
|
||||
const apiBlock = renderCaddyManagedBlock(
|
||||
sub2apiCaddyManagedMarker(target),
|
||||
@@ -48,11 +49,16 @@ export function publicExposureUpstream(target: Sub2ApiTargetConfig, exposure: Su
|
||||
if (exposure.local === null) throw new Error(`publicExposure.local is required for target ${target.id} when mode=pk01-local`);
|
||||
return `${exposure.local.upstreamHost}:${exposure.local.upstreamPort}`;
|
||||
}
|
||||
if (exposure.mode === "node-local-https") {
|
||||
if (exposure.localHttps === null) throw new Error(`publicExposure.localHttps is required for target ${target.id} when mode=node-local-https`);
|
||||
return `${exposure.dns.hostname}:${exposure.localHttps.httpsPort}`;
|
||||
}
|
||||
if (exposure.frpc === null) throw new Error(`publicExposure.frpc is required for target ${target.id} when mode=frp`);
|
||||
return `127.0.0.1:${exposure.frpc.remotePort}`;
|
||||
}
|
||||
|
||||
export function renderPk01CaddyService(exposure: Sub2ApiPublicExposureConfig): string {
|
||||
if (exposure.pk01 === null) throw new Error("publicExposure.pk01 is required when rendering PK01 Caddy service");
|
||||
return `[Unit]
|
||||
Description=UniDesk PK01 Caddy edge for PikaPython and Sub2API
|
||||
After=network-online.target docker.service
|
||||
@@ -227,6 +233,8 @@ export function applyScript(
|
||||
): string {
|
||||
const encoded = Buffer.from(yaml, "utf8").toString("base64");
|
||||
const cleanupPlan = managedResourceCleanupPlan(sub2api, target);
|
||||
const localHttpsExposure = target.publicExposure?.enabled === true && target.publicExposure.mode === "node-local-https" ? target.publicExposure : null;
|
||||
const localHttps = localHttpsExposure?.localHttps ?? null;
|
||||
const appSecretName = sub2api.runtime.database.secretName;
|
||||
const publicExposureSecretName = publicExposureSecretMaterial?.secretName ?? sub2api.defaults.cleanup.publicExposure.secretName;
|
||||
const publicExposureSecretKey = publicExposureSecretMaterial?.secretKey ?? "frpc.toml";
|
||||
@@ -299,6 +307,24 @@ export function applyScript(
|
||||
proxyUrl: accountLocalProxySecretMaterial.proxyUrl,
|
||||
valuesPrinted: false,
|
||||
}))})`;
|
||||
const localHttpsSummary = localHttps === null
|
||||
? "None"
|
||||
: `json.loads(${JSON.stringify(JSON.stringify({
|
||||
publicBaseUrl: localHttpsExposure?.publicBaseUrl,
|
||||
hostname: localHttpsExposure?.dns.hostname,
|
||||
expectedA: localHttpsExposure?.dns.expectedA,
|
||||
mode: "node-local-docker-caddy",
|
||||
route: localHttps.route,
|
||||
upstreamMode: localHttps.upstreamMode,
|
||||
composePath: localHttps.composePath,
|
||||
projectName: localHttps.projectName,
|
||||
serviceName: localHttps.serviceName,
|
||||
containerName: localHttps.containerName,
|
||||
image: localHttps.image,
|
||||
bind: `${localHttps.hostBind}:${localHttps.httpsPort}`,
|
||||
tlsMode: localHttps.tlsMode,
|
||||
valuesPrinted: false,
|
||||
}))})`;
|
||||
return `
|
||||
set -u
|
||||
tmp="$(mktemp -d)"
|
||||
@@ -319,6 +345,8 @@ apply_out="$tmp/apply.out"
|
||||
apply_err="$tmp/apply.err"
|
||||
cleanup_out="$tmp/cleanup.out"
|
||||
cleanup_err="$tmp/cleanup.err"
|
||||
local_https_out="$tmp/local-https.out"
|
||||
local_https_err="$tmp/local-https.err"
|
||||
kubectl create namespace ${target.namespace} --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$ns_out" 2>"$ns_err"
|
||||
ns_rc=$?
|
||||
secret_action="unknown"
|
||||
@@ -329,6 +357,8 @@ egress_secret_action="not-enabled"
|
||||
egress_secret_rc=0
|
||||
account_local_proxy_secret_action="not-enabled"
|
||||
account_local_proxy_secret_rc=0
|
||||
local_https_action="not-enabled"
|
||||
local_https_rc=0
|
||||
if [ "$ns_rc" -eq 0 ]; then
|
||||
if [ "${target.databaseMode}" = "external-pending" ]; then
|
||||
secret_action="external-pending-not-managed"
|
||||
@@ -506,7 +536,71 @@ 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" "$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'
|
||||
if [ "${localHttps === null ? "false" : "true"}" = "true" ]; then
|
||||
if [ "$apply_rc" -eq 0 ] && [ "$cleanup_rc" -eq 0 ]; then
|
||||
edge_dir=${localHttps === null ? "\"\"" : shQuote(localHttps.workDir)}
|
||||
compose_path=${localHttps === null ? "\"\"" : shQuote(localHttps.composePath)}
|
||||
caddyfile_path=${localHttps === null ? "\"\"" : shQuote(localHttps.caddyfilePath)}
|
||||
data_dir=${localHttps === null ? "\"\"" : shQuote(localHttps.dataDir)}
|
||||
config_dir=${localHttps === null ? "\"\"" : shQuote(localHttps.configDir)}
|
||||
mkdir -p "$edge_dir" "$(dirname "$compose_path")" "$(dirname "$caddyfile_path")" "$data_dir" "$config_dir" >"$local_https_out" 2>"$local_https_err"
|
||||
local_https_rc=$?
|
||||
if [ "$local_https_rc" -eq 0 ]; then
|
||||
service_cluster_ip="$(kubectl -n ${target.namespace} get service ${serviceName} -o jsonpath='{.spec.clusterIP}' 2>>"$local_https_err" || true)"
|
||||
if [ -z "$service_cluster_ip" ] || [ "$service_cluster_ip" = "None" ]; then
|
||||
printf '%s\\n' 'sub2api service clusterIP is empty; cannot render node-local HTTPS edge' >>"$local_https_err"
|
||||
local_https_rc=1
|
||||
fi
|
||||
fi
|
||||
if [ "$local_https_rc" -eq 0 ]; then
|
||||
cat >"$caddyfile_path" <<'CADDY'
|
||||
https://${localHttpsExposure?.dns.hostname}:${localHttps?.httpsPort} {
|
||||
tls internal
|
||||
reverse_proxy __SUB2API_SERVICE_CLUSTER_IP__:8080 {
|
||||
transport http {
|
||||
response_header_timeout ${localHttps?.responseHeaderTimeoutSeconds}s
|
||||
}
|
||||
}
|
||||
}
|
||||
CADDY
|
||||
sed -i "s/__SUB2API_SERVICE_CLUSTER_IP__/$service_cluster_ip/g" "$caddyfile_path"
|
||||
cat >"$compose_path" <<'COMPOSE'
|
||||
name: ${localHttps?.projectName}
|
||||
|
||||
services:
|
||||
${localHttps?.serviceName}:
|
||||
image: ${localHttps?.image}
|
||||
container_name: ${localHttps?.containerName}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${localHttps?.hostBind}:${localHttps?.httpsPort}:${localHttps?.httpsPort}"
|
||||
volumes:
|
||||
- ${localHttps?.caddyfilePath}:/etc/caddy/Caddyfile:ro
|
||||
- ${localHttps?.dataDir}:/data
|
||||
- ${localHttps?.configDir}:/config
|
||||
COMPOSE
|
||||
chmod 0644 "$caddyfile_path" "$compose_path" >>"$local_https_out" 2>>"$local_https_err" || local_https_rc=$?
|
||||
fi
|
||||
if [ "$local_https_rc" -eq 0 ]; then
|
||||
docker compose -f "$compose_path" -p ${localHttps === null ? "\"\"" : shQuote(localHttps.projectName)} up -d --force-recreate --remove-orphans >>"$local_https_out" 2>>"$local_https_err"
|
||||
local_https_rc=$?
|
||||
fi
|
||||
if [ "$local_https_rc" -eq 0 ]; then
|
||||
sleep 2
|
||||
curl --noproxy '*' -kfsS --connect-timeout 5 --max-time 20 --resolve ${localHttpsExposure === null || localHttps === null ? "\"\"" : shQuote(`${localHttpsExposure.dns.hostname}:${localHttps.httpsPort}:127.0.0.1`)} ${localHttpsExposure === null ? "\"\"" : shQuote(`${localHttpsExposure.publicBaseUrl}/health`)} >>"$local_https_out" 2>>"$local_https_err"
|
||||
local_https_rc=$?
|
||||
fi
|
||||
local_https_action="synced"
|
||||
else
|
||||
printf '%s\\n' 'skipped because Kubernetes apply or cleanup failed' >"$local_https_err"
|
||||
: >"$local_https_out"
|
||||
local_https_action="skipped-after-k8s-failure"
|
||||
fi
|
||||
else
|
||||
: >"$local_https_out"
|
||||
: >"$local_https_err"
|
||||
fi
|
||||
python3 - "$ns_rc" "$secret_rc" "$exposure_secret_rc" "$egress_secret_rc" "$account_local_proxy_secret_rc" "$apply_rc" "$cleanup_rc" "$local_https_rc" "$secret_action" "$exposure_secret_action" "$egress_secret_action" "$account_local_proxy_secret_action" "$local_https_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" "$local_https_out" "$local_https_err" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
ns_rc = int(sys.argv[1])
|
||||
@@ -516,11 +610,13 @@ egress_secret_rc = int(sys.argv[4])
|
||||
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:]
|
||||
local_https_rc = int(sys.argv[8])
|
||||
secret_action = sys.argv[9]
|
||||
exposure_secret_action = sys.argv[10]
|
||||
egress_secret_action = sys.argv[11]
|
||||
account_local_proxy_secret_action = sys.argv[12]
|
||||
local_https_action = sys.argv[13]
|
||||
paths = sys.argv[14:]
|
||||
def text(path):
|
||||
try:
|
||||
return open(path, encoding="utf-8").read()
|
||||
@@ -532,7 +628,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 account_local_proxy_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 and local_https_rc == 0,
|
||||
"target": "${target.id}",
|
||||
"namespace": "${target.namespace}",
|
||||
"databaseMode": "${target.databaseMode}",
|
||||
@@ -547,6 +643,7 @@ payload = {
|
||||
"valuesPrinted": False,
|
||||
},
|
||||
"publicExposure": ${exposureSecretSummary},
|
||||
"localHttpsExposure": ${localHttpsSummary},
|
||||
"egressProxy": ${egressProxySecretSummary},
|
||||
"accountLocalProxy": ${accountLocalProxySecretSummary},
|
||||
"steps": {
|
||||
@@ -557,6 +654,7 @@ payload = {
|
||||
"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:]},
|
||||
"localHttpsExposure": {"exitCode": local_https_rc, "action": local_https_action, "stdout": text(paths[14])[-4000:], "stderr": text(paths[15])[-4000:]},
|
||||
},
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
|
||||
Reference in New Issue
Block a user