Bind protected Sub2API manual accounts to proxy
This commit is contained in:
@@ -81,6 +81,8 @@ interface CodexPoolRuntimeTarget {
|
||||
egressProxy: {
|
||||
enabled: boolean;
|
||||
applyToSentinel: boolean;
|
||||
serviceName: string;
|
||||
listenPort: number;
|
||||
httpProxy: string;
|
||||
noProxy: string;
|
||||
} | null;
|
||||
@@ -156,9 +158,16 @@ interface CodexPoolManualAccountsConfig {
|
||||
protected: CodexPoolManualAccountProtection[];
|
||||
}
|
||||
|
||||
interface CodexPoolManualAccountProxyBinding {
|
||||
enabled: boolean;
|
||||
source: "target-egress-proxy";
|
||||
proxyName: string;
|
||||
}
|
||||
|
||||
interface CodexPoolManualAccountProtection {
|
||||
accountName: string;
|
||||
reason: string | null;
|
||||
proxyBinding: CodexPoolManualAccountProxyBinding | null;
|
||||
}
|
||||
|
||||
interface CodexPoolProfileConfig {
|
||||
@@ -631,6 +640,8 @@ function codexPoolRuntimeTarget(targetId?: string): CodexPoolRuntimeTarget {
|
||||
egressProxy = {
|
||||
enabled: true,
|
||||
applyToSentinel: raw.egressProxy.applyToSentinel === undefined ? true : raw.egressProxy.applyToSentinel === true,
|
||||
serviceName: proxyServiceName,
|
||||
listenPort,
|
||||
httpProxy: `http://${proxyServiceName}.${targetNamespace}.svc.cluster.local:${listenPort}`,
|
||||
noProxy,
|
||||
};
|
||||
@@ -1508,11 +1519,28 @@ function readManualAccountsConfig(value: unknown, defaults: CodexPoolManualAccou
|
||||
if (seen.has(normalized)) throw new Error(`${codexPoolConfigPath}.${key}.accountName is duplicated in manualAccounts.protected`);
|
||||
seen.add(normalized);
|
||||
const reason = isRecord(entry) ? readManualAccountReason(entry.reason, `${key}.reason`) : null;
|
||||
return { accountName, reason };
|
||||
const proxyBinding = isRecord(entry) ? readManualAccountProxyBinding(entry.proxyBinding, `${key}.proxyBinding`) : null;
|
||||
return { accountName, reason, proxyBinding };
|
||||
});
|
||||
return { protected: protectedAccounts };
|
||||
}
|
||||
|
||||
function readManualAccountProxyBinding(value: unknown, key: string): CodexPoolManualAccountProxyBinding | null {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
|
||||
const enabled = value.enabled === undefined ? true : value.enabled === true;
|
||||
const source = stringValue(value.source) ?? "target-egress-proxy";
|
||||
if (source !== "target-egress-proxy") throw new Error(`${codexPoolConfigPath}.${key}.source must be target-egress-proxy`);
|
||||
const proxyName = stringValue(value.proxyName);
|
||||
if (proxyName === null || proxyName.trim().length === 0) throw new Error(`${codexPoolConfigPath}.${key}.proxyName is required`);
|
||||
validateProxyName(proxyName, `${key}.proxyName`);
|
||||
return {
|
||||
enabled,
|
||||
source,
|
||||
proxyName,
|
||||
};
|
||||
}
|
||||
|
||||
function readManualAccountName(value: unknown, key: string): string | null {
|
||||
const text = stringValue(value)?.trim() ?? null;
|
||||
if (text === null || text.length === 0) return null;
|
||||
@@ -2122,18 +2150,42 @@ function compactManualAccounts(block: unknown): Record<string, unknown> | null {
|
||||
const items = recordArray(block.items).map((item) => pickSummaryFields(item, [
|
||||
"accountName",
|
||||
"reason",
|
||||
"ok",
|
||||
"exists",
|
||||
"accountId",
|
||||
"status",
|
||||
"schedulable",
|
||||
"inYamlProfiles",
|
||||
"runtimeMarkedUnideskManaged",
|
||||
"proxyBinding",
|
||||
"controlPolicy",
|
||||
]));
|
||||
const proxySync = isRecord(block.proxySync)
|
||||
? {
|
||||
ok: block.proxySync.ok,
|
||||
itemCount: block.proxySync.itemCount,
|
||||
items: recordArray(block.proxySync.items).map((item) => pickSummaryFields(item, [
|
||||
"accountName",
|
||||
"accountId",
|
||||
"enabled",
|
||||
"ok",
|
||||
"action",
|
||||
"proxyAction",
|
||||
"expectedProxyName",
|
||||
"proxyId",
|
||||
"runtimeProxyId",
|
||||
"runtimeProxyName",
|
||||
"bindingAligned",
|
||||
"controlPolicy",
|
||||
])),
|
||||
valuesPrinted: false,
|
||||
}
|
||||
: undefined;
|
||||
return {
|
||||
ok: block.ok,
|
||||
protectedCount: block.protectedCount,
|
||||
items,
|
||||
proxySync,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
@@ -2946,6 +2998,8 @@ function poolTarget(pool = readCodexPoolConfig(), target = codexPoolRuntimeTarge
|
||||
egressProxy: target.egressProxy === null ? null : {
|
||||
enabled: target.egressProxy.enabled,
|
||||
applyToSentinel: target.egressProxy.applyToSentinel,
|
||||
serviceName: target.egressProxy.serviceName,
|
||||
listenPort: target.egressProxy.listenPort,
|
||||
httpProxy: target.egressProxy.httpProxy,
|
||||
noProxy: target.egressProxy.noProxy,
|
||||
},
|
||||
@@ -4152,6 +4206,7 @@ EXPECTED_ACCOUNT_WS_MODES = json.loads(${JSON.stringify(JSON.stringify(desiredAc
|
||||
EXPECTED_ACCOUNT_TEMP_UNSCHEDULABLE = json.loads(${JSON.stringify(JSON.stringify(desiredAccountTempUnschedulableMap(pool)))})
|
||||
MANUAL_ACCOUNT_PROTECTIONS = json.loads(${JSON.stringify(JSON.stringify(pool.manualAccounts.protected))})
|
||||
SENTINEL_CONFIG = json.loads(${JSON.stringify(JSON.stringify(pool.sentinel))})
|
||||
TARGET_EGRESS_PROXY = json.loads(${JSON.stringify(JSON.stringify(target.egressProxy))})
|
||||
MODE = "${mode}"
|
||||
PAYLOAD_B64 = "${encodedPayload}"
|
||||
|
||||
@@ -4459,13 +4514,181 @@ def list_accounts(token):
|
||||
return extract_items(data)
|
||||
|
||||
def find_account_by_name(token, name):
|
||||
path = "/api/v1/admin/accounts?page=1&page_size=20&platform=openai&type=apikey&search=" + quote(str(name))
|
||||
path = "/api/v1/admin/accounts?page=1&page_size=50&platform=openai&search=" + quote(str(name))
|
||||
data = ensure_success(curl_api("GET", path, bearer=token), "find account " + str(name))
|
||||
for item in extract_items(data):
|
||||
if isinstance(item, dict) and item.get("name") == name:
|
||||
return item
|
||||
return None
|
||||
|
||||
def list_proxy_candidates(token, search=""):
|
||||
path = "/api/v1/admin/proxies?page=1&page_size=200"
|
||||
if search:
|
||||
path += "&search=" + quote(str(search))
|
||||
data = ensure_success(curl_api("GET", path, bearer=token), "list proxies")
|
||||
return extract_items(data)
|
||||
|
||||
def find_proxy_by_name(token, name):
|
||||
for item in list_proxy_candidates(token, name):
|
||||
if isinstance(item, dict) and item.get("name") == name:
|
||||
return item
|
||||
return None
|
||||
|
||||
def desired_manual_proxy_payload(protection):
|
||||
binding = protection.get("proxyBinding") if isinstance(protection, dict) else None
|
||||
if not isinstance(binding, dict) or binding.get("enabled") is not True:
|
||||
return None
|
||||
if binding.get("source") != "target-egress-proxy":
|
||||
raise RuntimeError("manual account proxyBinding source must be target-egress-proxy")
|
||||
if not isinstance(TARGET_EGRESS_PROXY, dict) or TARGET_EGRESS_PROXY.get("enabled") is not True:
|
||||
raise RuntimeError("manual account proxyBinding requires an enabled target egressProxy")
|
||||
service_name = TARGET_EGRESS_PROXY.get("serviceName")
|
||||
listen_port = TARGET_EGRESS_PROXY.get("listenPort")
|
||||
proxy_name = binding.get("proxyName")
|
||||
if not isinstance(service_name, str) or not service_name:
|
||||
raise RuntimeError("target egressProxy serviceName is missing")
|
||||
if not isinstance(listen_port, int) or listen_port <= 0:
|
||||
raise RuntimeError("target egressProxy listenPort is missing")
|
||||
if not isinstance(proxy_name, str) or not proxy_name:
|
||||
raise RuntimeError("manual account proxyBinding proxyName is missing")
|
||||
return {
|
||||
"name": proxy_name,
|
||||
"protocol": "http",
|
||||
"host": f"{service_name}.{NAMESPACE}.svc.cluster.local",
|
||||
"port": listen_port,
|
||||
"fallback_mode": "none",
|
||||
"expiry_warn_days": 0,
|
||||
}
|
||||
|
||||
def proxy_needs_update(proxy, payload):
|
||||
if not isinstance(proxy, dict):
|
||||
return True
|
||||
for key in ("name", "protocol", "host", "port", "fallback_mode", "expiry_warn_days"):
|
||||
if proxy.get(key) != payload.get(key):
|
||||
return True
|
||||
return proxy.get("status") != "active"
|
||||
|
||||
def ensure_manual_proxy(token, payload):
|
||||
current = find_proxy_by_name(token, payload["name"])
|
||||
if current is None:
|
||||
created = ensure_success(curl_api("POST", "/api/v1/admin/proxies", bearer=token, payload=payload), f"create proxy {payload['name']}")
|
||||
return created, "created"
|
||||
if proxy_needs_update(current, payload):
|
||||
update_payload = dict(payload)
|
||||
update_payload["status"] = "active"
|
||||
updated = ensure_success(curl_api("PUT", f"/api/v1/admin/proxies/{current['id']}", bearer=token, payload=update_payload), f"update proxy {payload['name']}")
|
||||
return updated if isinstance(updated, dict) else current, "updated"
|
||||
return current, "unchanged"
|
||||
|
||||
def manual_proxy_status(token, account, protection):
|
||||
payload = desired_manual_proxy_payload(protection)
|
||||
if payload is None:
|
||||
return {
|
||||
"enabled": False,
|
||||
"ok": True,
|
||||
"action": "not-configured",
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
proxy = find_proxy_by_name(token, payload["name"])
|
||||
runtime_proxy = account.get("proxy") if isinstance(account, dict) and isinstance(account.get("proxy"), dict) else None
|
||||
binding_aligned = (
|
||||
isinstance(account, dict)
|
||||
and isinstance(proxy, dict)
|
||||
and account.get("proxy_id") == proxy.get("id")
|
||||
)
|
||||
return {
|
||||
"enabled": True,
|
||||
"ok": binding_aligned,
|
||||
"action": "validate",
|
||||
"source": "target-egress-proxy",
|
||||
"expectedProxyName": payload["name"],
|
||||
"expectedProtocol": payload["protocol"],
|
||||
"expectedHost": payload["host"],
|
||||
"expectedPort": payload["port"],
|
||||
"proxyRecordExists": isinstance(proxy, dict),
|
||||
"proxyId": proxy.get("id") if isinstance(proxy, dict) else None,
|
||||
"runtimeProxyId": account.get("proxy_id") if isinstance(account, dict) else None,
|
||||
"runtimeProxyName": runtime_proxy.get("name") if isinstance(runtime_proxy, dict) else None,
|
||||
"bindingAligned": binding_aligned,
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
|
||||
def ensure_manual_account_proxy_bindings(token):
|
||||
items = []
|
||||
for protection in MANUAL_ACCOUNT_PROTECTIONS:
|
||||
if not isinstance(protection, dict):
|
||||
continue
|
||||
name = protection.get("accountName")
|
||||
if not isinstance(name, str) or not name:
|
||||
continue
|
||||
account = find_account_by_name(token, name)
|
||||
payload = desired_manual_proxy_payload(protection)
|
||||
if payload is None:
|
||||
items.append({
|
||||
"accountName": name,
|
||||
"enabled": False,
|
||||
"action": "not-configured",
|
||||
"ok": True,
|
||||
"valuesPrinted": False,
|
||||
})
|
||||
continue
|
||||
if not isinstance(account, dict):
|
||||
items.append({
|
||||
"accountName": name,
|
||||
"enabled": True,
|
||||
"action": "account-missing",
|
||||
"ok": False,
|
||||
"expectedProxyName": payload["name"],
|
||||
"valuesPrinted": False,
|
||||
})
|
||||
continue
|
||||
extra = account.get("extra") if isinstance(account.get("extra"), dict) else {}
|
||||
if extra.get("unidesk_managed") is True:
|
||||
items.append({
|
||||
"accountName": name,
|
||||
"accountId": account.get("id"),
|
||||
"enabled": True,
|
||||
"action": "refused-managed-runtime-account",
|
||||
"ok": False,
|
||||
"expectedProxyName": payload["name"],
|
||||
"valuesPrinted": False,
|
||||
})
|
||||
continue
|
||||
proxy, proxy_action = ensure_manual_proxy(token, payload)
|
||||
proxy_id = proxy.get("id") if isinstance(proxy, dict) else None
|
||||
if proxy_id is None:
|
||||
raise RuntimeError(f"proxy {payload['name']} has no id")
|
||||
action = "unchanged"
|
||||
if account.get("proxy_id") != proxy_id:
|
||||
updated = ensure_success(curl_api("PUT", f"/api/v1/admin/accounts/{account['id']}", bearer=token, payload={"proxy_id": proxy_id}), f"bind manual account proxy {name}")
|
||||
account = updated if isinstance(updated, dict) else account
|
||||
action = "bound"
|
||||
runtime_proxy = account.get("proxy") if isinstance(account.get("proxy"), dict) else None
|
||||
items.append({
|
||||
"accountName": name,
|
||||
"accountId": account.get("id"),
|
||||
"enabled": True,
|
||||
"action": action,
|
||||
"proxyAction": proxy_action,
|
||||
"ok": account.get("proxy_id") == proxy_id,
|
||||
"expectedProxyName": payload["name"],
|
||||
"expectedProtocol": payload["protocol"],
|
||||
"expectedHost": payload["host"],
|
||||
"expectedPort": payload["port"],
|
||||
"proxyId": proxy_id,
|
||||
"runtimeProxyId": account.get("proxy_id"),
|
||||
"runtimeProxyName": runtime_proxy.get("name") if isinstance(runtime_proxy, dict) else None,
|
||||
"bindingAligned": account.get("proxy_id") == proxy_id,
|
||||
"controlPolicy": "manual-protected: only proxy_id binding is YAML-controlled; credentials/status/schedulable are untouched",
|
||||
"valuesPrinted": False,
|
||||
})
|
||||
return {
|
||||
"ok": all(item.get("ok") is True for item in items),
|
||||
"itemCount": len(items),
|
||||
"items": items,
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
|
||||
def manual_account_protection_status(token):
|
||||
items = []
|
||||
desired_names = set(EXPECTED_ACCOUNT_CAPACITIES.keys())
|
||||
@@ -4477,6 +4700,7 @@ def manual_account_protection_status(token):
|
||||
continue
|
||||
account = find_account_by_name(token, name)
|
||||
extra = account.get("extra") if isinstance(account, dict) and isinstance(account.get("extra"), dict) else {}
|
||||
proxy_status = manual_proxy_status(token, account, protection)
|
||||
items.append({
|
||||
"accountName": name,
|
||||
"reason": protection.get("reason") if isinstance(protection.get("reason"), str) else None,
|
||||
@@ -4486,11 +4710,13 @@ def manual_account_protection_status(token):
|
||||
"schedulable": account.get("schedulable") if isinstance(account, dict) else None,
|
||||
"inYamlProfiles": name in desired_names,
|
||||
"runtimeMarkedUnideskManaged": extra.get("unidesk_managed") is True,
|
||||
"controlPolicy": "manual-protected: no create/update/prune/probe/freeze",
|
||||
"proxyBinding": proxy_status,
|
||||
"ok": proxy_status.get("ok") is True,
|
||||
"controlPolicy": "manual-protected: no create/update/prune/probe/freeze; optional proxy_id binding only when proxyBinding is configured",
|
||||
"valuesPrinted": False,
|
||||
})
|
||||
return {
|
||||
"ok": True,
|
||||
"ok": all(item.get("ok") is True for item in items),
|
||||
"protectedCount": len(items),
|
||||
"items": items,
|
||||
"valuesPrinted": False,
|
||||
@@ -6101,13 +6327,14 @@ def run_sync():
|
||||
if group_id is None:
|
||||
raise RuntimeError("pool group id missing after ensure")
|
||||
existing_accounts = list_accounts(token)
|
||||
manual_account_protections = manual_account_protection_status(token)
|
||||
planned_account_results = planned_sentinel_account_results(profiles, existing_accounts)
|
||||
sentinel_quality_prepare = ensure_sentinel_state_for_sync(planned_account_results, True)
|
||||
if sentinel_quality_prepare.get("ok") is not True:
|
||||
raise RuntimeError("prepare sentinel pending probe failed: " + json.dumps(sentinel_quality_prepare, ensure_ascii=False))
|
||||
protected_frozen_names = active_sentinel_quarantine_names()
|
||||
account_results, pruned_account_results = ensure_accounts(token, profiles, group_id, prune_removed, protected_frozen_names, existing_accounts)
|
||||
manual_account_proxy_bindings = ensure_manual_account_proxy_bindings(token)
|
||||
manual_account_protections = manual_account_protection_status(token)
|
||||
capacity_status = account_capacity_status(token)
|
||||
load_factor_status = account_load_factor_status(token)
|
||||
ws_v2_status = account_ws_v2_status(token)
|
||||
@@ -6125,7 +6352,7 @@ def run_sync():
|
||||
sentinel_quality = ensure_sentinel_state_for_sync(account_results)
|
||||
sentinel_reassert = reassert_sentinel_freezes_after_sync(token)
|
||||
return {
|
||||
"ok": gateway["ok"] is True and responses_smoke["ok"] is True and owner_concurrency["ok"] is True and capacity_status["ok"] is True and load_factor_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True and sentinel.get("ok") is True and sentinel_quality_prepare.get("ok") is True and sentinel_quality.get("ok") is True and sentinel_reassert.get("ok") is True and runtime_capabilities.get("ok") is True,
|
||||
"ok": gateway["ok"] is True and responses_smoke["ok"] is True and owner_concurrency["ok"] is True and capacity_status["ok"] is True and load_factor_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True and manual_account_proxy_bindings.get("ok") is True and manual_account_protections.get("ok") is True and sentinel.get("ok") is True and sentinel_quality_prepare.get("ok") is True and sentinel_quality.get("ok") is True and sentinel_reassert.get("ok") is True and runtime_capabilities.get("ok") is True,
|
||||
"degraded": bool(responses_smoke.get("degraded")) or bool(compact_evidence.get("degraded")) or bool(responses_evidence.get("degraded")) or runtime_capabilities.get("ok") is not True,
|
||||
"mode": "sync",
|
||||
"namespace": NAMESPACE,
|
||||
@@ -6144,7 +6371,7 @@ def run_sync():
|
||||
"processControl": {"schedulableRestore": "sentinel marker probe only; sync does not restore schedulable for existing accounts", "durableConfig": False},
|
||||
"valuesPrinted": False,
|
||||
},
|
||||
"manualAccounts": manual_account_protections,
|
||||
"manualAccounts": {**manual_account_protections, "proxySync": manual_account_proxy_bindings},
|
||||
"capacity": capacity_status,
|
||||
"loadFactor": load_factor_status,
|
||||
"webSocketsV2": ws_v2_status,
|
||||
@@ -6191,7 +6418,7 @@ def run_validate():
|
||||
runtime_capabilities = validate_runtime_capabilities(token)
|
||||
sentinel = sentinel_runtime_status()
|
||||
return {
|
||||
"ok": gateway["ok"] is True and responses_smoke["ok"] is True and (owner_concurrency is None or owner_concurrency["ok"] is True) and capacity_status["ok"] is True and load_factor_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True and sentinel.get("ok") is True and runtime_capabilities.get("ok") is True,
|
||||
"ok": gateway["ok"] is True and responses_smoke["ok"] is True and (owner_concurrency is None or owner_concurrency["ok"] is True) and capacity_status["ok"] is True and load_factor_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True and manual_account_protections.get("ok") is True and sentinel.get("ok") is True and runtime_capabilities.get("ok") is True,
|
||||
"degraded": bool(responses_smoke.get("degraded")) or bool(compact_evidence.get("degraded")) or bool(responses_evidence.get("degraded")) or runtime_capabilities.get("ok") is not True,
|
||||
"mode": "validate",
|
||||
"namespace": NAMESPACE,
|
||||
|
||||
Reference in New Issue
Block a user