Merge pull request #405 from pikasTech/feat/issue-404-sub2api-manual-binding-registry
#404 R2:Sub2API manual binding schema YAML 命名
This commit is contained in:
@@ -137,16 +137,27 @@ profiles:
|
|||||||
configFile: config.toml.socap
|
configFile: config.toml.socap
|
||||||
authFile: auth.json.socap
|
authFile: auth.json.socap
|
||||||
manualAccounts:
|
manualAccounts:
|
||||||
|
bindingSources:
|
||||||
|
active-target-egress-proxy:
|
||||||
|
enabled: true
|
||||||
|
kind: proxy
|
||||||
|
provider: target-egress-proxy
|
||||||
|
description: Bind a protected manual account to the selected target's YAML-declared egress proxy.
|
||||||
|
unified-pool-group:
|
||||||
|
enabled: true
|
||||||
|
kind: group
|
||||||
|
provider: pool-group
|
||||||
|
description: Attach a protected manual account to the unified Codex pool group.
|
||||||
protected:
|
protected:
|
||||||
- accountName: lucianepidgeon@gmail.com
|
- accountName: lucianepidgeon@gmail.com
|
||||||
reason: Manually configured in Sub2API; keep outside UniDesk-managed Codex pool and sentinel control.
|
reason: Manually configured in Sub2API; keep outside UniDesk-managed Codex pool and sentinel control.
|
||||||
proxyBinding:
|
proxyBinding:
|
||||||
enabled: true
|
enabled: true
|
||||||
source: target-egress-proxy
|
source: active-target-egress-proxy
|
||||||
proxyName: platform-infra-sub2api-egress-proxy
|
proxyName: platform-infra-sub2api-egress-proxy
|
||||||
groupBinding:
|
groupBinding:
|
||||||
enabled: true
|
enabled: true
|
||||||
source: pool-group
|
source: unified-pool-group
|
||||||
publicExposure:
|
publicExposure:
|
||||||
enabled: false
|
enabled: false
|
||||||
proxyName: platform-infra-sub2api
|
proxyName: platform-infra-sub2api
|
||||||
|
|||||||
@@ -156,18 +156,35 @@ interface CodexPoolConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CodexPoolManualAccountsConfig {
|
interface CodexPoolManualAccountsConfig {
|
||||||
|
bindingSources: CodexPoolManualBindingSourcesConfig;
|
||||||
protected: CodexPoolManualAccountProtection[];
|
protected: CodexPoolManualAccountProtection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CodexPoolManualBindingKind = "proxy" | "group";
|
||||||
|
type CodexPoolManualBindingProvider = "target-egress-proxy" | "pool-group";
|
||||||
|
|
||||||
|
interface CodexPoolManualBindingSource {
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
kind: CodexPoolManualBindingKind;
|
||||||
|
provider: CodexPoolManualBindingProvider;
|
||||||
|
description: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodexPoolManualBindingSourcesConfig {
|
||||||
|
items: CodexPoolManualBindingSource[];
|
||||||
|
byId: Record<string, CodexPoolManualBindingSource>;
|
||||||
|
}
|
||||||
|
|
||||||
interface CodexPoolManualAccountProxyBinding {
|
interface CodexPoolManualAccountProxyBinding {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
source: "target-egress-proxy";
|
source: string;
|
||||||
proxyName: string;
|
proxyName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodexPoolManualAccountGroupBinding {
|
interface CodexPoolManualAccountGroupBinding {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
source: "pool-group";
|
source: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodexPoolManualAccountProtection {
|
interface CodexPoolManualAccountProtection {
|
||||||
@@ -905,6 +922,10 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
|
|||||||
defaultAccountLoadFactor: pool.defaultAccountLoadFactor,
|
defaultAccountLoadFactor: pool.defaultAccountLoadFactor,
|
||||||
defaultSentinelProtect: pool.defaultSentinelProtect,
|
defaultSentinelProtect: pool.defaultSentinelProtect,
|
||||||
},
|
},
|
||||||
|
manualAccounts: {
|
||||||
|
bindingSources: pool.manualAccounts.bindingSources.items.map(manualBindingSourcePlan),
|
||||||
|
protected: resolvedManualAccountProtections(pool, runtimeTarget),
|
||||||
|
},
|
||||||
profiles: profiles.map((profile) => ({
|
profiles: profiles.map((profile) => ({
|
||||||
profile: profile.profile,
|
profile: profile.profile,
|
||||||
accountName: profile.accountName,
|
accountName: profile.accountName,
|
||||||
@@ -1514,6 +1535,7 @@ function desiredProfileCapacityTotal(profiles: CodexPoolProfileConfig[], default
|
|||||||
|
|
||||||
function readManualAccountsConfig(value: unknown): CodexPoolManualAccountsConfig {
|
function readManualAccountsConfig(value: unknown): CodexPoolManualAccountsConfig {
|
||||||
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.manualAccounts must be a YAML object`);
|
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.manualAccounts must be a YAML object`);
|
||||||
|
const bindingSources = readManualBindingSources(value.bindingSources, "manualAccounts.bindingSources");
|
||||||
const protectedRaw = value.protected;
|
const protectedRaw = value.protected;
|
||||||
if (!Array.isArray(protectedRaw)) throw new Error(`${codexPoolConfigPath}.manualAccounts.protected must be a YAML array`);
|
if (!Array.isArray(protectedRaw)) throw new Error(`${codexPoolConfigPath}.manualAccounts.protected must be a YAML array`);
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
@@ -1529,20 +1551,46 @@ function readManualAccountsConfig(value: unknown): CodexPoolManualAccountsConfig
|
|||||||
if (seen.has(normalized)) throw new Error(`${codexPoolConfigPath}.${key}.accountName is duplicated in manualAccounts.protected`);
|
if (seen.has(normalized)) throw new Error(`${codexPoolConfigPath}.${key}.accountName is duplicated in manualAccounts.protected`);
|
||||||
seen.add(normalized);
|
seen.add(normalized);
|
||||||
const reason = isRecord(entry) ? readManualAccountReason(entry.reason, `${key}.reason`) : null;
|
const reason = isRecord(entry) ? readManualAccountReason(entry.reason, `${key}.reason`) : null;
|
||||||
const proxyBinding = isRecord(entry) ? readManualAccountProxyBinding(entry.proxyBinding, `${key}.proxyBinding`) : null;
|
const proxyBinding = isRecord(entry) ? readManualAccountProxyBinding(entry.proxyBinding, `${key}.proxyBinding`, bindingSources) : null;
|
||||||
const groupBinding = isRecord(entry) ? readManualAccountGroupBinding(entry.groupBinding, `${key}.groupBinding`) : null;
|
const groupBinding = isRecord(entry) ? readManualAccountGroupBinding(entry.groupBinding, `${key}.groupBinding`, bindingSources) : null;
|
||||||
return { accountName, reason, proxyBinding, groupBinding };
|
return { accountName, reason, proxyBinding, groupBinding };
|
||||||
});
|
});
|
||||||
return { protected: protectedAccounts };
|
return { bindingSources, protected: protectedAccounts };
|
||||||
}
|
}
|
||||||
|
|
||||||
function readManualAccountProxyBinding(value: unknown, key: string): CodexPoolManualAccountProxyBinding | null {
|
function readManualBindingSources(value: unknown, key: string): CodexPoolManualBindingSourcesConfig {
|
||||||
|
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
|
||||||
|
const items: CodexPoolManualBindingSource[] = [];
|
||||||
|
const byId: Record<string, CodexPoolManualBindingSource> = {};
|
||||||
|
for (const [id, rawSource] of Object.entries(value)) {
|
||||||
|
const path = `${key}.${id}`;
|
||||||
|
validateManualBindingSourceId(id, path);
|
||||||
|
if (!isRecord(rawSource)) throw new Error(`${codexPoolConfigPath}.${path} must be a YAML object`);
|
||||||
|
const source: CodexPoolManualBindingSource = {
|
||||||
|
id,
|
||||||
|
enabled: readBooleanConfig(rawSource.enabled, `${path}.enabled`),
|
||||||
|
kind: readManualBindingKind(rawSource.kind, `${path}.kind`),
|
||||||
|
provider: readManualBindingProvider(rawSource.provider, `${path}.provider`),
|
||||||
|
description: readManualAccountReason(rawSource.description, `${path}.description`),
|
||||||
|
};
|
||||||
|
if (source.kind === "proxy" && source.provider !== "target-egress-proxy") {
|
||||||
|
throw new Error(`${codexPoolConfigPath}.${path}.provider must be target-egress-proxy for kind=proxy`);
|
||||||
|
}
|
||||||
|
if (source.kind === "group" && source.provider !== "pool-group") {
|
||||||
|
throw new Error(`${codexPoolConfigPath}.${path}.provider must be pool-group for kind=group`);
|
||||||
|
}
|
||||||
|
byId[id] = source;
|
||||||
|
items.push(source);
|
||||||
|
}
|
||||||
|
if (items.length === 0) throw new Error(`${codexPoolConfigPath}.${key} must declare at least one source`);
|
||||||
|
return { items, byId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function readManualAccountProxyBinding(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig): CodexPoolManualAccountProxyBinding | null {
|
||||||
if (value === undefined || value === null) return null;
|
if (value === undefined || value === null) return null;
|
||||||
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
|
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
|
||||||
const enabled = value.enabled === undefined ? true : value.enabled === true;
|
const enabled = readBooleanConfig(value.enabled, `${key}.enabled`);
|
||||||
const source = stringValue(value.source);
|
const source = readManualBindingSourceRef(value.source, `${key}.source`, bindingSources, "proxy", enabled);
|
||||||
if (source === null) throw new Error(`${codexPoolConfigPath}.${key}.source is required`);
|
|
||||||
if (source !== "target-egress-proxy") throw new Error(`${codexPoolConfigPath}.${key}.source must be target-egress-proxy`);
|
|
||||||
const proxyName = stringValue(value.proxyName);
|
const proxyName = stringValue(value.proxyName);
|
||||||
if (proxyName === null || proxyName.trim().length === 0) throw new Error(`${codexPoolConfigPath}.${key}.proxyName is required`);
|
if (proxyName === null || proxyName.trim().length === 0) throw new Error(`${codexPoolConfigPath}.${key}.proxyName is required`);
|
||||||
validateProxyName(proxyName, `${key}.proxyName`);
|
validateProxyName(proxyName, `${key}.proxyName`);
|
||||||
@@ -1553,19 +1601,44 @@ function readManualAccountProxyBinding(value: unknown, key: string): CodexPoolMa
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function readManualAccountGroupBinding(value: unknown, key: string): CodexPoolManualAccountGroupBinding | null {
|
function readManualAccountGroupBinding(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig): CodexPoolManualAccountGroupBinding | null {
|
||||||
if (value === undefined || value === null) return null;
|
if (value === undefined || value === null) return null;
|
||||||
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
|
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
|
||||||
const enabled = value.enabled === undefined ? true : value.enabled === true;
|
const enabled = readBooleanConfig(value.enabled, `${key}.enabled`);
|
||||||
const source = stringValue(value.source);
|
const source = readManualBindingSourceRef(value.source, `${key}.source`, bindingSources, "group", enabled);
|
||||||
if (source === null) throw new Error(`${codexPoolConfigPath}.${key}.source is required`);
|
|
||||||
if (source !== "pool-group") throw new Error(`${codexPoolConfigPath}.${key}.source must be pool-group`);
|
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
source,
|
source,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readManualBindingSourceRef(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig, expectedKind: CodexPoolManualBindingKind, bindingEnabled: boolean): string {
|
||||||
|
const sourceId = stringValue(value);
|
||||||
|
if (sourceId === null) throw new Error(`${codexPoolConfigPath}.${key} is required`);
|
||||||
|
validateManualBindingSourceId(sourceId, key);
|
||||||
|
const source = bindingSources.byId[sourceId];
|
||||||
|
if (source === undefined) throw new Error(`${codexPoolConfigPath}.${key} references unknown manualAccounts.bindingSources id ${sourceId}`);
|
||||||
|
if (source.kind !== expectedKind) throw new Error(`${codexPoolConfigPath}.${key} references ${sourceId} with kind=${source.kind}; expected ${expectedKind}`);
|
||||||
|
if (bindingEnabled && !source.enabled) throw new Error(`${codexPoolConfigPath}.${key} references disabled manualAccounts.bindingSources.${sourceId}`);
|
||||||
|
return sourceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readManualBindingKind(value: unknown, key: string): CodexPoolManualBindingKind {
|
||||||
|
const text = stringValue(value);
|
||||||
|
if (text !== "proxy" && text !== "group") throw new Error(`${codexPoolConfigPath}.${key} must be proxy or group`);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readManualBindingProvider(value: unknown, key: string): CodexPoolManualBindingProvider {
|
||||||
|
const text = stringValue(value);
|
||||||
|
if (text !== "target-egress-proxy" && text !== "pool-group") throw new Error(`${codexPoolConfigPath}.${key} must be target-egress-proxy or pool-group`);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateManualBindingSourceId(value: string, key: string): void {
|
||||||
|
if (!/^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported source id format`);
|
||||||
|
}
|
||||||
|
|
||||||
function readManualAccountName(value: unknown, key: string): string | null {
|
function readManualAccountName(value: unknown, key: string): string | null {
|
||||||
const text = stringValue(value)?.trim() ?? null;
|
const text = stringValue(value)?.trim() ?? null;
|
||||||
if (text === null || text.length === 0) return null;
|
if (text === null || text.length === 0) return null;
|
||||||
@@ -2079,6 +2152,7 @@ function codexPoolConfigSummary(pool: CodexPoolConfig, target: CodexPoolRuntimeT
|
|||||||
defaultSentinelProtect: pool.defaultSentinelProtect,
|
defaultSentinelProtect: pool.defaultSentinelProtect,
|
||||||
profileCount: pool.profiles.length,
|
profileCount: pool.profiles.length,
|
||||||
manualAccounts: {
|
manualAccounts: {
|
||||||
|
bindingSources: pool.manualAccounts.bindingSources.items.map(manualBindingSourcePlan),
|
||||||
protectedCount: pool.manualAccounts.protected.length,
|
protectedCount: pool.manualAccounts.protected.length,
|
||||||
protected: pool.manualAccounts.protected,
|
protected: pool.manualAccounts.protected,
|
||||||
controlPolicy: "manual accounts are not created, credential-updated, pruned, probed, or frozen by UniDesk codex-pool sync/sentinel; optional proxy_id and pool group membership bindings are narrow YAML-controlled exceptions",
|
controlPolicy: "manual accounts are not created, credential-updated, pruned, probed, or frozen by UniDesk codex-pool sync/sentinel; optional proxy_id and pool group membership bindings are narrow YAML-controlled exceptions",
|
||||||
@@ -3078,6 +3152,72 @@ function sentinelProfileSecrets(profiles: CodexProfile[]): CodexPoolSentinelProf
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvedManualAccountProtections(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record<string, unknown>[] {
|
||||||
|
return pool.manualAccounts.protected.map((account) => ({
|
||||||
|
accountName: account.accountName,
|
||||||
|
reason: account.reason,
|
||||||
|
proxyBinding: resolveManualProxyBinding(account.proxyBinding, pool, target),
|
||||||
|
groupBinding: resolveManualGroupBinding(account.groupBinding, pool),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveManualProxyBinding(binding: CodexPoolManualAccountProxyBinding | null, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record<string, unknown> | null {
|
||||||
|
if (binding === null) return null;
|
||||||
|
const source = manualBindingSource(pool, binding.source);
|
||||||
|
if (source.kind !== "proxy") throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${source.id} must have kind=proxy`);
|
||||||
|
if (binding.enabled && source.provider === "target-egress-proxy" && (target.egressProxy === null || target.egressProxy.enabled !== true)) {
|
||||||
|
throw new Error(`${codexPoolConfigPath}.manualAccounts.bindingSources.${source.id} requires ${sub2apiConfigPath}.targets[${target.id}].egressProxy.enabled=true`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
enabled: binding.enabled,
|
||||||
|
source: source.id,
|
||||||
|
proxyName: binding.proxyName,
|
||||||
|
sourcePlan: {
|
||||||
|
...manualBindingSourcePlan(source),
|
||||||
|
targetEgressProxy: source.provider === "target-egress-proxy" && target.egressProxy !== null
|
||||||
|
? {
|
||||||
|
targetId: target.id,
|
||||||
|
namespace: target.namespace,
|
||||||
|
serviceName: target.egressProxy.serviceName,
|
||||||
|
listenPort: target.egressProxy.listenPort,
|
||||||
|
httpProxy: target.egressProxy.httpProxy,
|
||||||
|
noProxy: target.egressProxy.noProxy,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveManualGroupBinding(binding: CodexPoolManualAccountGroupBinding | null, pool: CodexPoolConfig): Record<string, unknown> | null {
|
||||||
|
if (binding === null) return null;
|
||||||
|
const source = manualBindingSource(pool, binding.source);
|
||||||
|
if (source.kind !== "group") throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${source.id} must have kind=group`);
|
||||||
|
return {
|
||||||
|
enabled: binding.enabled,
|
||||||
|
source: source.id,
|
||||||
|
sourcePlan: {
|
||||||
|
...manualBindingSourcePlan(source),
|
||||||
|
poolGroupName: source.provider === "pool-group" ? pool.groupName : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function manualBindingSource(pool: CodexPoolConfig, sourceId: string): CodexPoolManualBindingSource {
|
||||||
|
const source = pool.manualAccounts.bindingSources.byId[sourceId];
|
||||||
|
if (source === undefined) throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${sourceId} is not declared`);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
function manualBindingSourcePlan(source: CodexPoolManualBindingSource): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
id: source.id,
|
||||||
|
enabled: source.enabled,
|
||||||
|
kind: source.kind,
|
||||||
|
provider: source.provider,
|
||||||
|
description: source.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function targetPublicExposureSummary(target: CodexPoolRuntimeTarget): Record<string, unknown> | null {
|
function targetPublicExposureSummary(target: CodexPoolRuntimeTarget): Record<string, unknown> | null {
|
||||||
const exposure = target.publicExposure;
|
const exposure = target.publicExposure;
|
||||||
if (exposure === null) return null;
|
if (exposure === null) return null;
|
||||||
@@ -4160,7 +4300,7 @@ EXPECTED_ACCOUNT_CAPACITIES = ${JSON.stringify(desiredAccountCapacityMap(pool))}
|
|||||||
EXPECTED_ACCOUNT_LOAD_FACTORS = ${JSON.stringify(desiredAccountLoadFactorMap(pool))}
|
EXPECTED_ACCOUNT_LOAD_FACTORS = ${JSON.stringify(desiredAccountLoadFactorMap(pool))}
|
||||||
EXPECTED_ACCOUNT_WS_MODES = json.loads(${JSON.stringify(JSON.stringify(desiredAccountWebSocketsV2ModeMap(pool)))})
|
EXPECTED_ACCOUNT_WS_MODES = json.loads(${JSON.stringify(JSON.stringify(desiredAccountWebSocketsV2ModeMap(pool)))})
|
||||||
EXPECTED_ACCOUNT_TEMP_UNSCHEDULABLE = json.loads(${JSON.stringify(JSON.stringify(desiredAccountTempUnschedulableMap(pool)))})
|
EXPECTED_ACCOUNT_TEMP_UNSCHEDULABLE = json.loads(${JSON.stringify(JSON.stringify(desiredAccountTempUnschedulableMap(pool)))})
|
||||||
MANUAL_ACCOUNT_PROTECTIONS = json.loads(${JSON.stringify(JSON.stringify(pool.manualAccounts.protected))})
|
MANUAL_ACCOUNT_PROTECTIONS = json.loads(${JSON.stringify(JSON.stringify(resolvedManualAccountProtections(pool, target)))})
|
||||||
SENTINEL_CONFIG = json.loads(${JSON.stringify(JSON.stringify(pool.sentinel))})
|
SENTINEL_CONFIG = json.loads(${JSON.stringify(JSON.stringify(pool.sentinel))})
|
||||||
TARGET_EGRESS_PROXY = json.loads(${JSON.stringify(JSON.stringify(target.egressProxy))})
|
TARGET_EGRESS_PROXY = json.loads(${JSON.stringify(JSON.stringify(target.egressProxy))})
|
||||||
MODE = "${mode}"
|
MODE = "${mode}"
|
||||||
@@ -4513,16 +4653,31 @@ def find_proxy_by_name(token, name):
|
|||||||
return item
|
return item
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def manual_binding_plan(binding, expected_kind):
|
||||||
|
if not isinstance(binding, dict):
|
||||||
|
return None
|
||||||
|
plan = binding.get("sourcePlan")
|
||||||
|
if not isinstance(plan, dict):
|
||||||
|
raise RuntimeError("manual account binding is missing resolved sourcePlan")
|
||||||
|
if plan.get("enabled") is not True:
|
||||||
|
raise RuntimeError(f"manual account binding source {plan.get('id')} is disabled")
|
||||||
|
if plan.get("kind") != expected_kind:
|
||||||
|
raise RuntimeError(f"manual account binding source {plan.get('id')} kind must be {expected_kind}")
|
||||||
|
return plan
|
||||||
|
|
||||||
def desired_manual_proxy_payload(protection):
|
def desired_manual_proxy_payload(protection):
|
||||||
binding = protection.get("proxyBinding") if isinstance(protection, dict) else None
|
binding = protection.get("proxyBinding") if isinstance(protection, dict) else None
|
||||||
if not isinstance(binding, dict) or binding.get("enabled") is not True:
|
if not isinstance(binding, dict) or binding.get("enabled") is not True:
|
||||||
return None
|
return None
|
||||||
if binding.get("source") != "target-egress-proxy":
|
plan = manual_binding_plan(binding, "proxy")
|
||||||
raise RuntimeError("manual account proxyBinding source must be target-egress-proxy")
|
if plan.get("provider") != "target-egress-proxy":
|
||||||
if not isinstance(TARGET_EGRESS_PROXY, dict) or TARGET_EGRESS_PROXY.get("enabled") is not True:
|
raise RuntimeError(f"manual account proxyBinding source {plan.get('id')} uses unsupported provider {plan.get('provider')}")
|
||||||
raise RuntimeError("manual account proxyBinding requires an enabled target egressProxy")
|
target_proxy = plan.get("targetEgressProxy")
|
||||||
service_name = TARGET_EGRESS_PROXY.get("serviceName")
|
if not isinstance(target_proxy, dict):
|
||||||
listen_port = TARGET_EGRESS_PROXY.get("listenPort")
|
raise RuntimeError(f"manual account proxyBinding source {plan.get('id')} requires an enabled target egressProxy")
|
||||||
|
service_name = target_proxy.get("serviceName")
|
||||||
|
listen_port = target_proxy.get("listenPort")
|
||||||
|
namespace = target_proxy.get("namespace") if isinstance(target_proxy.get("namespace"), str) and target_proxy.get("namespace") else NAMESPACE
|
||||||
proxy_name = binding.get("proxyName")
|
proxy_name = binding.get("proxyName")
|
||||||
if not isinstance(service_name, str) or not service_name:
|
if not isinstance(service_name, str) or not service_name:
|
||||||
raise RuntimeError("target egressProxy serviceName is missing")
|
raise RuntimeError("target egressProxy serviceName is missing")
|
||||||
@@ -4533,7 +4688,7 @@ def desired_manual_proxy_payload(protection):
|
|||||||
return {
|
return {
|
||||||
"name": proxy_name,
|
"name": proxy_name,
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"host": f"{service_name}.{NAMESPACE}.svc.cluster.local",
|
"host": f"{service_name}.{namespace}.svc.cluster.local",
|
||||||
"port": listen_port,
|
"port": listen_port,
|
||||||
"fallback_mode": "none",
|
"fallback_mode": "none",
|
||||||
"expiry_warn_days": 0,
|
"expiry_warn_days": 0,
|
||||||
@@ -4568,6 +4723,8 @@ def manual_proxy_status(token, account, protection):
|
|||||||
"action": "not-configured",
|
"action": "not-configured",
|
||||||
"valuesPrinted": False,
|
"valuesPrinted": False,
|
||||||
}
|
}
|
||||||
|
binding = protection.get("proxyBinding") if isinstance(protection, dict) else None
|
||||||
|
plan = manual_binding_plan(binding, "proxy")
|
||||||
proxy = find_proxy_by_name(token, payload["name"])
|
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
|
runtime_proxy = account.get("proxy") if isinstance(account, dict) and isinstance(account.get("proxy"), dict) else None
|
||||||
binding_aligned = (
|
binding_aligned = (
|
||||||
@@ -4579,7 +4736,8 @@ def manual_proxy_status(token, account, protection):
|
|||||||
"enabled": True,
|
"enabled": True,
|
||||||
"ok": binding_aligned,
|
"ok": binding_aligned,
|
||||||
"action": "validate",
|
"action": "validate",
|
||||||
"source": "target-egress-proxy",
|
"source": plan.get("id"),
|
||||||
|
"sourceProvider": plan.get("provider"),
|
||||||
"expectedProxyName": payload["name"],
|
"expectedProxyName": payload["name"],
|
||||||
"expectedProtocol": payload["protocol"],
|
"expectedProtocol": payload["protocol"],
|
||||||
"expectedHost": payload["host"],
|
"expectedHost": payload["host"],
|
||||||
@@ -4634,6 +4792,8 @@ def ensure_manual_account_proxy_bindings(token):
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
proxy, proxy_action = ensure_manual_proxy(token, payload)
|
proxy, proxy_action = ensure_manual_proxy(token, payload)
|
||||||
|
binding = protection.get("proxyBinding") if isinstance(protection, dict) else None
|
||||||
|
plan = manual_binding_plan(binding, "proxy")
|
||||||
proxy_id = proxy.get("id") if isinstance(proxy, dict) else None
|
proxy_id = proxy.get("id") if isinstance(proxy, dict) else None
|
||||||
if proxy_id is None:
|
if proxy_id is None:
|
||||||
raise RuntimeError(f"proxy {payload['name']} has no id")
|
raise RuntimeError(f"proxy {payload['name']} has no id")
|
||||||
@@ -4649,6 +4809,8 @@ def ensure_manual_account_proxy_bindings(token):
|
|||||||
"enabled": True,
|
"enabled": True,
|
||||||
"action": action,
|
"action": action,
|
||||||
"proxyAction": proxy_action,
|
"proxyAction": proxy_action,
|
||||||
|
"source": plan.get("id"),
|
||||||
|
"sourceProvider": plan.get("provider"),
|
||||||
"ok": account.get("proxy_id") == proxy_id,
|
"ok": account.get("proxy_id") == proxy_id,
|
||||||
"expectedProxyName": payload["name"],
|
"expectedProxyName": payload["name"],
|
||||||
"expectedProtocol": payload["protocol"],
|
"expectedProtocol": payload["protocol"],
|
||||||
@@ -4668,17 +4830,21 @@ def ensure_manual_account_proxy_bindings(token):
|
|||||||
"valuesPrinted": False,
|
"valuesPrinted": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def manual_group_binding_enabled(protection):
|
def manual_group_binding_plan(protection):
|
||||||
binding = protection.get("groupBinding") if isinstance(protection, dict) else None
|
binding = protection.get("groupBinding") if isinstance(protection, dict) else None
|
||||||
if not isinstance(binding, dict) or binding.get("enabled") is not True:
|
if not isinstance(binding, dict) or binding.get("enabled") is not True:
|
||||||
return False
|
return None
|
||||||
if binding.get("source") != "pool-group":
|
plan = manual_binding_plan(binding, "group")
|
||||||
raise RuntimeError("manual account groupBinding source must be pool-group")
|
if plan.get("provider") != "pool-group":
|
||||||
return True
|
raise RuntimeError(f"manual account groupBinding source {plan.get('id')} uses unsupported provider {plan.get('provider')}")
|
||||||
|
return plan
|
||||||
|
|
||||||
|
def manual_group_binding_enabled(protection):
|
||||||
|
return manual_group_binding_plan(protection) is not None
|
||||||
|
|
||||||
def manual_group_status(token, account, protection, group_id):
|
def manual_group_status(token, account, protection, group_id):
|
||||||
enabled = manual_group_binding_enabled(protection)
|
plan = manual_group_binding_plan(protection)
|
||||||
if not enabled:
|
if plan is None:
|
||||||
return {
|
return {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -4697,7 +4863,8 @@ def manual_group_status(token, account, protection, group_id):
|
|||||||
"enabled": True,
|
"enabled": True,
|
||||||
"ok": binding_aligned,
|
"ok": binding_aligned,
|
||||||
"action": "validate",
|
"action": "validate",
|
||||||
"source": "pool-group",
|
"source": plan.get("id"),
|
||||||
|
"sourceProvider": plan.get("provider"),
|
||||||
"poolGroupName": POOL_GROUP_NAME,
|
"poolGroupName": POOL_GROUP_NAME,
|
||||||
"poolGroupId": group_id,
|
"poolGroupId": group_id,
|
||||||
"bindingAligned": binding_aligned,
|
"bindingAligned": binding_aligned,
|
||||||
@@ -4721,6 +4888,7 @@ def ensure_manual_account_group_bindings(token, group_id):
|
|||||||
"valuesPrinted": False,
|
"valuesPrinted": False,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
plan = manual_group_binding_plan(protection)
|
||||||
account = find_account_by_name(token, name)
|
account = find_account_by_name(token, name)
|
||||||
if not isinstance(account, dict):
|
if not isinstance(account, dict):
|
||||||
items.append({
|
items.append({
|
||||||
@@ -4764,7 +4932,8 @@ def ensure_manual_account_group_bindings(token, group_id):
|
|||||||
"enabled": True,
|
"enabled": True,
|
||||||
"ok": binding_aligned,
|
"ok": binding_aligned,
|
||||||
"action": action,
|
"action": action,
|
||||||
"source": "pool-group",
|
"source": plan.get("id") if isinstance(plan, dict) else None,
|
||||||
|
"sourceProvider": plan.get("provider") if isinstance(plan, dict) else None,
|
||||||
"poolGroupName": POOL_GROUP_NAME,
|
"poolGroupName": POOL_GROUP_NAME,
|
||||||
"poolGroupId": group_id,
|
"poolGroupId": group_id,
|
||||||
"previousGroupIds": existing_group_ids,
|
"previousGroupIds": existing_group_ids,
|
||||||
@@ -6408,7 +6577,13 @@ def api_key_preview(api_key):
|
|||||||
return api_key[:10] + "..." + api_key[-4:]
|
return api_key[:10] + "..." + api_key[-4:]
|
||||||
|
|
||||||
def run_sync():
|
def run_sync():
|
||||||
|
global MANUAL_ACCOUNT_PROTECTIONS
|
||||||
payload = json.loads(base64.b64decode(PAYLOAD_B64).decode("utf-8"))
|
payload = json.loads(base64.b64decode(PAYLOAD_B64).decode("utf-8"))
|
||||||
|
manual_accounts_payload = payload.get("manualAccounts") if isinstance(payload.get("manualAccounts"), dict) else {}
|
||||||
|
resolved_manual_protections = manual_accounts_payload.get("protected")
|
||||||
|
if not isinstance(resolved_manual_protections, list):
|
||||||
|
raise RuntimeError("sync payload has no manualAccounts.protected binding plan")
|
||||||
|
MANUAL_ACCOUNT_PROTECTIONS = resolved_manual_protections
|
||||||
profiles = payload.get("profiles") or []
|
profiles = payload.get("profiles") or []
|
||||||
prune_removed = bool(payload.get("pruneRemoved"))
|
prune_removed = bool(payload.get("pruneRemoved"))
|
||||||
sentinel_payload = payload.get("sentinel") if isinstance(payload.get("sentinel"), dict) else {}
|
sentinel_payload = payload.get("sentinel") if isinstance(payload.get("sentinel"), dict) else {}
|
||||||
|
|||||||
Reference in New Issue
Block a user