708 lines
27 KiB
TypeScript
708 lines
27 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. redaction module for scripts/src/platform-infra-sub2api-codex.ts.
|
|
|
|
// Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:2035-2700 for #903.
|
|
|
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
import { homedir } from "node:os";
|
|
import { join } from "node:path";
|
|
import type { UniDeskConfig } from "../config";
|
|
import { rootPath } from "../config";
|
|
import type { RenderedCliResult } from "../output";
|
|
import { applyPk01CaddyBlock, prepareFrpcSecret, renderFrpcManifest, type PublicServiceExposure, type PublicServiceTarget } from "../platform-infra-public-service";
|
|
import { shortSha256Fingerprint } from "../platform-infra-ops-library";
|
|
import {
|
|
codexPoolSentinelSummary,
|
|
codexPoolSentinelRuntimeImage,
|
|
readCodexPoolSentinelConfig,
|
|
renderCodexPoolSentinelManifest,
|
|
type CodexPoolSentinelConfig,
|
|
type CodexPoolSentinelProfileSecret,
|
|
} from "../platform-infra-sub2api-codex-sentinel";
|
|
import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
|
|
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
|
|
|
|
import type { CodexPoolConfig, CodexPoolRuntimeTarget, CodexProfile, CodexTempUnschedulablePolicy } from "./types";
|
|
import { desiredAccountCapacityTotal } from "./accounts";
|
|
import { fingerprint, isRecord } from "./config-utils";
|
|
import { manualBindingSourcePlan, targetPublicExposureSummary } from "./public-exposure";
|
|
|
|
export function readAuthAPIKey(authPath: string): { apiKey: string | null; shape: string } {
|
|
if (!existsSync(authPath)) return { apiKey: null, shape: "missing" };
|
|
const parsed = JSON.parse(readFileSync(authPath, "utf8")) as unknown;
|
|
if (!isRecord(parsed)) return { apiKey: null, shape: "non-object" };
|
|
const value = parsed.OPENAI_API_KEY;
|
|
const shape = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
if (typeof value === "string" && value.length > 0) return { apiKey: value, shape };
|
|
return { apiKey: null, shape };
|
|
}
|
|
|
|
export function uniqueAccountName(profile: string, seen: Set<string>): string {
|
|
const normalized = profile
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9._-]+/gu, "-")
|
|
.replace(/^-+|-+$/gu, "") || "default";
|
|
let candidate = `unidesk-codex-${normalized}`;
|
|
let counter = 2;
|
|
while (seen.has(candidate)) {
|
|
candidate = `unidesk-codex-${normalized}-${counter}`;
|
|
counter += 1;
|
|
}
|
|
seen.add(candidate);
|
|
return candidate;
|
|
}
|
|
|
|
export function redactProfile(profile: CodexProfile): Record<string, unknown> {
|
|
return {
|
|
profile: profile.profile,
|
|
accountName: profile.accountName,
|
|
configFile: profile.configFile,
|
|
authFile: profile.authFile,
|
|
provider: profile.provider || null,
|
|
baseUrl: profile.baseUrl || null,
|
|
wireApi: profile.wireApi,
|
|
model: profile.model,
|
|
envKey: profile.envKey,
|
|
apiKeySource: profile.apiKeySource,
|
|
openaiResponsesWebSocketsV2Mode: profile.openaiResponsesWebSocketsV2Mode,
|
|
upstreamUserAgent: profile.upstreamUserAgent,
|
|
trustUpstream: profile.trustUpstream,
|
|
sentinelProtect: profile.sentinelProtect,
|
|
priority: profile.priority,
|
|
capacity: profile.capacity,
|
|
loadFactor: profile.loadFactor,
|
|
tempUnschedulable: tempUnschedulableSummary(profile.tempUnschedulable),
|
|
apiKeyPresent: profile.apiKey !== null && profile.apiKey.length > 0,
|
|
apiKeyBytes: profile.apiKey === null ? 0 : Buffer.byteLength(profile.apiKey, "utf8"),
|
|
apiKeyFingerprint: profile.apiKey === null ? null : fingerprint(profile.apiKey),
|
|
authOpenAIKeyShape: profile.authOpenAIKeyShape,
|
|
ok: profile.ok,
|
|
error: profile.error,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function compactProfile(profile: CodexProfile): Record<string, unknown> {
|
|
return {
|
|
profile: profile.profile,
|
|
accountName: profile.accountName,
|
|
provider: profile.provider || null,
|
|
model: profile.model,
|
|
priority: profile.priority,
|
|
trustUpstream: profile.trustUpstream,
|
|
sentinelProtectEnabled: profile.sentinelProtect.enabled,
|
|
sentinelProtectConsecutiveFailures: profile.sentinelProtect.enabled ? profile.sentinelProtect.consecutiveFailures : undefined,
|
|
capacity: profile.capacity,
|
|
loadFactor: profile.loadFactor,
|
|
tempUnschedulableEnabled: profile.tempUnschedulable.enabled,
|
|
tempUnschedulableRuleCount: profile.tempUnschedulable.rules.length,
|
|
apiKeyPresent: profile.apiKey !== null && profile.apiKey.length > 0,
|
|
ok: profile.ok,
|
|
error: profile.error,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function renderSub2ApiTempUnschedulableCredentials(policy: CodexTempUnschedulablePolicy): Record<string, unknown> {
|
|
if (!policy.enabled) return {};
|
|
return {
|
|
temp_unschedulable_enabled: policy.enabled,
|
|
temp_unschedulable_rules: policy.rules.map((rule) => ({
|
|
error_code: rule.statusCode,
|
|
keywords: [...rule.keywords],
|
|
duration_minutes: rule.durationMinutes,
|
|
description: rule.description ?? "",
|
|
})),
|
|
};
|
|
}
|
|
|
|
export function tempUnschedulableSummary(policy: CodexTempUnschedulablePolicy): Record<string, unknown> {
|
|
return {
|
|
enabled: policy.enabled,
|
|
ruleCount: policy.rules.length,
|
|
statusCodes: policy.rules.map((rule) => rule.statusCode),
|
|
};
|
|
}
|
|
|
|
export function codexPoolConfigSummary(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record<string, unknown> {
|
|
const accountCapacityTotal = desiredAccountCapacityTotal(pool);
|
|
return {
|
|
version: pool.version,
|
|
kind: pool.kind,
|
|
metadata: pool.metadata,
|
|
groupName: pool.groupName,
|
|
groupDescription: pool.groupDescription,
|
|
apiKeyName: pool.apiKeyName,
|
|
apiKeySecretName: pool.apiKeySecretName,
|
|
apiKeySecretKey: pool.apiKeySecretKey,
|
|
adminEmailDefault: pool.adminEmailDefault,
|
|
minOwnerBalanceUsd: pool.minOwnerBalanceUsd,
|
|
minOwnerConcurrency: pool.minOwnerConcurrency,
|
|
minOwnerConcurrencySource: pool.minOwnerConcurrencySource,
|
|
accountCapacityTotal,
|
|
defaultAccountPriority: pool.defaultAccountPriority,
|
|
defaultAccountCapacity: pool.defaultAccountCapacity,
|
|
defaultAccountLoadFactor: pool.defaultAccountLoadFactor,
|
|
defaultTempUnschedulable: tempUnschedulableSummary(pool.defaultTempUnschedulable),
|
|
defaultSentinelProtect: pool.defaultSentinelProtect,
|
|
profileCount: pool.profiles.length,
|
|
manualAccounts: {
|
|
bindingSources: pool.manualAccounts.bindingSources.items.map(manualBindingSourcePlan),
|
|
protectedCount: pool.manualAccounts.protected.length,
|
|
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",
|
|
},
|
|
publicExposure: targetPublicExposureSummary(target),
|
|
localCodex: pool.localCodex,
|
|
sentinel: codexPoolSentinelSummary(pool.sentinel),
|
|
disclosure: {
|
|
full: "bun scripts/cli.ts platform-infra sub2api codex-pool plan --full",
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function compactText(value: unknown, limit = 260): string {
|
|
if (typeof value !== "string") return "";
|
|
return value.length > limit ? `${value.slice(0, limit)}…` : value;
|
|
}
|
|
|
|
export function recordArray(value: unknown): Record<string, unknown>[] {
|
|
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
}
|
|
|
|
export function pickSummaryFields(item: Record<string, unknown>, keys: string[]): Record<string, unknown> {
|
|
const result: Record<string, unknown> = {};
|
|
for (const key of keys) {
|
|
if (item[key] !== undefined) result[key] = item[key];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function compactStatusBlock(block: unknown, keys: string[]): Record<string, unknown> | null {
|
|
if (!isRecord(block)) return null;
|
|
const items = recordArray(block.items);
|
|
const attentionItems = items
|
|
.filter((item) => item.ok === false)
|
|
.map((item) => pickSummaryFields(item, keys));
|
|
return {
|
|
ok: block.ok,
|
|
desired: block.desired,
|
|
missing: block.missing,
|
|
mismatched: block.mismatched,
|
|
totals: block.totals,
|
|
itemCount: items.length,
|
|
attentionItems,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function compactTempUnschedulableStatus(block: unknown): Record<string, unknown> | null {
|
|
if (!isRecord(block)) return null;
|
|
const items = recordArray(block.items);
|
|
const compactItems = items.map((item) => {
|
|
const result = pickSummaryFields(item, [
|
|
"accountName",
|
|
"accountId",
|
|
"status",
|
|
"schedulable",
|
|
"tempUnschedulableUntil",
|
|
"tempUnschedulableSet",
|
|
"ok",
|
|
]);
|
|
if (item.tempUnschedulableReasonPreview !== undefined) {
|
|
result.tempUnschedulableReason = tempUnschedulableReasonSummary(item.tempUnschedulableReasonPreview);
|
|
}
|
|
return result;
|
|
});
|
|
const frozenItems = compactItems.filter((item) => item.tempUnschedulableSet === true);
|
|
const focusedFrozenItems = uniqueByAccountName([
|
|
...frozenItems.filter((item) => isRecord(item.tempUnschedulableReason) && item.tempUnschedulableReason.statusCode === 400),
|
|
...frozenItems.filter((item) => item.schedulable === false),
|
|
...frozenItems,
|
|
]).slice(0, 6);
|
|
return {
|
|
ok: block.ok,
|
|
desired: block.desired,
|
|
enabledCount: Array.isArray(block.enabled) ? block.enabled.length : undefined,
|
|
missing: block.missing,
|
|
mismatched: block.mismatched,
|
|
itemCount: compactItems.length,
|
|
frozenCount: frozenItems.length,
|
|
frozenShown: focusedFrozenItems.length,
|
|
frozen: focusedFrozenItems.map((item) => pickSummaryFields(item, [
|
|
"accountName",
|
|
"accountId",
|
|
"schedulable",
|
|
"tempUnschedulableUntil",
|
|
"tempUnschedulableReason",
|
|
])),
|
|
manuallyUnschedulable: compactItems
|
|
.filter((item) => item.schedulable === false && item.tempUnschedulableSet !== true)
|
|
.map((item) => pickSummaryFields(item, ["accountName", "accountId", "status", "schedulable"])),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function compactManualAccounts(block: unknown): Record<string, unknown> | null {
|
|
if (!isRecord(block)) return null;
|
|
const items = recordArray(block.items).map((item) => pickSummaryFields(item, [
|
|
"accountName",
|
|
"reason",
|
|
"ok",
|
|
"exists",
|
|
"accountId",
|
|
"status",
|
|
"schedulable",
|
|
"inYamlProfiles",
|
|
"runtimeMarkedUnideskManaged",
|
|
"proxyBinding",
|
|
"groupBinding",
|
|
"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;
|
|
const groupSync = isRecord(block.groupSync)
|
|
? {
|
|
ok: block.groupSync.ok,
|
|
itemCount: block.groupSync.itemCount,
|
|
items: recordArray(block.groupSync.items).map((item) => pickSummaryFields(item, [
|
|
"accountName",
|
|
"accountId",
|
|
"enabled",
|
|
"ok",
|
|
"action",
|
|
"source",
|
|
"poolGroupName",
|
|
"poolGroupId",
|
|
"bindingAligned",
|
|
"controlPolicy",
|
|
])),
|
|
valuesPrinted: false,
|
|
}
|
|
: undefined;
|
|
return {
|
|
ok: block.ok,
|
|
protectedCount: block.protectedCount,
|
|
items,
|
|
proxySync,
|
|
groupSync,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function tempUnschedulableReasonSummary(value: unknown): Record<string, unknown> {
|
|
const reason = compactText(value, 180);
|
|
const statusCodeMatch = reason.match(/"status_code":(\d{3})/u) ?? reason.match(/OpenAI\s+(\d{3})/u) ?? reason.match(/\((\d{3})\)/u);
|
|
const keywordMatch = reason.match(/"matched_keyword":"([^"]+)"/u);
|
|
const summary: Record<string, unknown> = {
|
|
statusCode: statusCodeMatch ? Number(statusCodeMatch[1]) : undefined,
|
|
matchedKeyword: keywordMatch ? keywordMatch[1] : undefined,
|
|
};
|
|
if (!statusCodeMatch || !keywordMatch) summary.preview = compactText(reason, 120);
|
|
return summary;
|
|
}
|
|
|
|
export function uniqueByAccountName(items: Record<string, unknown>[]): Record<string, unknown>[] {
|
|
const seen = new Set<unknown>();
|
|
const result: Record<string, unknown>[] = [];
|
|
for (const item of items) {
|
|
const key = item.accountName ?? item.accountId;
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
result.push(item);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function compactRuntimeCapability(block: unknown): unknown {
|
|
if (!isRecord(block)) return block;
|
|
const probe = isRecord(block.probe) ? block.probe : {};
|
|
const logEvidence = isRecord(probe.logEvidence) ? probe.logEvidence : {};
|
|
const accountState = isRecord(probe.accountState) ? probe.accountState : (isRecord(probe.badAccountState) ? probe.badAccountState : {});
|
|
const resources = isRecord(block.resources) ? block.resources : {};
|
|
const requirement = isRecord(block.requirement) ? block.requirement : {};
|
|
return {
|
|
ok: block.ok,
|
|
required: block.required,
|
|
capability: block.capability,
|
|
outcome: block.outcome,
|
|
requirement: Object.keys(requirement).length === 0 ? undefined : {
|
|
statusCode: requirement.statusCode,
|
|
representativeKeyword: requirement.representativeKeyword,
|
|
durationMinutes: requirement.durationMinutes,
|
|
sourceAccountName: requirement.sourceAccountName,
|
|
},
|
|
requestEvidence: Object.keys(probe).length === 0 ? undefined : {
|
|
requestId: probe.requestId,
|
|
durationMs: probe.durationMs,
|
|
httpStatus: probe.httpStatus,
|
|
responseOk: probe.responseOk,
|
|
accountState: Object.keys(accountState).length === 0 ? undefined : {
|
|
accountId: accountState.accountId,
|
|
status: accountState.status,
|
|
schedulable: accountState.schedulable,
|
|
tempUnschedulableUntil: accountState.tempUnschedulableUntil,
|
|
tempUnschedulableSet: accountState.tempUnschedulableSet,
|
|
tempUnschedulableReasonPreview: compactText(accountState.tempUnschedulableReasonPreview, 180),
|
|
},
|
|
logEvidence: Object.keys(logEvidence).length === 0 ? undefined : {
|
|
matchedLogLineCount: logEvidence.matchedLogLineCount,
|
|
failovers: logEvidence.failovers,
|
|
final: logEvidence.final,
|
|
},
|
|
bodyPreview: probe.responseOk === false ? compactText(probe.bodyPreview, 240) : undefined,
|
|
},
|
|
resources: Object.keys(resources).length === 0 ? undefined : {
|
|
groupId: resources.groupId,
|
|
accountId: resources.accountId,
|
|
badAccountId: resources.badAccountId,
|
|
goodAccountId: resources.goodAccountId,
|
|
apiKeyId: resources.apiKeyId,
|
|
valuesPrinted: false,
|
|
},
|
|
message: block.message,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function compactGatewayModels(block: unknown): unknown {
|
|
if (!isRecord(block)) return block;
|
|
return pickSummaryFields(block, [
|
|
"ok",
|
|
"httpStatus",
|
|
"modelCount",
|
|
"method",
|
|
"serviceDns",
|
|
"valuesPrinted",
|
|
]);
|
|
}
|
|
|
|
export function compactGatewayResponses(block: unknown): unknown {
|
|
if (!isRecord(block)) return block;
|
|
const evidence = isRecord(block.evidence) ? block.evidence : {};
|
|
return {
|
|
ok: block.ok,
|
|
degraded: block.degraded,
|
|
outcome: block.outcome,
|
|
httpStatus: block.httpStatus,
|
|
method: block.method,
|
|
model: block.model,
|
|
requestId: block.requestId,
|
|
durationMs: block.durationMs,
|
|
outputTextPreview: block.outputTextPreview,
|
|
evidence: Object.keys(evidence).length === 0 ? undefined : {
|
|
matchedLogLineCount: evidence.matchedLogLineCount,
|
|
failoverCount: Array.isArray(evidence.failovers) ? evidence.failovers.length : undefined,
|
|
failovers: Array.isArray(evidence.failovers) ? evidence.failovers.slice(-5) : undefined,
|
|
final: evidence.final,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function compactGatewayResponsesRecent(block: unknown): unknown {
|
|
if (!isRecord(block)) return block;
|
|
return {
|
|
ok: block.ok,
|
|
degraded: block.degraded,
|
|
window: block.window,
|
|
tailLines: block.tailLines,
|
|
failoverCount: block.failoverCount,
|
|
forwardFailureCount: block.forwardFailureCount,
|
|
finalErrorCount: block.finalErrorCount,
|
|
slowFinalErrorCount: block.slowFinalErrorCount,
|
|
contextCanceledCount: block.contextCanceledCount,
|
|
ignoredProbeNoiseCount: block.ignoredProbeNoiseCount,
|
|
failoverBudgetExhaustedCount: Array.isArray(block.failoverBudgetExhausted) ? block.failoverBudgetExhausted.length : undefined,
|
|
failoverBudgetExhausted: Array.isArray(block.failoverBudgetExhausted) ? block.failoverBudgetExhausted.slice(-3).reverse() : undefined,
|
|
recentFailovers: Array.isArray(block.recentFailovers) ? block.recentFailovers.slice(-4).reverse() : undefined,
|
|
recentForwardFailures: Array.isArray(block.recentForwardFailures) ? block.recentForwardFailures.slice(-4).reverse().map((item) => ({
|
|
...item,
|
|
errorPreview: compactText(item.errorPreview, 220),
|
|
})) : undefined,
|
|
recentFinalErrors: Array.isArray(block.recentFinalErrors) ? block.recentFinalErrors.slice(-3).reverse() : undefined,
|
|
recentSlowFinalErrors: Array.isArray(block.recentSlowFinalErrors) ? block.recentSlowFinalErrors.slice(-3).reverse() : undefined,
|
|
recentContextCanceled: Array.isArray(block.recentContextCanceled) ? block.recentContextCanceled.slice(-3).reverse() : undefined,
|
|
logsExitCode: block.logsExitCode,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function compactGatewayCompactRecent(block: unknown): unknown {
|
|
if (!isRecord(block)) return block;
|
|
return {
|
|
ok: block.ok,
|
|
degraded: block.degraded,
|
|
window: block.window,
|
|
tailLines: block.tailLines,
|
|
failureCount: block.failureCount,
|
|
successCount: block.successCount,
|
|
failoverCount: block.failoverCount,
|
|
finalErrorCount: block.finalErrorCount,
|
|
contextCanceledCount: block.contextCanceledCount,
|
|
recentFailures: Array.isArray(block.recentFailures) ? block.recentFailures.slice(-2).reverse() : undefined,
|
|
recentSuccesses: Array.isArray(block.recentSuccesses) ? block.recentSuccesses.slice(-2).reverse() : undefined,
|
|
recentFailovers: Array.isArray(block.recentFailovers) ? block.recentFailovers.slice(-4).reverse() : undefined,
|
|
recentFinalErrors: Array.isArray(block.recentFinalErrors) ? block.recentFinalErrors.slice(-2).reverse() : undefined,
|
|
recentContextCanceled: Array.isArray(block.recentContextCanceled) ? block.recentContextCanceled.slice(-2).reverse() : undefined,
|
|
logsExitCode: block.logsExitCode,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function compactSentinelErrorDetails(value: unknown): Record<string, unknown> | undefined {
|
|
if (!isRecord(value)) return undefined;
|
|
const body = isRecord(value.body) ? value.body : {};
|
|
const openaiError = isRecord(value.openaiError) ? value.openaiError : {};
|
|
return {
|
|
kind: value.kind,
|
|
statusCode: value.statusCode,
|
|
code: value.code ?? body.code ?? openaiError.code,
|
|
type: value.type ?? body.type ?? openaiError.type,
|
|
bodyHash: value.bodyHash,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function compactSentinelQuarantine(item: unknown): Record<string, unknown> {
|
|
if (!isRecord(item)) return {};
|
|
const protect = isRecord(item.sentinelProtect) ? item.sentinelProtect : {};
|
|
return {
|
|
accountName: item.accountName,
|
|
until: item.until,
|
|
applied: item.applied,
|
|
reason: item.reason,
|
|
failureKind: item.failureKind,
|
|
intervalMinutes: item.intervalMinutes,
|
|
sentinelProtect: Object.keys(protect).length > 0 ? {
|
|
enabled: protect.enabled,
|
|
decision: protect.decision,
|
|
failureCount: protect.failureCount,
|
|
threshold: protect.threshold,
|
|
} : undefined,
|
|
error: compactSentinelErrorDetails(item.errorDetails),
|
|
};
|
|
}
|
|
|
|
export function compactSentinelRecentAccount(item: unknown): Record<string, unknown> {
|
|
if (!isRecord(item)) return {};
|
|
const action = isRecord(item.action) ? item.action : {};
|
|
const error = compactSentinelErrorDetails(item.errorDetails);
|
|
const protect = isRecord(item.sentinelProtect) ? item.sentinelProtect : {};
|
|
return {
|
|
accountName: item.accountName,
|
|
lastProbeAt: item.lastProbeAt,
|
|
lastStatus: item.lastStatus,
|
|
nextProbeAfter: item.nextProbeAfter,
|
|
ok: item.ok,
|
|
purpose: item.purpose,
|
|
httpStatus: item.httpStatus,
|
|
durationMs: item.durationMs,
|
|
markerMatched: item.markerMatched,
|
|
sentinelProtect: Object.keys(protect).length > 0 ? {
|
|
enabled: protect.enabled,
|
|
decision: protect.decision,
|
|
protected: protect.protected,
|
|
failureCount: protect.failureCount,
|
|
threshold: protect.threshold,
|
|
} : undefined,
|
|
failureKind: item.failureKind,
|
|
requestShape: item.requestShape,
|
|
action: Object.keys(action).length > 0 ? {
|
|
taken: action.taken,
|
|
type: action.type,
|
|
} : undefined,
|
|
errorStatusCode: error?.statusCode,
|
|
errorCode: error?.code,
|
|
errorBodyHash: error?.bodyHash,
|
|
};
|
|
}
|
|
|
|
export function compactSentinelLastRun(value: unknown): Record<string, unknown> | undefined {
|
|
if (!isRecord(value)) return undefined;
|
|
return {
|
|
at: value.at,
|
|
monitorEnabled: value.monitorEnabled,
|
|
actionsEnabled: value.actionsEnabled,
|
|
profileCount: value.profileCount,
|
|
selected: value.selected,
|
|
okCount: value.okCount,
|
|
mismatchCount: value.mismatchCount,
|
|
markerMismatchCount: value.markerMismatchCount,
|
|
transportFailureCount: value.transportFailureCount,
|
|
actionsTaken: value.actionsTaken,
|
|
gatewayFailureMonitor: value.gatewayFailureMonitor,
|
|
selection: value.selection,
|
|
reconcileCount: Array.isArray(value.reconcile) ? value.reconcile.length : undefined,
|
|
};
|
|
}
|
|
|
|
export function compactSentinelStatus(block: unknown): unknown {
|
|
if (!isRecord(block)) return block;
|
|
const runtime = isRecord(block.runtime) ? block.runtime : block;
|
|
const desired = isRecord(runtime.desired) ? runtime.desired : {};
|
|
const cronJob = isRecord(runtime.cronJob) ? runtime.cronJob : {};
|
|
const secret = isRecord(runtime.secret) ? runtime.secret : {};
|
|
const configMap = isRecord(runtime.configMap) ? runtime.configMap : {};
|
|
const state = isRecord(runtime.state) ? runtime.state : {};
|
|
const freezeReassert = isRecord(block.freezeReassert) ? block.freezeReassert : {};
|
|
const qualityGatePrepare = isRecord(block.qualityGatePrepare) ? block.qualityGatePrepare : {};
|
|
const qualityGate = isRecord(block.qualityGate) ? block.qualityGate : {};
|
|
const quarantined = recordArray(state.quarantined).map(compactSentinelQuarantine);
|
|
const recentAccounts = recordArray(state.recentAccounts).map(compactSentinelRecentAccount);
|
|
const recentAttention = recentAccounts.filter((item) => item.ok === false || isRecord(item.action) && item.action.taken === true);
|
|
const recentHealthy = recentAccounts
|
|
.filter((item) => item.ok === true)
|
|
.slice(-3)
|
|
.map((item) => pickSummaryFields(item, ["accountName", "lastProbeAt", "lastStatus", "nextProbeAfter", "ok", "httpStatus", "markerMatched"]));
|
|
return {
|
|
ok: block.ok,
|
|
action: block.action,
|
|
desired: {
|
|
monitorEnabled: desired.monitorEnabled,
|
|
actionsEnabled: desired.actionsEnabled,
|
|
schedule: desired.schedule,
|
|
cronJobName: desired.cronJobName,
|
|
configMapName: desired.configMapName,
|
|
credentialsSecretName: desired.credentialsSecretName,
|
|
stateConfigMapName: desired.stateConfigMapName,
|
|
},
|
|
cronJob: {
|
|
exists: cronJob.exists,
|
|
schedule: cronJob.schedule,
|
|
suspend: cronJob.suspend,
|
|
lastScheduleTime: cronJob.lastScheduleTime,
|
|
active: cronJob.active,
|
|
error: cronJob.error,
|
|
},
|
|
secret: {
|
|
exists: secret.exists,
|
|
profileSecretPresent: secret.profileSecretPresent,
|
|
valuesPrinted: false,
|
|
error: secret.error,
|
|
},
|
|
configMap: {
|
|
exists: configMap.exists,
|
|
configPresent: configMap.configPresent,
|
|
runnerPresent: configMap.runnerPresent,
|
|
error: configMap.error,
|
|
},
|
|
state: {
|
|
exists: state.exists,
|
|
accountCount: state.accountCount,
|
|
quarantinedCount: state.quarantinedCount,
|
|
quarantinedShown: Math.min(quarantined.length, 3),
|
|
quarantined: quarantined.slice(-3),
|
|
recentAccountCount: recentAccounts.length,
|
|
recentAttention: uniqueByAccountName(recentAttention.slice(-3)),
|
|
recentHealthy,
|
|
lastRun: compactSentinelLastRun(state.lastRun),
|
|
error: state.error,
|
|
},
|
|
freezeReassert: Object.keys(freezeReassert).length > 0 ? {
|
|
ok: freezeReassert.ok,
|
|
skipped: freezeReassert.skipped,
|
|
reason: freezeReassert.reason,
|
|
itemCount: freezeReassert.itemCount,
|
|
attentionItems: freezeReassert.attentionItems,
|
|
} : undefined,
|
|
pendingProbePrepare: Object.keys(qualityGatePrepare).length > 0 ? {
|
|
ok: qualityGatePrepare.ok,
|
|
skipped: qualityGatePrepare.skipped,
|
|
reason: qualityGatePrepare.reason,
|
|
changedCount: qualityGatePrepare.changedCount,
|
|
fingerprintOnlyCount: qualityGatePrepare.fingerprintOnlyCount,
|
|
pendingOnly: qualityGatePrepare.pendingOnly,
|
|
items: qualityGatePrepare.items,
|
|
} : undefined,
|
|
pendingProbe: Object.keys(qualityGate).length > 0 ? {
|
|
ok: qualityGate.ok,
|
|
skipped: qualityGate.skipped,
|
|
reason: qualityGate.reason,
|
|
changedCount: qualityGate.changedCount,
|
|
fingerprintOnlyCount: qualityGate.fingerprintOnlyCount,
|
|
clampedCount: qualityGate.clampedCount,
|
|
items: qualityGate.items,
|
|
clampedItems: qualityGate.clampedItems,
|
|
} : undefined,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function compactSentinelProbeResult(parsed: Record<string, unknown> | null): Record<string, unknown> | null {
|
|
if (parsed === null) return null;
|
|
const probe = isRecord(parsed.probe) ? parsed.probe : {};
|
|
const summary = isRecord(probe.summary) ? probe.summary : {};
|
|
const state = isRecord(parsed.sentinelState) ? parsed.sentinelState : {};
|
|
return {
|
|
ok: parsed.ok,
|
|
mode: parsed.mode,
|
|
namespace: parsed.namespace,
|
|
job: parsed.job,
|
|
requestedAccounts: parsed.requestedAccounts,
|
|
summary: {
|
|
at: summary.at,
|
|
monitorEnabled: summary.monitorEnabled,
|
|
actionsEnabled: summary.actionsEnabled,
|
|
selected: summary.selected,
|
|
okCount: summary.okCount,
|
|
mismatchCount: summary.mismatchCount,
|
|
markerMismatchCount: summary.markerMismatchCount,
|
|
transportFailureCount: summary.transportFailureCount,
|
|
actionsTaken: summary.actionsTaken,
|
|
gatewayFailureMonitor: summary.gatewayFailureMonitor,
|
|
selection: summary.selection,
|
|
},
|
|
results: recordArray(probe.results).map((item) => pickSummaryFields(item, [
|
|
"accountName",
|
|
"purpose",
|
|
"ok",
|
|
"markerMatched",
|
|
"sentinelProtect",
|
|
"httpStatus",
|
|
"durationMs",
|
|
"usage",
|
|
"outputHash",
|
|
"outputPreview",
|
|
"responseBodyHash",
|
|
"responseBodyPreview",
|
|
"error",
|
|
"errorDetails",
|
|
"failureKind",
|
|
"sdk",
|
|
"requestShape",
|
|
])),
|
|
actions: recordArray(probe.actions).map((item) => pickSummaryFields(item, [
|
|
"accountName",
|
|
"taken",
|
|
"type",
|
|
"error",
|
|
])),
|
|
sentinelState: {
|
|
quarantined: state.quarantined,
|
|
recentAccounts: state.recentAccounts,
|
|
lastRun: state.lastRun,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|