940 lines
47 KiB
TypeScript
940 lines
47 KiB
TypeScript
// SPEC: issue-1616 YAML-first AgentRun provider profile online configuration.
|
|
// Provides a thin CLI over YAML-declared provider credential Secrets, runtime GitOps env and bounded validation.
|
|
import type { UniDeskConfig } from "../config";
|
|
import { startJob } from "../jobs";
|
|
import { sha256Fingerprint } from "../platform-infra-ops-library";
|
|
import {
|
|
agentRunImageArtifact,
|
|
renderedFilesDigest,
|
|
renderAgentRunGitopsFiles,
|
|
} from "../agentrun-manifests";
|
|
import {
|
|
agentRunLaneSummary,
|
|
agentRunProviderCredentialRefs,
|
|
resolveAgentRunLaneTarget,
|
|
type AgentRunLaneSecretSpec,
|
|
type AgentRunLaneSpec,
|
|
} from "../agentrun-lanes";
|
|
import { refreshYamlLaneScript } from "./git-mirror";
|
|
import type { ConfirmOptions, DisclosureOptions } from "./options";
|
|
import {
|
|
AGENTRUN_PROVIDER_ACTIVATION_FENCE_ISSUE,
|
|
providerSecretActivationPreflight,
|
|
providerSecretDesiredEncodedFingerprint,
|
|
providerSecretStagedActivation,
|
|
type ProviderSecretActivationTarget,
|
|
} from "./provider-activation";
|
|
import { compactAgentRunLaneStatusTarget } from "./trigger";
|
|
import {
|
|
collectLaneSecretSources,
|
|
createYamlLaneJobScript,
|
|
inspectSecretSourceValue,
|
|
restartYamlLaneScript,
|
|
type LaneSecretSource,
|
|
type LaneSecretSourceInspection,
|
|
yamlLaneGitopsPublishJobManifest,
|
|
yamlLaneGitopsPublishPayloadFromProbe,
|
|
yamlLaneJobProbeScript,
|
|
} from "./secrets";
|
|
import { capture, captureJsonPayload, compactCapture, progressEvent, record, shQuote, sleep, stringOrNull } from "./utils";
|
|
|
|
export interface ProviderProfileOptions extends ConfirmOptions, DisclosureOptions {
|
|
readonly node: string | null;
|
|
readonly lane: string | null;
|
|
readonly profile: string | null;
|
|
readonly wait: boolean;
|
|
readonly prompt: string | null;
|
|
}
|
|
|
|
export function parseProviderProfileOptions(args: string[]): ProviderProfileOptions {
|
|
let node: string | null = null;
|
|
let lane: string | null = null;
|
|
let profile: string | null = null;
|
|
let full = false;
|
|
let raw = false;
|
|
let wait = false;
|
|
let prompt: string | null = null;
|
|
let confirm = false;
|
|
let dryRun = false;
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const arg = args[index];
|
|
if (arg === "--confirm") {
|
|
confirm = true;
|
|
continue;
|
|
}
|
|
if (arg === "--dry-run") {
|
|
dryRun = true;
|
|
continue;
|
|
}
|
|
if (arg === "--full") {
|
|
full = true;
|
|
continue;
|
|
}
|
|
if (arg === "--raw") {
|
|
raw = true;
|
|
continue;
|
|
}
|
|
if (arg === "--wait") {
|
|
wait = true;
|
|
continue;
|
|
}
|
|
if (arg === "--node" || arg === "--lane" || arg === "--profile" || arg === "--prompt") {
|
|
const value = args[index + 1];
|
|
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
|
|
if (arg === "--node") node = value;
|
|
if (arg === "--lane") lane = value;
|
|
if (arg === "--profile") profile = value;
|
|
if (arg === "--prompt") prompt = value;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
throw new Error(`unsupported provider-profile option: ${arg}`);
|
|
}
|
|
if (confirm && dryRun) throw new Error("accepts only one of --confirm or --dry-run");
|
|
return { node, lane, profile, full, raw, wait, prompt, confirm, dryRun: dryRun || !confirm };
|
|
}
|
|
|
|
export function providerProfileHelp(): string {
|
|
return [
|
|
"Usage: bun scripts/cli.ts agentrun provider-profile <plan|status|runtime-status|apply|validate> --node <NODE> --lane <lane> --profile <profile> [options]",
|
|
"",
|
|
"Examples:",
|
|
" bun scripts/cli.ts agentrun provider-profile plan --node NC01 --lane nc01-v02 --profile gpt.pika",
|
|
" bun scripts/cli.ts agentrun provider-profile status --node NC01 --lane nc01-v02 --profile gpt.pika",
|
|
" bun scripts/cli.ts agentrun provider-profile runtime-status --node NC01 --lane nc01-v02 --profile gpt.pika",
|
|
" bun scripts/cli.ts agentrun provider-profile apply --node NC01 --lane nc01-v02 --profile gpt.pika --confirm",
|
|
" bun scripts/cli.ts job status <jobId> --tail-bytes 12000",
|
|
" bun scripts/cli.ts agentrun provider-profile validate --node NC01 --lane nc01-v02 --profile gpt.pika --wait",
|
|
"",
|
|
"Boundary:",
|
|
" confirmed apply returns an async job only after the provider Secret gate reports an exact data no-op; the worker skips the provider Secret write, publishes runtime GitOps using the current manager image, refreshes Argo and restarts the manager without rebuilding images or creating a PipelineRun.",
|
|
].join("\n");
|
|
}
|
|
|
|
export async function runProviderProfileCommand(config: UniDeskConfig, action: string | undefined, args: string[]): Promise<Record<string, unknown>> {
|
|
const options = parseProviderProfileOptions(args);
|
|
if (action === "plan") return providerProfilePlan(options);
|
|
if (action === "status") return providerProfileStatus(config, options);
|
|
if (action === "runtime-status") return providerProfileRuntimeStatus(config, options);
|
|
if (action === "apply") {
|
|
if (options.confirm && !options.wait) return providerProfileApplyAsync(config, options);
|
|
return providerProfileApply(config, options);
|
|
}
|
|
if (action === "validate") return providerProfileValidate(config, options);
|
|
throw new Error(`unsupported provider-profile action: ${action ?? "(empty)"}`);
|
|
}
|
|
|
|
async function providerProfileApplyAsync(config: UniDeskConfig, options: ProviderProfileOptions): Promise<Record<string, unknown>> {
|
|
const plan = providerProfilePlan({ ...options, confirm: false, dryRun: true });
|
|
if (plan.ok !== true) return plan;
|
|
const { spec, profile } = resolveProviderProfile(options);
|
|
const secrets = spec.secrets.filter((secret) => secret.providerCredentialProfile === profile);
|
|
const activationTargets = providerProfileActivationTargets(inspectProviderProfileSources(spec, secrets));
|
|
const activationPreflight = await providerSecretActivationPreflight(config, spec, activationTargets, { full: options.full || options.raw });
|
|
if (activationPreflight.ok !== true) {
|
|
return providerProfileActivationBlockedResult(spec, profile, secrets, activationPreflight, "async-submit");
|
|
}
|
|
const args = [
|
|
"bun",
|
|
"scripts/cli.ts",
|
|
"agentrun",
|
|
"provider-profile",
|
|
"apply",
|
|
"--node",
|
|
spec.nodeId,
|
|
"--lane",
|
|
spec.lane,
|
|
"--profile",
|
|
profile,
|
|
"--confirm",
|
|
"--wait",
|
|
];
|
|
const jobName = `agentrun_${spec.lane}_provider_profile_${profile.replace(/[^A-Za-z0-9_-]/gu, "-")}_apply`;
|
|
const job = startJob(jobName, args, `Apply YAML-declared AgentRun provider profile ${profile} on ${spec.nodeId}/${spec.lane}`);
|
|
const statusCommand = `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`;
|
|
return {
|
|
ok: true,
|
|
command: "agentrun provider-profile apply",
|
|
mode: "async-job",
|
|
mutation: true,
|
|
node: spec.nodeId,
|
|
lane: spec.lane,
|
|
profile,
|
|
job,
|
|
jobCreated: true,
|
|
activationPreflight,
|
|
statusCommand,
|
|
next: {
|
|
status: statusCommand,
|
|
full: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000 --full`,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
async function providerProfileRuntimeStatus(config: UniDeskConfig, options: ProviderProfileOptions): Promise<Record<string, unknown>> {
|
|
const { configPath, spec, profile, secrets } = resolveProviderProfile(options);
|
|
const live = await capture(config, spec.nodeKubeRoute, ["sh", "--", providerProfileRuntimeStatusScript(spec, secrets)]);
|
|
const payload = captureJsonPayload(live);
|
|
const ready = live.exitCode === 0
|
|
&& payload.ok === true
|
|
&& record(payload.secrets).ready === true
|
|
&& record(payload.providerServices).ready === true;
|
|
return {
|
|
ok: ready,
|
|
command: "agentrun provider-profile runtime-status",
|
|
mode: "runtime-only-profile-preflight",
|
|
status: ready ? "ready" : "blocked",
|
|
degradedReason: ready ? null : "runtime-provider-profile-unavailable",
|
|
mutation: false,
|
|
ownerSourceInspected: false,
|
|
runtimeSecretValuesDecoded: false,
|
|
configPath,
|
|
target: options.full ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec),
|
|
profile: providerProfileSummary(spec, profile, secrets),
|
|
runtime: payload,
|
|
capture: compactCapture(live, { full: options.full || options.raw || live.exitCode !== 0, stdoutTailChars: 2000, stderrTailChars: 2000 }),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function resolveProviderProfile(options: ProviderProfileOptions): { configPath: string; spec: AgentRunLaneSpec; profile: string; secrets: AgentRunLaneSecretSpec[] } {
|
|
if (options.profile === null) throw new Error("provider-profile requires --profile");
|
|
const target = resolveAgentRunLaneTarget(options);
|
|
const secrets = target.spec.secrets.filter((secret) => secret.providerCredentialProfile === options.profile);
|
|
if (secrets.length === 0) {
|
|
const declared = agentRunProviderCredentialRefs(target.spec).map((item) => item.profile);
|
|
throw new Error(`provider profile ${options.profile} is not declared in ${target.configPath}; declared profiles: ${declared.join(", ") || "(none)"}`);
|
|
}
|
|
return { ...target, profile: options.profile, secrets };
|
|
}
|
|
|
|
function providerProfilePlan(options: ProviderProfileOptions): Record<string, unknown> {
|
|
const { configPath, spec, profile, secrets } = resolveProviderProfile(options);
|
|
const inspected = inspectProviderProfileSources(spec, secrets);
|
|
const source = providerProfileSourceSummary(inspected);
|
|
const discloseFull = options.full || options.raw;
|
|
const values = inspected.map(({ secret, inspection }) => {
|
|
return {
|
|
id: secret.id,
|
|
sourceRef: secret.sourceRef,
|
|
sourceMode: secret.sourceMode,
|
|
transform: secret.transform,
|
|
targetRef: secret.targetRef,
|
|
key: secret.targetRef.key,
|
|
codexConfig: secret.codexConfig === null ? null : codexConfigSummary(secret.codexConfig),
|
|
...inspection.summary,
|
|
valuesPrinted: false,
|
|
};
|
|
});
|
|
return {
|
|
ok: source.ready,
|
|
command: "agentrun provider-profile plan",
|
|
mode: "yaml-first-online-profile",
|
|
mutation: false,
|
|
status: source.ready ? "ready" : "blocked",
|
|
degradedReason: source.ready ? null : "lane-secret-source-unavailable",
|
|
blockedBeforeMutation: !source.ready,
|
|
configPath,
|
|
target: discloseFull ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec),
|
|
profile: providerProfileSummary(spec, profile, secrets),
|
|
source: discloseFull ? source : compactProviderProfileSource(source),
|
|
plan: {
|
|
secretCount: values.length,
|
|
secrets: discloseFull ? values : compactProviderProfilePlanSecrets(values),
|
|
runtime: {
|
|
managerDeployment: spec.runtime.managerDeployment,
|
|
runnerEgressProxyUrl: spec.deployment.runner.egressProxyUrl,
|
|
runnerNoProxyExtra: spec.deployment.runner.noProxyExtra,
|
|
providerServices: providerRuntimeServiceTargets(secrets),
|
|
valuesPrinted: false,
|
|
},
|
|
applySteps: ["activation-preflight", "provider-secret-no-op-skip", "gitops-publish-current-image", "argocd-refresh-sync", "manager-rollout-restart"],
|
|
activationPreflightRequired: true,
|
|
providerSecretMutationPolicy: {
|
|
mode: "fail-closed-until-manager-activation-fence",
|
|
exactNoOpWriteSkipped: true,
|
|
trackingIssue: AGENTRUN_PROVIDER_ACTIVATION_FENCE_ISSUE,
|
|
valuesPrinted: false,
|
|
},
|
|
rebuildImages: false,
|
|
pipelineRun: false,
|
|
valuesPrinted: false,
|
|
},
|
|
next: {
|
|
apply: `bun scripts/cli.ts agentrun provider-profile apply --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile} --confirm`,
|
|
status: `bun scripts/cli.ts agentrun provider-profile status --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile}`,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function compactProviderProfilePlanSecrets(values: readonly Record<string, unknown>[]): Record<string, unknown>[] {
|
|
return values.map((item) => ({
|
|
id: item.id ?? null,
|
|
targetRef: item.targetRef ?? null,
|
|
key: item.key ?? null,
|
|
present: item.present === true,
|
|
fingerprint: item.fingerprint ?? null,
|
|
valueBytes: item.valueBytes ?? null,
|
|
degradedReason: item.degradedReason ?? null,
|
|
valuesPrinted: false,
|
|
}));
|
|
}
|
|
|
|
async function providerProfileStatus(config: UniDeskConfig, options: ProviderProfileOptions): Promise<Record<string, unknown>> {
|
|
const { configPath, spec, profile, secrets } = resolveProviderProfile(options);
|
|
const inspected = inspectProviderProfileSources(spec, secrets);
|
|
const source = providerProfileSourceSummary(inspected);
|
|
if (!source.ready) return providerProfileSourceUnavailable("status", configPath, spec, profile, secrets, source);
|
|
const live = await capture(config, spec.nodeKubeRoute, ["sh", "--", providerProfileLiveStatusScript(spec, secrets)]);
|
|
const payload = captureJsonPayload(live);
|
|
const comparison = providerProfileLiveComparison(inspected, payload);
|
|
const discloseFull = options.full || options.raw;
|
|
const yamlDesired = {
|
|
secrets: inspected.map(({ secret, inspection }) => ({
|
|
id: secret.id,
|
|
targetRef: secret.targetRef,
|
|
key: secret.targetRef.key,
|
|
fingerprint: inspection.summary.fingerprint,
|
|
valueBytes: inspection.summary.valueBytes,
|
|
codexConfig: secret.codexConfig === null ? null : codexConfigSummary(secret.codexConfig),
|
|
valuesPrinted: false,
|
|
})),
|
|
runnerNoProxyExtra: spec.deployment.runner.noProxyExtra,
|
|
runnerEgressProxyUrl: spec.deployment.runner.egressProxyUrl,
|
|
valuesPrinted: false,
|
|
};
|
|
return {
|
|
ok: live.exitCode === 0 && payload.ok !== false && payload.runtimeSecretValuesDecoded === false && comparison.ready,
|
|
command: "agentrun provider-profile status",
|
|
mode: "yaml-first-online-profile",
|
|
mutation: false,
|
|
configPath,
|
|
target: options.full ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec),
|
|
profile: providerProfileSummary(spec, profile, secrets),
|
|
source: discloseFull ? source : compactProviderProfileSource(source),
|
|
yamlDesired: discloseFull ? yamlDesired : compactProviderProfileDesired(yamlDesired),
|
|
live: discloseFull
|
|
? { ...payload, desiredComparison: comparison, valuesPrinted: false }
|
|
: compactProviderProfileLive(payload, comparison),
|
|
capture: compactCapture(live, { full: options.full || options.raw || live.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
|
|
next: {
|
|
apply: `bun scripts/cli.ts agentrun provider-profile apply --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile} --confirm`,
|
|
validate: `bun scripts/cli.ts agentrun provider-profile validate --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile} --wait`,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function compactProviderProfileSource(source: ReturnType<typeof providerProfileSourceSummary>): Record<string, unknown> {
|
|
return {
|
|
ready: source.ready,
|
|
count: source.count,
|
|
presentCount: source.presentCount,
|
|
missingCount: source.missingCount,
|
|
missing: source.items
|
|
.filter((item) => item.present !== true)
|
|
.map((item) => ({ id: item.id ?? null, targetRef: item.targetRef ?? null, degradedReason: item.degradedReason ?? null, valuesPrinted: false })),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function compactProviderProfileDesired(desired: Record<string, unknown>): Record<string, unknown> {
|
|
const secrets = Array.isArray(desired.secrets) ? desired.secrets.map(record) : [];
|
|
return {
|
|
secretCount: secrets.length,
|
|
secrets: secrets.map((item) => ({
|
|
id: item.id ?? null,
|
|
targetRef: item.targetRef ?? null,
|
|
fingerprint: item.fingerprint ?? null,
|
|
valueBytes: item.valueBytes ?? null,
|
|
valuesPrinted: false,
|
|
})),
|
|
runnerEgressProxyUrl: desired.runnerEgressProxyUrl ?? null,
|
|
runnerNoProxyExtraCount: Array.isArray(desired.runnerNoProxyExtra) ? desired.runnerNoProxyExtra.length : 0,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function compactProviderProfileLive(payload: Record<string, unknown>, comparison: ReturnType<typeof providerProfileLiveComparison>): Record<string, unknown> {
|
|
const manager = record(payload.manager);
|
|
const secrets = record(payload.secrets);
|
|
return {
|
|
ok: payload.ok === true,
|
|
manager: {
|
|
deploymentExists: manager.deploymentExists === true,
|
|
readyReplicas: manager.readyReplicas ?? null,
|
|
replicas: manager.replicas ?? null,
|
|
sourceCommit: manager.sourceCommit ?? null,
|
|
envIdentity: manager.envIdentity ?? null,
|
|
runnerEgressProxyUrl: manager.runnerEgressProxyUrl ?? null,
|
|
noProxyContainsHyue: manager.noProxyContainsHyue === true,
|
|
noProxyContainsPikapython: manager.noProxyContainsPikapython === true,
|
|
valuesPrinted: false,
|
|
},
|
|
secrets: {
|
|
count: secrets.count ?? null,
|
|
ready: secrets.ready === true,
|
|
items: Array.isArray(secrets.items) ? secrets.items.map(record).map((item) => ({
|
|
namespace: item.namespace ?? null,
|
|
name: item.name ?? null,
|
|
key: item.key ?? null,
|
|
present: item.present === true,
|
|
keyPresent: item.keyPresent === true,
|
|
encodedBytes: item.encodedBytes ?? null,
|
|
encodedFingerprint: item.encodedFingerprint ?? null,
|
|
valuesPrinted: false,
|
|
})) : [],
|
|
valuesPrinted: false,
|
|
},
|
|
providerServices: {
|
|
count: record(payload.providerServices).count ?? null,
|
|
ready: record(payload.providerServices).ready === true,
|
|
items: Array.isArray(record(payload.providerServices).items)
|
|
? (record(payload.providerServices).items as unknown[]).map(record).map((item) => ({
|
|
providerId: item.providerId ?? null,
|
|
baseUrl: item.baseUrl ?? null,
|
|
namespace: item.namespace ?? null,
|
|
name: item.name ?? null,
|
|
port: item.port ?? null,
|
|
present: item.present === true,
|
|
portPresent: item.portPresent === true,
|
|
valuesPrinted: false,
|
|
}))
|
|
: [],
|
|
valuesPrinted: false,
|
|
},
|
|
desiredComparison: comparison,
|
|
runtimeSecretValuesDecoded: payload.runtimeSecretValuesDecoded === true,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
async function providerProfileApply(config: UniDeskConfig, options: ProviderProfileOptions): Promise<Record<string, unknown>> {
|
|
const { configPath, spec, profile, secrets } = resolveProviderProfile(options);
|
|
if (options.dryRun || !options.confirm) return providerProfilePlan({ ...options, dryRun: true, confirm: false });
|
|
const inspected = inspectProviderProfileSources(spec, secrets);
|
|
const source = providerProfileSourceSummary(inspected);
|
|
if (!source.ready) return providerProfileSourceUnavailable("apply", configPath, spec, profile, secrets, source);
|
|
providerProfileApplyProgress(spec, profile, "activation-preflight", "running");
|
|
const activationPreflight = await providerSecretActivationPreflight(config, spec, providerProfileActivationTargets(inspected), { full: options.full || options.raw });
|
|
providerProfileApplyProgress(spec, profile, "activation-preflight", activationPreflight.ok ? "succeeded" : "failed");
|
|
if (activationPreflight.ok !== true) {
|
|
return providerProfileActivationBlockedResult(spec, profile, secrets, activationPreflight, "worker-before-secret-sync");
|
|
}
|
|
const secretPayload = {
|
|
ok: true,
|
|
status: "skipped-exact-provider-secret-no-op",
|
|
secretSyncStarted: false,
|
|
providerSecretWriteSkipped: true,
|
|
skippedProviderSecretCount: inspected.length,
|
|
reason: "provider-secret-writes-require-agentrun-manager-activation-fence",
|
|
activationFenceIssue: AGENTRUN_PROVIDER_ACTIVATION_FENCE_ISSUE,
|
|
runtimeSecretValuesDecoded: false,
|
|
valuesPrinted: false,
|
|
};
|
|
providerProfileApplyProgress(spec, profile, "secret-sync", "succeeded", { status: secretPayload.status });
|
|
providerProfileApplyProgress(spec, profile, "current-image-resolve", "running");
|
|
const liveBefore = await capture(config, spec.nodeKubeRoute, ["sh", "--", currentManagerImageScript(spec)]);
|
|
const liveBeforePayload = captureJsonPayload(liveBefore);
|
|
const sourceCommit = stringOrNull(liveBeforePayload.sourceCommit);
|
|
const envIdentity = stringOrNull(liveBeforePayload.envIdentity);
|
|
const digest = stringOrNull(liveBeforePayload.digest);
|
|
if (liveBefore.exitCode !== 0 || sourceCommit === null || envIdentity === null || digest === null) {
|
|
providerProfileApplyProgress(spec, profile, "current-image-resolve", "failed");
|
|
return applyFailure(configPath, spec, profile, "current-image-resolve", liveBeforePayload, liveBefore);
|
|
}
|
|
providerProfileApplyProgress(spec, profile, "current-image-resolve", "succeeded", { sourceCommit });
|
|
const image = agentRunImageArtifact(spec, { sourceCommit, envIdentity, digest, status: "reused" });
|
|
const renderedFiles = renderAgentRunGitopsFiles(spec, { sourceCommit, image });
|
|
providerProfileApplyProgress(spec, profile, "gitops-publish", "running", { sourceCommit });
|
|
const publish = await publishProfileGitops(config, spec, sourceCommit, renderedFiles);
|
|
providerProfileApplyProgress(spec, profile, "gitops-publish", publish.ok ? "succeeded" : "failed", { sourceCommit });
|
|
if (publish.ok !== true) {
|
|
return { ok: false, command: "agentrun provider-profile apply", mode: "confirmed-online-profile", mutation: true, configPath, target: compactAgentRunLaneStatusTarget(spec), profile, phase: "gitops-publish", result: publish, valuesPrinted: false };
|
|
}
|
|
providerProfileApplyProgress(spec, profile, "argocd-refresh-sync", "running", { sourceCommit });
|
|
const argo = await capture(config, spec.nodeKubeRoute, ["sh", "--", providerProfileArgoRefreshSyncScript(spec)]);
|
|
const argoPayload = captureJsonPayload(argo);
|
|
providerProfileApplyProgress(spec, profile, "argocd-refresh-sync", argo.exitCode === 0 && argoPayload.ok !== false ? "succeeded" : "failed", { sourceCommit });
|
|
if (argo.exitCode !== 0 || argoPayload.ok === false) {
|
|
return applyFailure(configPath, spec, profile, "argocd-refresh-sync", argoPayload, argo);
|
|
}
|
|
providerProfileApplyProgress(spec, profile, "manager-restart", "running", { sourceCommit });
|
|
const restart = await capture(config, spec.nodeKubeRoute, ["sh", "--", restartYamlLaneScript(spec)]);
|
|
const restartPayload = captureJsonPayload(restart);
|
|
providerProfileApplyProgress(spec, profile, "manager-restart", restart.exitCode === 0 && restartPayload.ok !== false ? "succeeded" : "failed", { sourceCommit });
|
|
if (restart.exitCode !== 0 || restartPayload.ok === false) {
|
|
return applyFailure(configPath, spec, profile, "manager-restart", restartPayload, restart);
|
|
}
|
|
providerProfileApplyProgress(spec, profile, "runtime-status", "running", { sourceCommit });
|
|
const liveAfter = await capture(config, spec.nodeKubeRoute, ["sh", "--", providerProfileLiveStatusScript(spec, secrets)]);
|
|
const liveAfterPayload = captureJsonPayload(liveAfter);
|
|
providerProfileApplyProgress(spec, profile, "runtime-status", liveAfter.exitCode === 0 && liveAfterPayload.ok !== false ? "succeeded" : "failed", { sourceCommit });
|
|
return {
|
|
ok: liveAfter.exitCode === 0 && liveAfterPayload.ok !== false,
|
|
command: "agentrun provider-profile apply",
|
|
mode: "confirmed-online-profile",
|
|
mutation: true,
|
|
configPath,
|
|
target: compactAgentRunLaneStatusTarget(spec),
|
|
profile: providerProfileSummary(spec, profile, secrets),
|
|
steps: {
|
|
activationPreflight,
|
|
secretSync: secretPayload,
|
|
gitops: publish.payload,
|
|
argo: argoPayload,
|
|
restart: restartPayload,
|
|
live: liveAfterPayload,
|
|
valuesPrinted: false,
|
|
},
|
|
renderedFiles: {
|
|
count: renderedFiles.length,
|
|
digest: renderedFilesDigest(renderedFiles),
|
|
valuesPrinted: false,
|
|
},
|
|
image: { sourceCommit, envIdentity, digest, status: "reused", valuesPrinted: false },
|
|
rebuildImages: false,
|
|
pipelineRun: false,
|
|
captures: options.full || options.raw || liveAfter.exitCode !== 0 ? {
|
|
argo: compactCapture(argo, { full: options.full || options.raw || argo.exitCode !== 0 }),
|
|
restart: compactCapture(restart, { full: options.full || options.raw || restart.exitCode !== 0 }),
|
|
live: compactCapture(liveAfter, { full: options.full || options.raw || liveAfter.exitCode !== 0 }),
|
|
} : undefined,
|
|
next: {
|
|
status: `bun scripts/cli.ts agentrun provider-profile status --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile}`,
|
|
validate: `bun scripts/cli.ts agentrun provider-profile validate --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile} --wait`,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function providerProfileActivationBlockedResult(
|
|
spec: AgentRunLaneSpec,
|
|
profile: string,
|
|
secrets: readonly AgentRunLaneSecretSpec[],
|
|
activationPreflight: Record<string, unknown>,
|
|
stage: "async-submit" | "worker-before-secret-sync",
|
|
): Record<string, unknown> {
|
|
return {
|
|
ok: false,
|
|
command: "agentrun provider-profile apply",
|
|
mode: stage === "async-submit" ? "activation-preflight-blocked-before-job" : "activation-preflight-blocked-before-secret-sync",
|
|
status: "blocked",
|
|
degradedReason: activationPreflight.reason ?? "existing-runner-provider-secret-activation-risk",
|
|
phase: "activation-preflight",
|
|
stage,
|
|
mutation: false,
|
|
blockedBeforeMutation: true,
|
|
jobCreated: stage === "worker-before-secret-sync",
|
|
jobAlreadyRunning: stage === "worker-before-secret-sync",
|
|
secretSyncStarted: false,
|
|
target: { node: spec.nodeId, lane: spec.lane, namespace: spec.runtime.namespace, valuesPrinted: false },
|
|
profile: providerProfileSummary(spec, profile, secrets),
|
|
activationPreflight,
|
|
stagedActivation: providerSecretStagedActivation(spec, [profile]),
|
|
next: {
|
|
observeRunners: `bun scripts/cli.ts agentrun control-plane cleanup-runners --node ${spec.nodeId} --lane ${spec.lane} --dry-run`,
|
|
retryAfterManagerFence: `bun scripts/cli.ts agentrun provider-profile apply --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile} --confirm`,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function providerProfileApplyProgress(
|
|
spec: AgentRunLaneSpec,
|
|
profile: string,
|
|
phase: string,
|
|
status: "running" | "succeeded" | "failed",
|
|
detail: Record<string, unknown> = {},
|
|
): void {
|
|
progressEvent("agentrun.provider-profile.apply.progress", {
|
|
node: spec.nodeId,
|
|
lane: spec.lane,
|
|
profile,
|
|
phase,
|
|
status,
|
|
...detail,
|
|
valuesPrinted: false,
|
|
});
|
|
}
|
|
|
|
async function providerProfileValidate(config: UniDeskConfig, options: ProviderProfileOptions): Promise<Record<string, unknown>> {
|
|
const { configPath, spec, profile, secrets } = resolveProviderProfile(options);
|
|
const inspected = inspectProviderProfileSources(spec, secrets);
|
|
const source = providerProfileSourceSummary(inspected);
|
|
if (!source.ready) return providerProfileSourceUnavailable("validate", configPath, spec, profile, secrets, source);
|
|
const prompt = options.prompt ?? `请只回复:${profile} upstream OK`;
|
|
const live = await capture(config, spec.nodeKubeRoute, ["sh", "--", providerProfileLiveStatusScript(spec, secrets)]);
|
|
const livePayload = captureJsonPayload(live);
|
|
const canRun = live.exitCode === 0 && livePayload.ok !== false;
|
|
return {
|
|
ok: canRun,
|
|
command: "agentrun provider-profile validate",
|
|
mode: "profile-runtime-preflight",
|
|
mutation: false,
|
|
configPath,
|
|
target: compactAgentRunLaneStatusTarget(spec),
|
|
profile: providerProfileSummary(spec, profile, secrets),
|
|
source,
|
|
promptBytes: Buffer.byteLength(prompt, "utf8"),
|
|
live: livePayload,
|
|
note: "This command validates YAML/live profile wiring. User-entry Web validation still runs through HWLAB web-probe so it uses the same Workbench path.",
|
|
webProbeNext: `bun scripts/cli.ts web-probe observe command <observer> --type sendPrompt --provider ${profile} --text '${prompt.replaceAll("'", "'\\''")}'`,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
interface ProviderProfileInspectedSecret {
|
|
readonly secret: AgentRunLaneSecretSpec;
|
|
readonly source: LaneSecretSource | null;
|
|
readonly inspection: LaneSecretSourceInspection;
|
|
}
|
|
|
|
function inspectProviderProfileSources(spec: AgentRunLaneSpec, secrets: readonly AgentRunLaneSecretSpec[]): ProviderProfileInspectedSecret[] {
|
|
const sources = new Map(collectLaneSecretSources(spec).map((source) => [source.id, source]));
|
|
return secrets.map((secret) => {
|
|
const source = sources.get(secret.id) ?? null;
|
|
if (source !== null) return { secret, source, inspection: inspectSecretSourceValue(spec, source) };
|
|
return {
|
|
secret,
|
|
source,
|
|
inspection: {
|
|
ok: false,
|
|
sourceValue: null,
|
|
summary: {
|
|
present: false,
|
|
sourceFilePresent: false,
|
|
sourcePath: secret.sourceRef,
|
|
fingerprint: null,
|
|
valueBytes: null,
|
|
degradedReason: "lane-secret-source-declaration-missing",
|
|
message: `secret ${secret.id} has no resolved lane source`,
|
|
valuesPrinted: false,
|
|
},
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
function providerProfileActivationTargets(items: readonly ProviderProfileInspectedSecret[]): ProviderSecretActivationTarget[] {
|
|
return items.map(({ secret, inspection }) => {
|
|
if (inspection.sourceValue === null) throw new Error(`provider profile source ${secret.id} is unavailable before activation preflight`);
|
|
return {
|
|
id: secret.id,
|
|
targetRef: secret.targetRef,
|
|
desiredEncodedFingerprint: providerSecretDesiredEncodedFingerprint(inspection.sourceValue.value),
|
|
};
|
|
});
|
|
}
|
|
|
|
function providerProfileSourceSummary(items: readonly ProviderProfileInspectedSecret[]): {
|
|
ready: boolean;
|
|
count: number;
|
|
presentCount: number;
|
|
missingCount: number;
|
|
items: Record<string, unknown>[];
|
|
valuesPrinted: false;
|
|
} {
|
|
const summaries = items.map(({ secret, inspection }) => ({
|
|
id: secret.id,
|
|
sourceRef: secret.sourceRef,
|
|
sourceMode: secret.sourceMode,
|
|
transform: secret.transform,
|
|
targetRef: secret.targetRef,
|
|
...inspection.summary,
|
|
valuesPrinted: false,
|
|
}));
|
|
const presentCount = summaries.filter((item) => item.present === true).length;
|
|
return {
|
|
ready: items.length > 0 && items.every((item) => item.inspection.ok),
|
|
count: items.length,
|
|
presentCount,
|
|
missingCount: items.length - presentCount,
|
|
items: summaries,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function providerProfileSourceUnavailable(
|
|
action: "status" | "apply" | "validate",
|
|
configPath: string,
|
|
spec: AgentRunLaneSpec,
|
|
profile: string,
|
|
secrets: readonly AgentRunLaneSecretSpec[],
|
|
source: ReturnType<typeof providerProfileSourceSummary>,
|
|
): Record<string, unknown> {
|
|
return {
|
|
ok: false,
|
|
command: `agentrun provider-profile ${action}`,
|
|
mode: "profile-source-preflight",
|
|
status: "blocked",
|
|
degradedReason: "lane-secret-source-unavailable",
|
|
mutation: false,
|
|
blockedBeforeMutation: true,
|
|
configPath,
|
|
target: compactAgentRunLaneStatusTarget(spec),
|
|
profile: providerProfileSummary(spec, profile, secrets),
|
|
source,
|
|
next: {
|
|
recheck: `bun scripts/cli.ts agentrun provider-profile status --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile} --raw`,
|
|
applyAfterOwnerProvision: `bun scripts/cli.ts agentrun provider-profile apply --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile} --confirm`,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function providerProfileLiveComparison(items: readonly ProviderProfileInspectedSecret[], payload: Record<string, unknown>): Record<string, unknown> & { ready: boolean } {
|
|
const liveItems = Array.isArray(record(payload.secrets).items)
|
|
? (record(payload.secrets).items as unknown[]).map(record)
|
|
: [];
|
|
const byTarget = new Map(liveItems.map((item) => [
|
|
`${stringOrNull(item.namespace) ?? ""}/${stringOrNull(item.name) ?? ""}/${stringOrNull(item.key) ?? ""}`,
|
|
item,
|
|
]));
|
|
const comparisons = items.map(({ secret, inspection }) => {
|
|
const target = secret.targetRef;
|
|
const live = byTarget.get(`${target.namespace}/${target.name}/${target.key}`) ?? {};
|
|
const desiredEncodedFingerprint = inspection.sourceValue === null
|
|
? null
|
|
: sha256Fingerprint(Buffer.from(inspection.sourceValue.value, "utf8").toString("base64"));
|
|
const liveEncodedFingerprint = stringOrNull(live.encodedFingerprint);
|
|
return {
|
|
id: secret.id,
|
|
targetRef: target,
|
|
desiredEncodedFingerprint,
|
|
liveEncodedFingerprint,
|
|
present: live.present === true,
|
|
keyPresent: live.keyPresent === true,
|
|
encodedFingerprintMatches: desiredEncodedFingerprint !== null && liveEncodedFingerprint === desiredEncodedFingerprint,
|
|
valuesPrinted: false,
|
|
};
|
|
});
|
|
return {
|
|
ready: comparisons.length > 0 && comparisons.every((item) => item.keyPresent && item.encodedFingerprintMatches),
|
|
count: comparisons.length,
|
|
matchingCount: comparisons.filter((item) => item.encodedFingerprintMatches).length,
|
|
items: comparisons,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function applyFailure(configPath: string, spec: AgentRunLaneSpec, profile: string, phase: string, payload: Record<string, unknown>, captureResult: Awaited<ReturnType<typeof capture>>): Record<string, unknown> {
|
|
return {
|
|
ok: false,
|
|
command: "agentrun provider-profile apply",
|
|
mode: "confirmed-online-profile",
|
|
mutation: true,
|
|
configPath,
|
|
target: compactAgentRunLaneStatusTarget(spec),
|
|
profile,
|
|
phase,
|
|
result: payload,
|
|
capture: compactCapture(captureResult, { full: true, stdoutTailChars: 4000, stderrTailChars: 4000 }),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
async function publishProfileGitops(config: UniDeskConfig, spec: AgentRunLaneSpec, sourceCommit: string, files: readonly { path: string; content: string }[]): Promise<Record<string, unknown> & { ok: boolean; payload: Record<string, unknown> }> {
|
|
const jobName = `profile-gitops-${spec.nodeId.toLowerCase()}-${spec.lane}-${Date.now().toString(36)}`.slice(0, 63);
|
|
const manifest = yamlLaneGitopsPublishJobManifest(spec, files, jobName);
|
|
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
|
if (created.exitCode !== 0) return { ok: false, payload: { ok: false, status: "create-failed", jobName, valuesPrinted: false }, capture: compactCapture(created, { full: true }), valuesPrinted: false };
|
|
const startedAt = Date.now();
|
|
let polls = 0;
|
|
let lastPayload: Record<string, unknown> = {};
|
|
while (Date.now() - startedAt < 300_000) {
|
|
polls += 1;
|
|
const probe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
|
|
const probePayload = captureJsonPayload(probe);
|
|
const publishPayload = yamlLaneGitopsPublishPayloadFromProbe(probePayload);
|
|
if (Object.keys(publishPayload).length > 0) lastPayload = publishPayload;
|
|
progressEvent("agentrun.provider-profile.gitops-publish.progress", { node: spec.nodeId, lane: spec.lane, sourceCommit, jobName, polls, status: stringOrNull(publishPayload.status) ?? "running", valuesPrinted: false });
|
|
if (probePayload.succeeded === true) return { ok: publishPayload.ok === true, payload: publishPayload, jobName, polls, elapsedMs: Date.now() - startedAt, probe: compactCapture(probe), valuesPrinted: false };
|
|
if (probePayload.failed === true) return { ok: false, payload: { ...lastPayload, ok: false, status: "failed", jobName, valuesPrinted: false }, jobName, polls, elapsedMs: Date.now() - startedAt, probe: compactCapture(probe, { full: true }), valuesPrinted: false };
|
|
await sleep(3_000);
|
|
}
|
|
return { ok: false, payload: { ...lastPayload, ok: false, status: "timeout", jobName, valuesPrinted: false }, jobName, polls, elapsedMs: Date.now() - startedAt, valuesPrinted: false };
|
|
}
|
|
|
|
function providerProfileSummary(spec: AgentRunLaneSpec, profile: string, secrets: readonly AgentRunLaneSecretSpec[]): Record<string, unknown> {
|
|
const credential = agentRunProviderCredentialRefs(spec, profile)[0] ?? null;
|
|
return {
|
|
profile,
|
|
declared: true,
|
|
backendProfile: profile === "gpt.pika" ? "gpt-pika" : profile,
|
|
secretIds: secrets.map((secret) => secret.id),
|
|
secretRef: credential?.secretRef ?? null,
|
|
configuredKeys: credential?.secretRef.keys ?? secrets.map((secret) => secret.targetRef.key),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function codexConfigSummary(config: NonNullable<AgentRunLaneSecretSpec["codexConfig"]>): Record<string, unknown> {
|
|
return {
|
|
modelProvider: config.modelProvider,
|
|
model: config.model,
|
|
reviewModel: config.reviewModel,
|
|
modelContextWindow: config.modelContextWindow,
|
|
modelAutoCompactTokenLimit: config.modelAutoCompactTokenLimit,
|
|
modelCatalogJson: config.modelCatalogJson,
|
|
approvalsReviewer: config.approvalsReviewer,
|
|
modelProviderCount: config.modelProviders.length,
|
|
providers: config.modelProviders.map((provider) => ({
|
|
id: provider.id,
|
|
baseUrl: provider.baseUrl,
|
|
wireApi: provider.wireApi,
|
|
requiresOpenaiAuth: provider.requiresOpenaiAuth,
|
|
})),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function providerRuntimeServiceTargets(secrets: readonly AgentRunLaneSecretSpec[]): Record<string, unknown>[] {
|
|
const targets: Record<string, unknown>[] = [];
|
|
const seen = new Set<string>();
|
|
for (const secret of secrets) {
|
|
for (const provider of secret.codexConfig?.modelProviders ?? []) {
|
|
const parsed = new URL(provider.baseUrl);
|
|
const hostParts = parsed.hostname.split(".");
|
|
if (hostParts.length < 4 || hostParts[2] !== "svc" || hostParts[3] !== "cluster") continue;
|
|
const port = parsed.port.length > 0 ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
|
|
const key = `${provider.id}/${hostParts[1]}/${hostParts[0]}/${port}`;
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
targets.push({
|
|
providerId: provider.id,
|
|
baseUrl: provider.baseUrl,
|
|
host: parsed.hostname,
|
|
namespace: hostParts[1],
|
|
name: hostParts[0],
|
|
port,
|
|
valuesPrinted: false,
|
|
});
|
|
}
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
function currentManagerImageScript(spec: AgentRunLaneSpec): string {
|
|
return [
|
|
"set -eu",
|
|
`namespace=${shQuote(spec.runtime.namespace)}`,
|
|
`deployment=${shQuote(spec.runtime.managerDeployment)}`,
|
|
"kubectl -n \"$namespace\" get deployment \"$deployment\" -o json >/tmp/agentrun-provider-profile-current-manager.json",
|
|
"node <<'NODE'",
|
|
"const fs = require('node:fs');",
|
|
" const j = JSON.parse(fs.readFileSync('/tmp/agentrun-provider-profile-current-manager.json', 'utf8'));",
|
|
" const c = (j.spec?.template?.spec?.containers || [])[0] || {};",
|
|
" const env = new Map((c.env || []).map((e) => [e.name, e.value || null]));",
|
|
" const image = c.image || '';",
|
|
" const digest = image.includes('@') ? image.split('@').pop() : null;",
|
|
" console.log(JSON.stringify({ ok: Boolean(env.get('AGENTRUN_SOURCE_COMMIT') && env.get('AGENTRUN_ENV_IDENTITY') && digest), sourceCommit: env.get('AGENTRUN_SOURCE_COMMIT'), envIdentity: env.get('AGENTRUN_ENV_IDENTITY'), digest, imageStatus: 'reused', valuesPrinted: false }));",
|
|
"NODE",
|
|
].join("\n");
|
|
}
|
|
|
|
function providerProfileLiveStatusScript(spec: AgentRunLaneSpec, secrets: readonly AgentRunLaneSecretSpec[]): string {
|
|
const refs = Buffer.from(JSON.stringify({
|
|
secrets: secrets.map((secret) => secret.targetRef),
|
|
providerServices: providerRuntimeServiceTargets(secrets),
|
|
}), "utf8").toString("base64");
|
|
return [
|
|
"set -eu",
|
|
`namespace=${shQuote(spec.runtime.namespace)}`,
|
|
`deployment=${shQuote(spec.runtime.managerDeployment)}`,
|
|
`refs_b64=${shQuote(refs)}`,
|
|
"kubectl -n \"$namespace\" get deployment \"$deployment\" -o json >/tmp/agentrun-provider-profile-deploy.json",
|
|
"REFS_B64=\"$refs_b64\" node <<'NODE'",
|
|
"const fs = require('node:fs');",
|
|
"const cp = require('node:child_process');",
|
|
"const refs = JSON.parse(Buffer.from(process.env.REFS_B64 || '', 'base64').toString('utf8'));",
|
|
"const deployment = JSON.parse(fs.readFileSync('/tmp/agentrun-provider-profile-deploy.json', 'utf8'));",
|
|
"const container = deployment.spec?.template?.spec?.containers?.[0] || {};",
|
|
"const env = new Map((container.env || []).map((e) => [e.name, e.value || null]));",
|
|
"const noProxy = env.get('AGENTRUN_RUNNER_NO_PROXY_EXTRA') || '';",
|
|
"const secretItems = (refs.secrets || []).map((ref) => {",
|
|
" const out = cp.spawnSync('kubectl', ['-n', ref.namespace, 'get', 'secret', ref.name, '-o', 'json'], { encoding: 'utf8' });",
|
|
" if (out.status !== 0) return { namespace: ref.namespace, name: ref.name, key: ref.key, present: false, keyPresent: false, valuesPrinted: false };",
|
|
" const secret = JSON.parse(out.stdout);",
|
|
" const raw = secret.data?.[ref.key] || '';",
|
|
" const encodedBytes = raw ? Buffer.byteLength(raw, 'utf8') : 0;",
|
|
" const encodedFingerprint = raw ? 'sha256:' + require('node:crypto').createHash('sha256').update(raw).digest('hex') : null;",
|
|
" return { namespace: ref.namespace, name: ref.name, key: ref.key, present: true, keyPresent: Boolean(raw), encodedBytes, encodedFingerprint, valuesPrinted: false };",
|
|
"});",
|
|
"const serviceItems = (refs.providerServices || []).map((target) => {",
|
|
" const out = cp.spawnSync('kubectl', ['-n', target.namespace, 'get', 'service', target.name, '-o', 'json'], { encoding: 'utf8' });",
|
|
" if (out.status !== 0) return { ...target, present: false, portPresent: false, valuesPrinted: false };",
|
|
" const service = JSON.parse(out.stdout);",
|
|
" const ports = Array.isArray(service.spec?.ports) ? service.spec.ports : [];",
|
|
" return { ...target, present: true, portPresent: ports.some((item) => Number(item.port) === Number(target.port)), valuesPrinted: false };",
|
|
"});",
|
|
"const secretsReady = secretItems.every((item) => item.keyPresent);",
|
|
"const servicesReady = serviceItems.every((item) => item.present && item.portPresent);",
|
|
"console.log(JSON.stringify({ ok: secretsReady && servicesReady, manager: { deploymentExists: Boolean(deployment.metadata?.name), readyReplicas: deployment.status?.readyReplicas ?? null, replicas: deployment.status?.replicas ?? null, sourceCommit: env.get('AGENTRUN_SOURCE_COMMIT'), envIdentity: env.get('AGENTRUN_ENV_IDENTITY'), runnerEgressProxyUrl: env.get('AGENTRUN_RUNNER_EGRESS_PROXY_URL'), runnerNoProxyExtra: noProxy, noProxyContainsHyue: noProxy.includes('hyueapi.com'), noProxyContainsPikapython: noProxy.includes('api.pikapython.com') || noProxy.includes('.pikapython.com'), valuesPrinted: false }, secrets: { count: secretItems.length, ready: secretsReady, items: secretItems, valuesPrinted: false }, providerServices: { count: serviceItems.length, ready: servicesReady, items: serviceItems, valuesPrinted: false }, runtimeSecretValuesDecoded: false, valuesPrinted: false }));",
|
|
"NODE",
|
|
].join("\n");
|
|
}
|
|
|
|
function providerProfileRuntimeStatusScript(spec: AgentRunLaneSpec, secrets: readonly AgentRunLaneSecretSpec[]): string {
|
|
const refs = Buffer.from(JSON.stringify({
|
|
secrets: secrets.map((secret) => ({ id: secret.id, targetRef: secret.targetRef })),
|
|
providerServices: providerRuntimeServiceTargets(secrets),
|
|
}), "utf8").toString("base64");
|
|
return [
|
|
"set -eu",
|
|
`namespace=${shQuote(spec.runtime.namespace)}`,
|
|
`deployment=${shQuote(spec.runtime.managerDeployment)}`,
|
|
`refs_b64=${shQuote(refs)}`,
|
|
"kubectl -n \"$namespace\" get deployment \"$deployment\" -o json >/tmp/agentrun-provider-profile-runtime-deploy.json",
|
|
"REFS_B64=\"$refs_b64\" node <<'NODE'",
|
|
"const fs = require('node:fs');",
|
|
"const cp = require('node:child_process');",
|
|
"const refs = JSON.parse(Buffer.from(process.env.REFS_B64 || '', 'base64').toString('utf8'));",
|
|
"const deployment = JSON.parse(fs.readFileSync('/tmp/agentrun-provider-profile-runtime-deploy.json', 'utf8'));",
|
|
"const secretItems = (refs.secrets || []).map((entry) => {",
|
|
" const ref = entry.targetRef || {};",
|
|
" const out = cp.spawnSync('kubectl', ['-n', ref.namespace, 'get', 'secret', ref.name, '-o', 'json'], { encoding: 'utf8' });",
|
|
" if (out.status !== 0) return { id: entry.id || null, namespace: ref.namespace || null, name: ref.name || null, key: ref.key || null, present: false, keyPresent: false, valuesPrinted: false };",
|
|
" const secret = JSON.parse(out.stdout);",
|
|
" const data = secret.data && typeof secret.data === 'object' ? secret.data : {};",
|
|
" const keyPresent = Object.prototype.hasOwnProperty.call(data, ref.key) && typeof data[ref.key] === 'string' && data[ref.key].length > 0;",
|
|
" return { id: entry.id || null, namespace: ref.namespace || null, name: ref.name || null, key: ref.key || null, present: true, keyPresent, valuesPrinted: false };",
|
|
"});",
|
|
"const serviceItems = (refs.providerServices || []).map((target) => {",
|
|
" const out = cp.spawnSync('kubectl', ['-n', target.namespace, 'get', 'service', target.name, '-o', 'json'], { encoding: 'utf8' });",
|
|
" if (out.status !== 0) return { ...target, present: false, portPresent: false, valuesPrinted: false };",
|
|
" const service = JSON.parse(out.stdout);",
|
|
" const ports = Array.isArray(service.spec?.ports) ? service.spec.ports : [];",
|
|
" return { ...target, present: true, portPresent: ports.some((item) => Number(item.port) === Number(target.port)), valuesPrinted: false };",
|
|
"});",
|
|
"const secretsReady = secretItems.length > 0 && secretItems.every((item) => item.keyPresent);",
|
|
"const servicesReady = serviceItems.every((item) => item.present && item.portPresent);",
|
|
"console.log(JSON.stringify({ ok: secretsReady && servicesReady, manager: { deploymentExists: Boolean(deployment.metadata?.name), readyReplicas: deployment.status?.readyReplicas ?? null, replicas: deployment.status?.replicas ?? null, valuesPrinted: false }, secrets: { count: secretItems.length, ready: secretsReady, presentCount: secretItems.filter((item) => item.present).length, keyPresentCount: secretItems.filter((item) => item.keyPresent).length, items: secretItems, valuesPrinted: false }, providerServices: { count: serviceItems.length, ready: servicesReady, items: serviceItems, valuesPrinted: false }, ownerSourceInspected: false, runtimeSecretValuesDecoded: false, valuesPrinted: false }));",
|
|
"NODE",
|
|
].join("\n");
|
|
}
|
|
|
|
function providerProfileArgoRefreshSyncScript(spec: AgentRunLaneSpec): string {
|
|
return [
|
|
refreshYamlLaneScript(spec),
|
|
`namespace=${shQuote(spec.gitops.argoNamespace)}`,
|
|
`application=${shQuote(spec.gitops.argoApplication)}`,
|
|
"kubectl -n \"$namespace\" patch application \"$application\" --type=merge -p '{\"operation\":{\"sync\":{\"syncStrategy\":{\"hook\":{}}}}}' >/tmp/agentrun-provider-profile-argo-sync.out 2>/tmp/agentrun-provider-profile-argo-sync.err || true",
|
|
"for i in $(seq 1 60); do",
|
|
" app_json=$(kubectl -n \"$namespace\" get application \"$application\" -o json 2>/dev/null || printf '{}')",
|
|
" APP_JSON=\"$app_json\" node <<'NODE' >/tmp/agentrun-provider-profile-argo-state.json",
|
|
"const app = JSON.parse(process.env.APP_JSON || '{}');",
|
|
"const sync = app.status?.sync || {};",
|
|
"const health = app.status?.health || {};",
|
|
"const op = app.status?.operationState || null;",
|
|
"console.log(JSON.stringify({sync: sync.status || null, health: health.status || null, revision: sync.revision || null, operationPhase: op?.phase || null, message: op?.message || null}));",
|
|
"NODE",
|
|
" if node -e \"const s=require('fs').readFileSync('/tmp/agentrun-provider-profile-argo-state.json','utf8'); const j=JSON.parse(s); process.exit(j.sync==='Synced' && j.health==='Healthy' && (!j.operationPhase || j.operationPhase==='Succeeded') ? 0 : 1)\"; then break; fi",
|
|
" sleep 3",
|
|
"done",
|
|
"cat /tmp/agentrun-provider-profile-argo-state.json | node -e \"let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{const j=JSON.parse(s||'{}'); console.log(JSON.stringify({ok:j.sync==='Synced' && j.health==='Healthy', ...j, valuesPrinted:false}));})\"",
|
|
].join("\n");
|
|
}
|