ci: migrate sentinel to gitea pac
This commit is contained in:
@@ -6,6 +6,8 @@ const pipelineRun = process.env.PIPELINERUN || "";
|
||||
const shouldWait = process.env.WAIT === "true";
|
||||
const timeoutSeconds = requiredPositiveNumber("TIMEOUT_SECONDS");
|
||||
const pollIntervalSeconds = requiredPositiveNumber("POLL_INTERVAL_SECONDS");
|
||||
const logsTailLines = Number(process.env.LOGS_TAIL_LINES || "240");
|
||||
const maxLogBytes = Number(process.env.MAX_LOG_BYTES || "32000");
|
||||
const host = process.env.KUBERNETES_SERVICE_HOST;
|
||||
const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443");
|
||||
const token = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8").trim();
|
||||
@@ -106,6 +108,7 @@ const condition = succeededCondition(latest.object);
|
||||
const completed = condition?.status === "True";
|
||||
const failed = condition?.status === "False";
|
||||
const terminal = completed || failed;
|
||||
const logsTail = terminal ? await pipelineRunLogsTail() : "";
|
||||
const output = {
|
||||
ok: !failed,
|
||||
submitted: true,
|
||||
@@ -120,6 +123,7 @@ const output = {
|
||||
timedOutWait: shouldWait && !terminal,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
pipelineRun: compact(latest.object),
|
||||
logsTail,
|
||||
statusAuthority: "kubernetes-api-serviceaccount",
|
||||
parsedDownstreamCliOutput: false,
|
||||
valuesRedacted: true,
|
||||
@@ -132,3 +136,25 @@ function requiredPositiveNumber(name) {
|
||||
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number`);
|
||||
return value;
|
||||
}
|
||||
|
||||
async function pipelineRunLogsTail() {
|
||||
const selector = encodeURIComponent(`tekton.dev/pipelineRun=${pipelineRun}`);
|
||||
const podsResult = await request("GET", `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods?labelSelector=${selector}`);
|
||||
if (podsResult.status < 200 || podsResult.status >= 300) return "";
|
||||
const pods = parseBody(podsResult);
|
||||
const chunks = [];
|
||||
for (const pod of Array.isArray(pods?.items) ? pods.items : []) {
|
||||
const podName = pod?.metadata?.name;
|
||||
const containers = [
|
||||
...(Array.isArray(pod?.spec?.initContainers) ? pod.spec.initContainers : []),
|
||||
...(Array.isArray(pod?.spec?.containers) ? pod.spec.containers : []),
|
||||
];
|
||||
for (const container of containers) {
|
||||
if (!podName || !container?.name) continue;
|
||||
const path = `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods/${encodeURIComponent(podName)}/log?container=${encodeURIComponent(container.name)}&tailLines=${Math.max(1, logsTailLines)}`;
|
||||
const log = await request("GET", path);
|
||||
if (log.status >= 200 && log.status < 300 && log.text) chunks.push(log.text);
|
||||
}
|
||||
}
|
||||
return chunks.join("\n").slice(-Math.max(1, maxLogBytes));
|
||||
}
|
||||
|
||||
@@ -318,6 +318,9 @@ export function runSentinelPublishJob(state: SentinelCicdState, publishGitops: b
|
||||
const pipelineRunName = sentinelPipelineRunName(state, rerun);
|
||||
const manifest = sentinelPublishPipelineRunManifest(state, pipelineRunName, publishGitops);
|
||||
const namespace = stringAt(state.cicd, "builder.namespace");
|
||||
if (process.env.KUBERNETES_SERVICE_HOST && process.env.KUBERNETES_SERVICE_PORT) {
|
||||
return runSentinelPublishPipelineRunInCluster(state, manifest, namespace, pipelineRunName, publishGitops, timeoutSeconds);
|
||||
}
|
||||
sentinelProgressEvent("sentinel.publish.progress", { phase: "create-pipelinerun", status: "submitting", pipelineRun: pipelineRunName, publishGitops, sourceCommit: state.sourceHead.commit, node: state.spec.nodeId, lane: state.spec.lane });
|
||||
const created = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", createTektonPipelineRunScript(namespace, manifest)], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 });
|
||||
if (created.exitCode !== 0) {
|
||||
@@ -367,6 +370,47 @@ export function runSentinelPublishJob(state: SentinelCicdState, publishGitops: b
|
||||
return withSentinelRemoteJobDiagnostics(state, { ok: false, phase: "pipelinerun-timeout", resourceKind: "PipelineRun", jobName: pipelineRunName, payload: { ok: false, status: "timeout", valuesRedacted: true }, polls, elapsedMs: Date.now() - startedAt, probe: lastProbe, valuesRedacted: true }, "publish");
|
||||
}
|
||||
|
||||
function runSentinelPublishPipelineRunInCluster(state: SentinelCicdState, manifest: Record<string, unknown>, namespace: string, pipelineRunName: string, publishGitops: boolean, timeoutSeconds: number): SentinelRemoteJobResult {
|
||||
sentinelProgressEvent("sentinel.publish.progress", { phase: "native-submit-pipelinerun", status: "submitting", pipelineRun: pipelineRunName, publishGitops, sourceCommit: state.sourceHead.commit, node: state.spec.nodeId, lane: state.spec.lane });
|
||||
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
|
||||
const result = runCommand(["node", "scripts/native/cicd/submit-pipelinerun.mjs"], repoRoot, {
|
||||
input: manifestB64,
|
||||
timeoutMs: Math.max(5_000, Math.min(timeoutSeconds * 1000, controlPlaneWaitWarningSeconds(state) * 1000)),
|
||||
env: {
|
||||
...process.env,
|
||||
NAMESPACE: namespace,
|
||||
PIPELINERUN: pipelineRunName,
|
||||
WAIT: "true",
|
||||
TIMEOUT_SECONDS: String(Math.max(1, Math.min(timeoutSeconds, controlPlaneWaitWarningSeconds(state)))),
|
||||
POLL_INTERVAL_SECONDS: "2",
|
||||
LOGS_TAIL_LINES: "240",
|
||||
MAX_LOG_BYTES: "48000",
|
||||
},
|
||||
});
|
||||
const parsed = parseJsonObject(result.stdout) ?? {};
|
||||
const payload = sentinelPayloadFromLogs(String(parsed.logsTail ?? ""));
|
||||
const pipelineRun = record(parsed.pipelineRun);
|
||||
const failed = parsed.failed === true || record(pipelineRun).conditionStatus === "False";
|
||||
const completed = parsed.completed === true || record(pipelineRun).conditionStatus === "True";
|
||||
const phase = failed ? "pipelinerun-failed" : completed ? "pipelinerun-succeeded" : "pipelinerun-timeout";
|
||||
const ok = result.exitCode === 0 && completed && payload.ok === true;
|
||||
return withSentinelRemoteJobDiagnostics(state, {
|
||||
ok,
|
||||
phase,
|
||||
resourceKind: "PipelineRun",
|
||||
jobName: pipelineRunName,
|
||||
payload: Object.keys(payload).length === 0 ? { ok: false, status: completed ? "result-missing" : failed ? "failed" : "timeout", valuesRedacted: true } : payload,
|
||||
polls: parsed.polls ?? null,
|
||||
elapsedMs: parsed.elapsedMs ?? result.durationMs ?? null,
|
||||
probe: {
|
||||
nativeSubmit: parsed,
|
||||
capture: compactCommand(result),
|
||||
valuesRedacted: true,
|
||||
},
|
||||
valuesRedacted: true,
|
||||
}, "publish");
|
||||
}
|
||||
|
||||
export function sentinelPublishPipelineRunManifest(state: SentinelCicdState, pipelineRunName: string, publishGitops: boolean): Record<string, unknown> {
|
||||
const namespace = stringAt(state.cicd, "builder.namespace");
|
||||
const buildkitImage = requireSentinelBuildkitImage(state);
|
||||
@@ -808,11 +852,12 @@ function sentinelPublishShell(state: SentinelCicdState, jobName: string, publish
|
||||
"fi",
|
||||
"gitops_finished_ms=$(now_ms)",
|
||||
"finished_ms=$(now_ms)",
|
||||
"node - \"$job_name\" \"$source_commit\" \"$source_stage_ref\" \"$mirror_commit\" \"$image_ref\" \"$digest_ref\" \"$gitops_commit\" \"$changed\" \"$file_count\" \"$started_ms\" \"$finished_ms\" \"$source_fetch_started_ms\" \"$source_fetch_finished_ms\" \"$monitor_web_verify_started_ms\" \"$monitor_web_verify_finished_ms\" \"$image_build_started_ms\" \"$image_build_finished_ms\" \"$gitops_started_ms\" \"$gitops_finished_ms\" \"$env_reuse_mode\" \"$env_reuse_node_deps_path\" \"$env_reuse_node_deps_present\" \"$env_reuse_node_deps_entries\" \"$env_reuse_linked_node_deps\" \"$image_build_cache_hits\" \"$image_build_step_lines\" \"$image_build_log_tail_b64\" \"$image_build_builder\" \"$image_build_package_mode\" \"$image_build_network_mode\" \"$image_build_proxy_source\" \"$image_build_http_proxy_present\" \"$image_build_https_proxy_present\" \"$image_build_all_proxy_present\" \"$image_build_no_proxy_present\" \"$context_ignore_entries\" <<'NODE'",
|
||||
"const [jobName, sourceCommit, sourceStageRef, mirrorCommit, imageRef, digestRef, gitopsCommit, changed, fileCount, startedMs, finishedMs, sourceFetchStartedMs, sourceFetchFinishedMs, monitorWebVerifyStartedMs, monitorWebVerifyFinishedMs, imageBuildStartedMs, imageBuildFinishedMs, gitopsStartedMs, gitopsFinishedMs, envReuseMode, envReuseNodeDepsPath, envReuseNodeDepsPresent, envReuseNodeDepsEntries, envReuseLinkedNodeDeps, imageBuildCacheHits, imageBuildStepLines, imageBuildLogTailB64, imageBuildBuilder, imageBuildPackageMode, imageBuildNetworkMode, imageBuildProxySource, imageBuildHttpProxyPresent, imageBuildHttpsProxyPresent, imageBuildAllProxyPresent, imageBuildNoProxyPresent, contextIgnoreEntries] = process.argv.slice(2);",
|
||||
`source_authority=${shellQuote(state.sourceHead.sourceAuthority)}`,
|
||||
"node - \"$job_name\" \"$source_commit\" \"$source_stage_ref\" \"$mirror_commit\" \"$image_ref\" \"$digest_ref\" \"$gitops_commit\" \"$changed\" \"$file_count\" \"$started_ms\" \"$finished_ms\" \"$source_fetch_started_ms\" \"$source_fetch_finished_ms\" \"$monitor_web_verify_started_ms\" \"$monitor_web_verify_finished_ms\" \"$image_build_started_ms\" \"$image_build_finished_ms\" \"$gitops_started_ms\" \"$gitops_finished_ms\" \"$env_reuse_mode\" \"$env_reuse_node_deps_path\" \"$env_reuse_node_deps_present\" \"$env_reuse_node_deps_entries\" \"$env_reuse_linked_node_deps\" \"$image_build_cache_hits\" \"$image_build_step_lines\" \"$image_build_log_tail_b64\" \"$image_build_builder\" \"$image_build_package_mode\" \"$image_build_network_mode\" \"$image_build_proxy_source\" \"$image_build_http_proxy_present\" \"$image_build_https_proxy_present\" \"$image_build_all_proxy_present\" \"$image_build_no_proxy_present\" \"$context_ignore_entries\" \"$source_authority\" <<'NODE'",
|
||||
"const [jobName, sourceCommit, sourceStageRef, mirrorCommit, imageRef, digestRef, gitopsCommit, changed, fileCount, startedMs, finishedMs, sourceFetchStartedMs, sourceFetchFinishedMs, monitorWebVerifyStartedMs, monitorWebVerifyFinishedMs, imageBuildStartedMs, imageBuildFinishedMs, gitopsStartedMs, gitopsFinishedMs, envReuseMode, envReuseNodeDepsPath, envReuseNodeDepsPresent, envReuseNodeDepsEntries, envReuseLinkedNodeDeps, imageBuildCacheHits, imageBuildStepLines, imageBuildLogTailB64, imageBuildBuilder, imageBuildPackageMode, imageBuildNetworkMode, imageBuildProxySource, imageBuildHttpProxyPresent, imageBuildHttpsProxyPresent, imageBuildAllProxyPresent, imageBuildNoProxyPresent, contextIgnoreEntries, sourceAuthority] = process.argv.slice(2);",
|
||||
"const elapsed = (start, finish) => Number(finish) - Number(start);",
|
||||
"const cacheHits = Number(imageBuildCacheHits || 0);",
|
||||
"console.log(JSON.stringify({ ok:true, status:'succeeded', jobName, sourceCommit, sourceStageRef, sourceAuthority:'git-mirror-snapshot', mirrorCommit, imageRef, digestRef, gitopsCommit: gitopsCommit || null, changed: changed === 'true', fileCount: Number(fileCount || 0), elapsedMs: elapsed(startedMs, finishedMs), stageTimings: { sourceFetchMs: elapsed(sourceFetchStartedMs, sourceFetchFinishedMs), monitorWebVerifyMs: elapsed(monitorWebVerifyStartedMs, monitorWebVerifyFinishedMs), imageBuildMs: elapsed(imageBuildStartedMs, imageBuildFinishedMs), gitopsMs: elapsed(gitopsStartedMs, gitopsFinishedMs), totalMs: elapsed(startedMs, finishedMs), valuesRedacted:true }, envReuse: { mode: envReuseMode, nodeDepsPath: envReuseNodeDepsPath, nodeDepsPresent: envReuseNodeDepsPresent === 'true', nodeDepsEntries: Number(envReuseNodeDepsEntries || 0), linkedNodeDeps: Number(envReuseLinkedNodeDeps || 0), dependencyReuse: envReuseNodeDepsPresent === 'true' ? 'hit' : 'miss', valuesRedacted:true }, imageBuild: { builder: 'k8s-buildkit-rootless', builderImage: imageBuildBuilder, cacheHitLines: cacheHits, stepLines: Number(imageBuildStepLines || 0), layerCache: cacheHits > 0 ? 'hit' : 'unknown-or-miss', packageMode: imageBuildPackageMode, networkMode: imageBuildNetworkMode, proxySource: imageBuildProxySource, proxy: { httpProxyPresent: imageBuildHttpProxyPresent === 'true', httpsProxyPresent: imageBuildHttpsProxyPresent === 'true', allProxyPresent: imageBuildAllProxyPresent === 'true', noProxyPresent: imageBuildNoProxyPresent === 'true', valuesRedacted:true }, contextIgnoreEntries: Number(contextIgnoreEntries || 0), verifyLocation: 'pre-image-build', logTail: Buffer.from(imageBuildLogTailB64 || '', 'base64').toString('utf8'), valuesRedacted:true }, completedStages: ['source-fetch', 'monitor-web-verify', 'image-build', gitopsCommit ? 'gitops' : 'gitops-skipped'], valuesRedacted:true }));",
|
||||
"console.log(JSON.stringify({ ok:true, status:'succeeded', jobName, sourceCommit, sourceStageRef, sourceAuthority, mirrorCommit, imageRef, digestRef, gitopsCommit: gitopsCommit || null, changed: changed === 'true', fileCount: Number(fileCount || 0), elapsedMs: elapsed(startedMs, finishedMs), stageTimings: { sourceFetchMs: elapsed(sourceFetchStartedMs, sourceFetchFinishedMs), monitorWebVerifyMs: elapsed(monitorWebVerifyStartedMs, monitorWebVerifyFinishedMs), imageBuildMs: elapsed(imageBuildStartedMs, imageBuildFinishedMs), gitopsMs: elapsed(gitopsStartedMs, gitopsFinishedMs), totalMs: elapsed(startedMs, finishedMs), valuesRedacted:true }, envReuse: { mode: envReuseMode, nodeDepsPath: envReuseNodeDepsPath, nodeDepsPresent: envReuseNodeDepsPresent === 'true', nodeDepsEntries: Number(envReuseNodeDepsEntries || 0), linkedNodeDeps: Number(envReuseLinkedNodeDeps || 0), dependencyReuse: envReuseNodeDepsPresent === 'true' ? 'hit' : 'miss', valuesRedacted:true }, imageBuild: { builder: 'k8s-buildkit-rootless', builderImage: imageBuildBuilder, cacheHitLines: cacheHits, stepLines: Number(imageBuildStepLines || 0), layerCache: cacheHits > 0 ? 'hit' : 'unknown-or-miss', packageMode: imageBuildPackageMode, networkMode: imageBuildNetworkMode, proxySource: imageBuildProxySource, proxy: { httpProxyPresent: imageBuildHttpProxyPresent === 'true', httpsProxyPresent: imageBuildHttpsProxyPresent === 'true', allProxyPresent: imageBuildAllProxyPresent === 'true', noProxyPresent: imageBuildNoProxyPresent === 'true', valuesRedacted:true }, contextIgnoreEntries: Number(contextIgnoreEntries || 0), verifyLocation: 'pre-image-build', logTail: Buffer.from(imageBuildLogTailB64 || '', 'base64').toString('utf8'), valuesRedacted:true }, completedStages: ['source-fetch', 'monitor-web-verify', 'image-build', gitopsCommit ? 'gitops' : 'gitops-skipped'], valuesRedacted:true }));",
|
||||
"NODE",
|
||||
"trap - EXIT",
|
||||
].join("\n");
|
||||
|
||||
@@ -16,6 +16,14 @@ export type WebProbeSentinelPublishAction = "publish-current";
|
||||
export type WebProbeSentinelMaintenanceAction = "status" | "start" | "stop";
|
||||
export type WebProbeSentinelDashboardAction = "verify" | "screenshot" | "trigger";
|
||||
export type WebProbeSentinelReportView = "summary" | "turn-summary" | "findings" | "trace-frame" | "auth-session-switch-summary";
|
||||
export type WebProbeSentinelSourceAuthority = "git-mirror-snapshot" | "gitea-snapshot";
|
||||
|
||||
export interface WebProbeSentinelSourceOverrideOptions {
|
||||
readonly sourceCommit: string | null;
|
||||
readonly sourceStageRef: string | null;
|
||||
readonly sourceMirrorCommit: string | null;
|
||||
readonly sourceAuthority: WebProbeSentinelSourceAuthority | null;
|
||||
}
|
||||
|
||||
export type WebProbeSentinelOptions =
|
||||
| {
|
||||
@@ -36,6 +44,7 @@ export type WebProbeSentinelOptions =
|
||||
readonly confirm: boolean;
|
||||
readonly wait: boolean;
|
||||
readonly timeoutSeconds: number;
|
||||
readonly sourceOverride: WebProbeSentinelSourceOverrideOptions;
|
||||
}
|
||||
| {
|
||||
readonly kind: "control-plane";
|
||||
@@ -48,6 +57,7 @@ export type WebProbeSentinelOptions =
|
||||
readonly wait: boolean;
|
||||
readonly timeoutSeconds: number;
|
||||
readonly rerun: boolean;
|
||||
readonly sourceOverride: WebProbeSentinelSourceOverrideOptions;
|
||||
}
|
||||
| {
|
||||
readonly kind: "publish";
|
||||
@@ -60,6 +70,7 @@ export type WebProbeSentinelOptions =
|
||||
readonly wait: boolean;
|
||||
readonly timeoutSeconds: number;
|
||||
readonly rerun: boolean;
|
||||
readonly sourceOverride: WebProbeSentinelSourceOverrideOptions;
|
||||
}
|
||||
| {
|
||||
readonly kind: "maintenance";
|
||||
@@ -146,7 +157,7 @@ export interface SourceHead {
|
||||
readonly commit: string | null;
|
||||
readonly stageRef: string | null;
|
||||
readonly mirrorCommit: string | null;
|
||||
readonly sourceAuthority: "git-mirror-cache" | "git-mirror-snapshot";
|
||||
readonly sourceAuthority: "git-mirror-cache" | "git-mirror-snapshot" | "gitea-cache" | "gitea-snapshot";
|
||||
readonly latestDrift: boolean;
|
||||
readonly result: CompactCommandResult;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
type WebProbeSentinelDashboardAction,
|
||||
type WebProbeSentinelOptions,
|
||||
type WebProbeSentinelReportView,
|
||||
type WebProbeSentinelSourceAuthority,
|
||||
} from "./hwlab-node-web-sentinel-cicd-shared";
|
||||
import {
|
||||
applySentinelArgoApplication,
|
||||
@@ -108,6 +109,8 @@ export type {
|
||||
WebProbeSentinelDashboardAction,
|
||||
WebProbeSentinelOptions,
|
||||
WebProbeSentinelReportView,
|
||||
WebProbeSentinelSourceAuthority,
|
||||
WebProbeSentinelSourceOverrideOptions,
|
||||
} from "./hwlab-node-web-sentinel-cicd-shared";
|
||||
export {
|
||||
arrayAt,
|
||||
@@ -143,13 +146,13 @@ export interface SentinelSourceOverride {
|
||||
readonly commit: string;
|
||||
readonly stageRef?: string | null;
|
||||
readonly mirrorCommit?: string | null;
|
||||
readonly sourceAuthority: "git-mirror-snapshot";
|
||||
readonly sourceAuthority: WebProbeSentinelSourceAuthority;
|
||||
}
|
||||
|
||||
export function runWebProbeSentinelCommand(spec: HwlabRuntimeLaneSpec, options: WebProbeSentinelOptions): RenderedCliResult {
|
||||
if (options.kind === "config") return withWebProbeSentinelConfigRendered(webProbeSentinelConfigPlan(spec, options.action, options.sentinelId));
|
||||
requireSentinelIdForRegistry(spec, options.sentinelId, `web-probe sentinel ${options.kind}`);
|
||||
const state = loadSentinelCicdState(spec, options.sentinelId, options.timeoutSeconds, sentinelSourceResolveMode(options));
|
||||
const state = loadSentinelCicdState(spec, options.sentinelId, options.timeoutSeconds, sentinelSourceResolveMode(options), sentinelSourceOverrideFromOptions(options));
|
||||
if (options.kind === "image") return runSentinelImage(state, options);
|
||||
if (options.kind === "control-plane") return runSentinelControlPlane(state, options);
|
||||
if (options.kind === "publish") return runSentinelPublishCurrent(state, options);
|
||||
@@ -159,6 +162,19 @@ export function runWebProbeSentinelCommand(spec: HwlabRuntimeLaneSpec, options:
|
||||
return runSentinelReport(state, options);
|
||||
}
|
||||
|
||||
function sentinelSourceOverrideFromOptions(options: WebProbeSentinelOptions): SentinelSourceOverride | null {
|
||||
if (options.kind !== "image" && options.kind !== "control-plane" && options.kind !== "publish") return null;
|
||||
const override = options.sourceOverride;
|
||||
if (override.sourceCommit === null && override.sourceStageRef === null && override.sourceMirrorCommit === null && override.sourceAuthority === null) return null;
|
||||
if (override.sourceCommit === null) throw new Error("--source-commit is required when overriding web-probe sentinel source");
|
||||
return {
|
||||
commit: override.sourceCommit,
|
||||
stageRef: override.sourceStageRef,
|
||||
mirrorCommit: override.sourceMirrorCommit,
|
||||
sourceAuthority: override.sourceAuthority ?? "git-mirror-snapshot",
|
||||
};
|
||||
}
|
||||
|
||||
function sentinelSourceResolveMode(options: WebProbeSentinelOptions): SourceResolveMode {
|
||||
if (options.kind === "image" && options.action === "build" && options.confirm && options.wait) return "sync";
|
||||
if (options.kind === "control-plane" && options.action === "trigger-current" && options.confirm && options.wait) return "sync";
|
||||
@@ -347,6 +363,51 @@ function runSentinelPublishCurrentConfirmedInner(state: SentinelCicdState, optio
|
||||
lane: state.spec.lane,
|
||||
sentinelId: state.sentinelId,
|
||||
});
|
||||
if (process.env.KUBERNETES_SERVICE_HOST && process.env.KUBERNETES_SERVICE_PORT) {
|
||||
const publish = runSentinelPublishJob(state, true, Math.max(1, remainingBudgetSeconds()), options.rerun);
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
const ok = record(publish).ok === true;
|
||||
const result = {
|
||||
ok,
|
||||
command,
|
||||
node: state.spec.nodeId,
|
||||
lane: state.spec.lane,
|
||||
sentinelId: state.sentinelId,
|
||||
mode: "pac-publish-only",
|
||||
mutation: true,
|
||||
specRef: SPEC_REF,
|
||||
source: state.sourceHead,
|
||||
image: state.image,
|
||||
pipelineRun: record(publish).jobName ?? sentinelPipelineRunName(state, options.rerun),
|
||||
controlPlane: {
|
||||
ok,
|
||||
mode: "pac-publish-only",
|
||||
mutation: true,
|
||||
source: state.sourceHead,
|
||||
publish,
|
||||
argo: {
|
||||
namespace: stringAt(state.cicd, "argo.namespace"),
|
||||
applicationName: stringAt(state.cicd, "argo.applicationName"),
|
||||
},
|
||||
valuesRedacted: true,
|
||||
},
|
||||
health: { ok: false, skipped: true, reason: "pac-publish-only-runtime-closeout-read-by-platform-infra-status", valuesRedacted: true },
|
||||
elapsedMs,
|
||||
timings: publishCurrentStageTimings({ publish }, null, elapsedMs),
|
||||
slowStages: publishCurrentSlowStages(state, publishCurrentStageTimings({ publish }, null, elapsedMs), budgetSeconds),
|
||||
withinBudget: elapsedMs <= budgetSeconds * 1000,
|
||||
warnings: [
|
||||
"PaC in-cluster publish-current submits the native sentinel publish PipelineRun and leaves Argo/runtime closeout to platform-infra pipelines-as-code status.",
|
||||
...sentinelCicdElapsedWarnings(record(publish).elapsedMs, "sentinel publish", controlPlaneWaitWarningSeconds(state)),
|
||||
...sentinelRemoteJobTimeoutWarnings(publish, "sentinel publish"),
|
||||
],
|
||||
blocker: ok ? null : { code: "sentinel-pac-publish-not-ready", reason: "sentinel publish PipelineRun did not complete successfully" },
|
||||
recoveryNext: controlPlaneRecoveryNext(state, ok, publish, null, false),
|
||||
next: publishCurrentNext(state),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
return rendered(ok, command, renderPublishCurrentResult(result));
|
||||
}
|
||||
if (state.configReady && state.sourceHead.ok && remainingBudgetSeconds() >= 5) {
|
||||
interruptContext.phase = "already-current-registry-probe";
|
||||
const registryProbe = probeImageRegistry(state, Math.max(1, Math.min(remainingBudgetSeconds(), 5)));
|
||||
@@ -377,6 +438,7 @@ function runSentinelPublishCurrentConfirmedInner(state: SentinelCicdState, optio
|
||||
wait: true,
|
||||
timeoutSeconds: Math.max(1, remainingBudgetSeconds()),
|
||||
rerun: options.rerun,
|
||||
sourceOverride: options.sourceOverride,
|
||||
});
|
||||
let health: Record<string, unknown>;
|
||||
let healthElapsedMs: number | null = null;
|
||||
@@ -632,7 +694,7 @@ function sourceHeadFromOverride(cicd: Record<string, unknown>, override: Sentine
|
||||
timedOut: false,
|
||||
stdoutBytes: 0,
|
||||
stderrBytes: 0,
|
||||
stdoutPreview: "source supplied by cicd branch-follower k8s git-mirror snapshot",
|
||||
stdoutPreview: `source supplied by ${override.sourceAuthority}`,
|
||||
stderrPreview: "",
|
||||
},
|
||||
};
|
||||
@@ -755,8 +817,8 @@ function validateSentinelSourceAuthority(cicd: Record<string, unknown>): void {
|
||||
const allowHostGit = booleanAt(cicd, "sourceAuthority.allowHostGit");
|
||||
const allowGithubDirectInPipeline = booleanAt(cicd, "sourceAuthority.allowGithubDirectInPipeline");
|
||||
const missingObjectPolicy = stringAt(cicd, "sourceSnapshot.missingObjectPolicy");
|
||||
if (mode !== "gitMirrorSnapshot") throw new Error("sourceAuthority.mode must be gitMirrorSnapshot");
|
||||
if (resolver !== "k8s-git-mirror") throw new Error("sourceAuthority.resolver must be k8s-git-mirror");
|
||||
const supported = (mode === "gitMirrorSnapshot" && resolver === "k8s-git-mirror") || (mode === "giteaSnapshot" && resolver === "gitea-mirror");
|
||||
if (!supported) throw new Error("sourceAuthority must be gitMirrorSnapshot/k8s-git-mirror or giteaSnapshot/gitea-mirror");
|
||||
if (allowHostGit !== false) throw new Error("sourceAuthority.allowHostGit must be false");
|
||||
if (allowGithubDirectInPipeline !== false) throw new Error("sourceAuthority.allowGithubDirectInPipeline must be false");
|
||||
if (missingObjectPolicy !== "fail-fast") throw new Error("sourceSnapshot.missingObjectPolicy must be fail-fast");
|
||||
@@ -1597,8 +1659,13 @@ function sentinelControlPlaneConfirmedResult(state: SentinelCicdState, options:
|
||||
const deadline = startedAt + waitBudgetSeconds * 1000;
|
||||
const remainingCicdWaitSeconds = () => strictRemainingSeconds(deadline, waitBudgetSeconds);
|
||||
const remainingCommandSeconds = () => Math.max(1, remainingCicdWaitSeconds());
|
||||
const sourceMirrorProbe = applyOnly ? null : probeSourceMirror(state, Math.min(remainingCommandSeconds(), 20));
|
||||
const sourceMirrorSync = applyOnly ? null : record(sourceMirrorProbe).ok === true ? sentinelSourceMirrorAlreadyPresentResult(state, sourceMirrorProbe) : runSentinelSourceMirrorSyncJob(state, remainingCommandSeconds());
|
||||
const giteaSource = state.sourceHead.sourceAuthority === "gitea-snapshot" || state.sourceHead.sourceAuthority === "gitea-cache";
|
||||
const sourceMirrorProbe = applyOnly || giteaSource ? null : probeSourceMirror(state, Math.min(remainingCommandSeconds(), 20));
|
||||
const sourceMirrorSync = applyOnly
|
||||
? null
|
||||
: giteaSource
|
||||
? sentinelSourceMirrorAlreadyPresentResult(state, { ok: true, mode: "gitea-controlled-mirror-snapshot", stageRef: state.sourceHead.stageRef, sourceCommit: state.sourceHead.commit, valuesRedacted: true })
|
||||
: record(sourceMirrorProbe).ok === true ? sentinelSourceMirrorAlreadyPresentResult(state, sourceMirrorProbe) : runSentinelSourceMirrorSyncJob(state, remainingCommandSeconds());
|
||||
const sourceMirrorReady = applyOnly || record(sourceMirrorSync).ok === true;
|
||||
const publish = applyOnly
|
||||
? null
|
||||
|
||||
@@ -23,7 +23,7 @@ import { parseNodeWebProbeObserveCollectView, type NodeWebProbeObserveCollectVie
|
||||
import { withWebObserveCommandRendered, withWebObserveStatusRendered } from "../hwlab-node-web-observe-render";
|
||||
import { buildWebObserveWrapperForObserveOptions, webObserveWrapperStateDirFromStatus } from "../hwlab-node-web-observe-wrapper";
|
||||
import { renderWebObserveWrapperContract } from "../hwlab-node-web-observe-wrapper-render";
|
||||
import { runWebProbeSentinelCommand, type WebProbeSentinelDashboardAction, type WebProbeSentinelOptions, type WebProbeSentinelReportView } from "../hwlab-node-web-sentinel-cicd";
|
||||
import { runWebProbeSentinelCommand, type WebProbeSentinelDashboardAction, type WebProbeSentinelOptions, type WebProbeSentinelReportView, type WebProbeSentinelSourceAuthority, type WebProbeSentinelSourceOverrideOptions } from "../hwlab-node-web-sentinel-cicd";
|
||||
import { hwlabNodeHelp, hwlabNodeObservabilityHelp, hwlabNodeWebProbeHelp } from "../hwlab-node-help";
|
||||
import { compactWebProbeResult, compactWebProbeScriptResult } from "../hwlab-node-web-probe-summary";
|
||||
import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRuleSummaries, nodeObservabilityWarningAlertExpression, nodeObservabilityWarningAlertSummaries } from "../hwlab-node-observability-promql";
|
||||
@@ -77,6 +77,10 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
"--timeout-ms",
|
||||
"--wait-timeout-ms",
|
||||
"--command-timeout-seconds",
|
||||
"--source-commit",
|
||||
"--source-stage-ref",
|
||||
"--source-mirror-commit",
|
||||
"--source-authority",
|
||||
]), new Set(["--dry-run", "--confirm", "--wait", "--rerun", "--quick-verify", "--raw", "--full", "--latest", "--full-page", "--no-full-page"]));
|
||||
const node = requiredOption(args, "--node");
|
||||
assertNodeId(node);
|
||||
@@ -89,21 +93,22 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
const dryRun = args.includes("--dry-run");
|
||||
if (confirm && dryRun) throw new Error("web-probe sentinel accepts only one of --confirm or --dry-run");
|
||||
const timeoutSeconds = positiveIntegerOption(args, "--timeout-seconds", 900, 3600);
|
||||
const sourceOverride = parseWebProbeSentinelSourceOverride(args);
|
||||
let sentinel: WebProbeSentinelOptions;
|
||||
if (sentinelActionRaw === "plan" || sentinelActionRaw === "status") {
|
||||
sentinel = { kind: "config", action: sentinelActionRaw, node, lane, sentinelId, dryRun };
|
||||
} else if (sentinelActionRaw === "image") {
|
||||
const imageAction = args[1];
|
||||
if (imageAction !== "status" && imageAction !== "build") throw new Error("web-probe sentinel image usage: image status|build --node NODE --lane vNN [--dry-run|--confirm]");
|
||||
sentinel = { kind: "image", action: imageAction, node, lane, sentinelId, dryRun: imageAction === "build" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds };
|
||||
sentinel = { kind: "image", action: imageAction, node, lane, sentinelId, dryRun: imageAction === "build" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds, sourceOverride };
|
||||
} else if (sentinelActionRaw === "control-plane") {
|
||||
const controlPlaneAction = args[1];
|
||||
if (controlPlaneAction !== "plan" && controlPlaneAction !== "apply" && controlPlaneAction !== "status" && controlPlaneAction !== "trigger-current") {
|
||||
throw new Error("web-probe sentinel control-plane usage: control-plane plan|apply|status|trigger-current --node NODE --lane vNN [--dry-run|--confirm]");
|
||||
}
|
||||
sentinel = { kind: "control-plane", action: controlPlaneAction, node, lane, sentinelId, dryRun: controlPlaneAction === "apply" || controlPlaneAction === "trigger-current" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds, rerun: args.includes("--rerun") };
|
||||
sentinel = { kind: "control-plane", action: controlPlaneAction, node, lane, sentinelId, dryRun: controlPlaneAction === "apply" || controlPlaneAction === "trigger-current" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds, rerun: args.includes("--rerun"), sourceOverride };
|
||||
} else if (sentinelActionRaw === "publish-current") {
|
||||
sentinel = { kind: "publish", action: "publish-current", node, lane, sentinelId, dryRun: dryRun || !confirm, confirm, wait: args.includes("--wait"), timeoutSeconds, rerun: args.includes("--rerun") };
|
||||
sentinel = { kind: "publish", action: "publish-current", node, lane, sentinelId, dryRun: dryRun || !confirm, confirm, wait: args.includes("--wait"), timeoutSeconds, rerun: args.includes("--rerun"), sourceOverride };
|
||||
} else if (sentinelActionRaw === "maintenance") {
|
||||
const maintenanceAction = args[1];
|
||||
if (maintenanceAction !== "status" && maintenanceAction !== "start" && maintenanceAction !== "stop") {
|
||||
@@ -180,6 +185,22 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
};
|
||||
}
|
||||
|
||||
function parseWebProbeSentinelSourceOverride(args: string[]): WebProbeSentinelSourceOverrideOptions {
|
||||
const sourceCommit = optionValue(args, "--source-commit") ?? null;
|
||||
const sourceStageRef = optionValue(args, "--source-stage-ref") ?? null;
|
||||
const sourceMirrorCommit = optionValue(args, "--source-mirror-commit") ?? null;
|
||||
const sourceAuthorityRaw = optionValue(args, "--source-authority") ?? null;
|
||||
if (sourceCommit !== null && !/^[0-9a-f]{40}$/iu.test(sourceCommit)) throw new Error(`--source-commit must be a full git sha, got ${sourceCommit}`);
|
||||
if (sourceMirrorCommit !== null && !/^[0-9a-f]{40}$/iu.test(sourceMirrorCommit)) throw new Error(`--source-mirror-commit must be a full git sha, got ${sourceMirrorCommit}`);
|
||||
if (sourceStageRef !== null && !sourceStageRef.startsWith("refs/")) throw new Error(`--source-stage-ref must be a git ref, got ${sourceStageRef}`);
|
||||
let sourceAuthority: WebProbeSentinelSourceAuthority | null = null;
|
||||
if (sourceAuthorityRaw !== null) {
|
||||
if (sourceAuthorityRaw !== "git-mirror-snapshot" && sourceAuthorityRaw !== "gitea-snapshot") throw new Error("--source-authority must be git-mirror-snapshot or gitea-snapshot");
|
||||
sourceAuthority = sourceAuthorityRaw;
|
||||
}
|
||||
return { sourceCommit, sourceStageRef, sourceMirrorCommit, sourceAuthority };
|
||||
}
|
||||
|
||||
function parseWebProbeSentinelDashboardAction(value: string | undefined): WebProbeSentinelDashboardAction {
|
||||
if (value === "verify" || value === "screenshot" || value === "trigger") return value;
|
||||
throw new Error("web-probe sentinel dashboard usage: dashboard verify|screenshot|trigger --node NODE --lane vNN --sentinel <id>");
|
||||
|
||||
@@ -109,8 +109,8 @@ metadata:
|
||||
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: unidesk
|
||||
app.kubernetes.io/part-of: agentrun
|
||||
unidesk.ai/spec: GH-1552
|
||||
app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
|
||||
unidesk.ai/spec: $UNIDESK_PAC_SPEC
|
||||
spec:
|
||||
url: $UNIDESK_PAC_REPOSITORY_URL
|
||||
git_provider:
|
||||
@@ -128,6 +128,45 @@ $(printf '%s' "$UNIDESK_PAC_PARAMS_JSON" | node -e 'const fs=require("fs"); cons
|
||||
EOF
|
||||
}
|
||||
|
||||
consumer_rbac_manifest() {
|
||||
cat <<EOF
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: unidesk-pac-consumer-runner
|
||||
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: unidesk
|
||||
app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
|
||||
unidesk.ai/spec: $UNIDESK_PAC_SPEC
|
||||
rules:
|
||||
- apiGroups: ["tekton.dev"]
|
||||
resources: ["pipelineruns", "taskruns"]
|
||||
verbs: ["get", "list", "watch", "create", "patch", "update"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods", "pods/log"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: unidesk-pac-consumer-runner-default
|
||||
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: unidesk
|
||||
app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
|
||||
unidesk.ai/spec: $UNIDESK_PAC_SPEC
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: default
|
||||
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: unidesk-pac-consumer-runner
|
||||
EOF
|
||||
}
|
||||
|
||||
apply_release() {
|
||||
tmp=$(mktemp)
|
||||
printf '%s' "$UNIDESK_PAC_RELEASE_MANIFEST_B64" | base64 -d > "$tmp"
|
||||
@@ -156,6 +195,7 @@ apply_action() {
|
||||
fi
|
||||
apply_release
|
||||
kubectl create ns "$UNIDESK_PAC_TARGET_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||
consumer_rbac_manifest | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f - >/dev/null
|
||||
token=$(ensure_token)
|
||||
test -n "$token"
|
||||
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" create secret generic "$UNIDESK_PAC_SECRET_NAME" \
|
||||
@@ -205,7 +245,7 @@ const rows = (data.items || [])
|
||||
startTime: item.status?.startTime || null,
|
||||
completionTime: item.status?.completionTime || null,
|
||||
durationSeconds: durationSeconds(item),
|
||||
sourceCommit: item.metadata.labels?.['pipelinesascode.tekton.dev/sha'] || item.metadata.labels?.['agentrun.pikastech.local/source-commit'] || null,
|
||||
sourceCommit: item.metadata.labels?.['pipelinesascode.tekton.dev/sha'] || item.metadata.labels?.['agentrun.pikastech.local/source-commit'] || item.metadata.labels?.['unidesk.ai/source-commit'] || item.metadata.labels?.['hwlab.pikastech.local/source-commit'] || null,
|
||||
};
|
||||
});
|
||||
process.stdout.write(JSON.stringify(rows));
|
||||
@@ -262,10 +302,21 @@ for (const line of lines) {
|
||||
}
|
||||
const publish = [...records].reverse().find((item) => item.phase === 'gitops-publish' || item.gitopsCommit);
|
||||
const image = publish || [...records].reverse().find((item) => item.imageStatus || item.status === 'reused' || item.status === 'built');
|
||||
function digestOf(item) {
|
||||
if (item.digest) return item.digest;
|
||||
const ref = item.digestRef || '';
|
||||
const index = String(ref).indexOf('@');
|
||||
return index >= 0 ? String(ref).slice(index + 1) : null;
|
||||
}
|
||||
function envReuseOf(item) {
|
||||
const env = item.envReuse || {};
|
||||
return env.dependencyReuse || env.mode || item.envReuseStatus || null;
|
||||
}
|
||||
process.stdout.write(JSON.stringify(image ? {
|
||||
imageStatus: image.imageStatus || image.status || null,
|
||||
imageStatus: image.imageStatus || image.status || (image.digestRef ? 'built' : null),
|
||||
envIdentity: image.envIdentity || null,
|
||||
digest: image.digest || null,
|
||||
envReuse: envReuseOf(image),
|
||||
digest: digestOf(image),
|
||||
gitopsCommit: image.gitopsCommit || null,
|
||||
sourceCommit: image.sourceCommit || null,
|
||||
valuesPrinted: false,
|
||||
|
||||
@@ -39,6 +39,7 @@ interface PacConfig {
|
||||
};
|
||||
defaults: {
|
||||
targetId: string;
|
||||
consumerId: string;
|
||||
};
|
||||
release: {
|
||||
version: string;
|
||||
@@ -67,7 +68,12 @@ interface PacConfig {
|
||||
branch: string;
|
||||
};
|
||||
};
|
||||
repository: {
|
||||
repositories: PacRepository[];
|
||||
consumers: PacConsumer[];
|
||||
}
|
||||
|
||||
interface PacRepository {
|
||||
id: string;
|
||||
name: string;
|
||||
namespace: string;
|
||||
providerType: "gitea";
|
||||
@@ -80,8 +86,10 @@ interface PacConfig {
|
||||
webhookSecretKey: string;
|
||||
concurrencyLimit: number;
|
||||
params: Record<string, string>;
|
||||
};
|
||||
consumer: {
|
||||
}
|
||||
|
||||
interface PacConsumer {
|
||||
id: string;
|
||||
node: string;
|
||||
lane: string;
|
||||
namespace: string;
|
||||
@@ -89,11 +97,12 @@ interface PacConfig {
|
||||
pipelineRunPrefix: string;
|
||||
argoNamespace: string;
|
||||
argoApplication: string;
|
||||
};
|
||||
repositoryRef: string;
|
||||
}
|
||||
|
||||
interface CommonOptions {
|
||||
targetId: string | null;
|
||||
consumerId: string | null;
|
||||
full: boolean;
|
||||
raw: boolean;
|
||||
}
|
||||
@@ -164,8 +173,13 @@ function readPacConfig(): PacConfig {
|
||||
const gitea = y.objectField(root, "gitea", "");
|
||||
const admin = y.objectField(gitea, "admin", "gitea");
|
||||
const webhook = y.objectField(gitea, "webhook", "gitea");
|
||||
const repository = y.objectField(root, "repository", "");
|
||||
const consumer = y.objectField(root, "consumer", "");
|
||||
const repositories = root.repositories === undefined
|
||||
? [parseRepository(y.objectField(root, "repository", ""), "repository")]
|
||||
: y.arrayOfRecords(root.repositories, "repositories").map((item, index) => parseRepository(item, `repositories[${index}]`));
|
||||
const consumers = root.consumers === undefined
|
||||
? [parseConsumer(y.objectField(root, "consumer", ""), "consumer", repositories[0]?.id ?? "default")]
|
||||
: y.arrayOfRecords(root.consumers, "consumers").map((item, index) => parseConsumer(item, `consumers[${index}]`, repositories[0]?.id ?? "default"));
|
||||
const defaults = y.objectField(root, "defaults", "");
|
||||
const parsed: PacConfig = {
|
||||
version: y.integerField(root, "version", ""),
|
||||
kind: "platform-infra-pipelines-as-code",
|
||||
@@ -176,7 +190,8 @@ function readPacConfig(): PacConfig {
|
||||
relatedIssues: y.numberArrayField(y.objectField(root, "metadata", ""), "relatedIssues", "metadata"),
|
||||
},
|
||||
defaults: {
|
||||
targetId: y.stringField(y.objectField(root, "defaults", ""), "targetId", "defaults"),
|
||||
targetId: y.stringField(defaults, "targetId", "defaults"),
|
||||
consumerId: typeof defaults.consumerId === "string" && defaults.consumerId.length > 0 ? defaults.consumerId : consumers[0]?.id ?? "default",
|
||||
},
|
||||
release: {
|
||||
version: y.stringField(release, "version", "release"),
|
||||
@@ -205,34 +220,47 @@ function readPacConfig(): PacConfig {
|
||||
branch: y.stringField(webhook, "branch", "gitea.webhook"),
|
||||
},
|
||||
},
|
||||
repository: {
|
||||
name: y.kubernetesNameField(repository, "name", "repository"),
|
||||
namespace: y.kubernetesNameField(repository, "namespace", "repository"),
|
||||
providerType: y.enumField(repository, "providerType", "repository", ["gitea"] as const),
|
||||
url: urlField(repository, "url", "repository"),
|
||||
cloneUrl: urlField(repository, "cloneUrl", "repository"),
|
||||
owner: y.stringField(repository, "owner", "repository"),
|
||||
repo: y.stringField(repository, "repo", "repository"),
|
||||
secretName: y.kubernetesNameField(repository, "secretName", "repository"),
|
||||
tokenKey: y.stringField(repository, "tokenKey", "repository"),
|
||||
webhookSecretKey: y.stringField(repository, "webhookSecretKey", "repository"),
|
||||
concurrencyLimit: positiveInteger(repository, "concurrencyLimit", "repository"),
|
||||
params: stringRecord(y.objectField(repository, "params", "repository"), "repository.params"),
|
||||
},
|
||||
consumer: {
|
||||
node: y.stringField(consumer, "node", "consumer"),
|
||||
lane: y.stringField(consumer, "lane", "consumer"),
|
||||
namespace: y.kubernetesNameField(consumer, "namespace", "consumer"),
|
||||
pipeline: y.kubernetesNameField(consumer, "pipeline", "consumer"),
|
||||
pipelineRunPrefix: y.stringField(consumer, "pipelineRunPrefix", "consumer"),
|
||||
argoNamespace: y.kubernetesNameField(consumer, "argoNamespace", "consumer"),
|
||||
argoApplication: y.kubernetesNameField(consumer, "argoApplication", "consumer"),
|
||||
},
|
||||
repositories,
|
||||
consumers,
|
||||
};
|
||||
validateConfig(parsed);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseRepository(repository: Record<string, unknown>, path: string): PacRepository {
|
||||
const name = y.kubernetesNameField(repository, "name", path);
|
||||
return {
|
||||
id: typeof repository.id === "string" && repository.id.length > 0 ? repository.id : name,
|
||||
name,
|
||||
namespace: y.kubernetesNameField(repository, "namespace", path),
|
||||
providerType: y.enumField(repository, "providerType", path, ["gitea"] as const),
|
||||
url: urlField(repository, "url", path),
|
||||
cloneUrl: urlField(repository, "cloneUrl", path),
|
||||
owner: y.stringField(repository, "owner", path),
|
||||
repo: y.stringField(repository, "repo", path),
|
||||
secretName: y.kubernetesNameField(repository, "secretName", path),
|
||||
tokenKey: y.stringField(repository, "tokenKey", path),
|
||||
webhookSecretKey: y.stringField(repository, "webhookSecretKey", path),
|
||||
concurrencyLimit: positiveInteger(repository, "concurrencyLimit", path),
|
||||
params: stringRecord(y.objectField(repository, "params", path), `${path}.params`),
|
||||
};
|
||||
}
|
||||
|
||||
function parseConsumer(consumer: Record<string, unknown>, path: string, defaultRepositoryRef: string): PacConsumer {
|
||||
const id = typeof consumer.id === "string" && consumer.id.length > 0 ? consumer.id : `${y.stringField(consumer, "node", path)}-${y.stringField(consumer, "lane", path)}`;
|
||||
return {
|
||||
id,
|
||||
node: y.stringField(consumer, "node", path),
|
||||
lane: y.stringField(consumer, "lane", path),
|
||||
namespace: y.kubernetesNameField(consumer, "namespace", path),
|
||||
pipeline: y.kubernetesNameField(consumer, "pipeline", path),
|
||||
pipelineRunPrefix: y.stringField(consumer, "pipelineRunPrefix", path),
|
||||
argoNamespace: y.kubernetesNameField(consumer, "argoNamespace", path),
|
||||
argoApplication: y.kubernetesNameField(consumer, "argoApplication", path),
|
||||
repositoryRef: typeof consumer.repositoryRef === "string" && consumer.repositoryRef.length > 0 ? consumer.repositoryRef : defaultRepositoryRef,
|
||||
};
|
||||
}
|
||||
|
||||
function parseTarget(record: Record<string, unknown>, index: number): PacTarget {
|
||||
const path = `targets[${index}]`;
|
||||
return {
|
||||
@@ -247,10 +275,22 @@ function parseTarget(record: Record<string, unknown>, index: number): PacTarget
|
||||
function validateConfig(config: PacConfig): void {
|
||||
if (config.version !== 1) throw new Error(`${configLabel}.version must be 1`);
|
||||
resolveTarget(config, config.defaults.targetId);
|
||||
if (config.repository.providerType !== "gitea") throw new Error(`${configLabel}.repository.providerType must be gitea`);
|
||||
if (config.release.waitTimeoutSeconds > 55) throw new Error(`${configLabel}.release.waitTimeoutSeconds must fit the 60s trans budget`);
|
||||
if (config.repository.namespace !== config.consumer.namespace) throw new Error(`${configLabel}.repository.namespace must match consumer.namespace`);
|
||||
if (new URL(config.repository.cloneUrl).origin !== new URL(config.gitea.internalBaseUrl).origin) throw new Error(`${configLabel}.repository.cloneUrl must use the configured internal Gitea base URL`);
|
||||
resolveConsumer(config, config.defaults.consumerId);
|
||||
const ids = new Set<string>();
|
||||
for (const repository of config.repositories) {
|
||||
if (ids.has(repository.id)) throw new Error(`${configLabel}.repositories id must be unique: ${repository.id}`);
|
||||
ids.add(repository.id);
|
||||
if (repository.providerType !== "gitea") throw new Error(`${configLabel}.repositories.${repository.id}.providerType must be gitea`);
|
||||
if (new URL(repository.cloneUrl).origin !== new URL(config.gitea.internalBaseUrl).origin) throw new Error(`${configLabel}.repositories.${repository.id}.cloneUrl must use the configured internal Gitea base URL`);
|
||||
}
|
||||
const consumerIds = new Set<string>();
|
||||
for (const consumer of config.consumers) {
|
||||
if (consumerIds.has(consumer.id)) throw new Error(`${configLabel}.consumers id must be unique: ${consumer.id}`);
|
||||
consumerIds.add(consumer.id);
|
||||
const repository = resolveRepository(config, consumer.repositoryRef);
|
||||
if (repository.namespace !== consumer.namespace) throw new Error(`${configLabel}.consumers.${consumer.id}.namespace must match repository namespace`);
|
||||
}
|
||||
if (!config.gitea.webhook.events.includes("push")) throw new Error(`${configLabel}.gitea.webhook.events must include push`);
|
||||
}
|
||||
|
||||
@@ -262,30 +302,48 @@ function resolveTarget(config: PacConfig, targetId: string | null): PacTarget {
|
||||
return target;
|
||||
}
|
||||
|
||||
function resolveConsumer(config: PacConfig, consumerId: string | null): PacConsumer {
|
||||
const id = consumerId ?? config.defaults.consumerId;
|
||||
const consumer = config.consumers.find((item) => item.id.toLowerCase() === id.toLowerCase());
|
||||
if (consumer === undefined) throw new Error(`unknown Pipelines-as-Code consumer ${id}; known consumers: ${config.consumers.map((item) => item.id).join(", ")}`);
|
||||
return consumer;
|
||||
}
|
||||
|
||||
function resolveRepository(config: PacConfig, repositoryRef: string): PacRepository {
|
||||
const repository = config.repositories.find((item) => item.id === repositoryRef || item.name === repositoryRef);
|
||||
if (repository === undefined) throw new Error(`unknown Pipelines-as-Code repository ${repositoryRef}; known repositories: ${config.repositories.map((item) => item.id).join(", ")}`);
|
||||
return repository;
|
||||
}
|
||||
|
||||
function plan(options: CommonOptions): Record<string, unknown> {
|
||||
const pac = readPacConfig();
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
const secrets = secretSummaries(pac);
|
||||
return {
|
||||
ok: true,
|
||||
action: "platform-infra-pipelines-as-code-plan",
|
||||
mutation: false,
|
||||
target: targetSummary(target),
|
||||
config: configSummary(pac),
|
||||
config: configSummary(pac, consumer, repository),
|
||||
release: pac.release,
|
||||
repository: repositorySummary(pac),
|
||||
repository: repositorySummary(repository),
|
||||
consumer,
|
||||
secrets,
|
||||
policy: policyChecks(pac),
|
||||
next: nextCommands(target.id),
|
||||
policy: policyChecks(repository),
|
||||
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
||||
};
|
||||
}
|
||||
|
||||
async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
const secrets = ensureSecrets(pac, !options.dryRun, options.dryRun);
|
||||
const releaseManifest = options.dryRun ? "" : await fetchReleaseManifest(pac);
|
||||
const result = await capture(config, target.route, ["sh"], remoteScript("apply", pac, target, options, secrets, releaseManifest));
|
||||
const result = await capture(config, target.route, ["sh"], remoteScript("apply", pac, target, repository, consumer, options, secrets, releaseManifest));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && parsed?.ok === true,
|
||||
@@ -293,20 +351,23 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
mutation: !options.dryRun,
|
||||
mode: options.dryRun ? "dry-run" : "confirmed",
|
||||
target: targetSummary(target),
|
||||
config: compactConfigSummary(pac),
|
||||
config: compactConfigSummary(pac, consumer, repository),
|
||||
release: { version: pac.release.version, manifestUrl: pac.release.manifestUrl },
|
||||
repository: repositorySummary(pac),
|
||||
repository: repositorySummary(repository),
|
||||
consumer,
|
||||
secrets: secretSummaries(pac),
|
||||
remote: parsed ?? compactCapture(result, { full: true }),
|
||||
next: nextCommands(target.id),
|
||||
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
||||
};
|
||||
}
|
||||
|
||||
async function status(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
const secrets = ensureSecrets(pac, false);
|
||||
const result = await capture(config, target.route, ["sh"], remoteScript("status", pac, target, { ...options, confirm: false, dryRun: true, wait: false }, secrets, ""));
|
||||
const result = await capture(config, target.route, ["sh"], remoteScript("status", pac, target, repository, consumer, { ...options, confirm: false, dryRun: true, wait: false }, secrets, ""));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const summary = parsed === null ? null : statusSummary(parsed);
|
||||
return {
|
||||
@@ -314,27 +375,31 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
|
||||
action: "platform-infra-pipelines-as-code-status",
|
||||
mutation: false,
|
||||
target: targetSummary(target),
|
||||
config: compactConfigSummary(pac),
|
||||
config: compactConfigSummary(pac, consumer, repository),
|
||||
consumer,
|
||||
summary,
|
||||
remote: parsed === null ? compactCapture(result, { full: true }) : options.raw || options.full ? parsed : summary,
|
||||
next: nextCommands(target.id),
|
||||
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
||||
};
|
||||
}
|
||||
|
||||
async function webhookTest(config: UniDeskConfig, options: WebhookTestOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
if (!options.confirm) return { ok: false, action: "platform-infra-pipelines-as-code-webhook-test", mutation: false, mode: "missing-confirm", error: "webhook-test requires --confirm" };
|
||||
const secrets = ensureSecrets(pac, false);
|
||||
const result = await capture(config, target.route, ["sh"], remoteScript("webhook-test", pac, target, { ...options, dryRun: false, wait: false }, secrets, ""));
|
||||
const result = await capture(config, target.route, ["sh"], remoteScript("webhook-test", pac, target, repository, consumer, { ...options, dryRun: false, wait: false }, secrets, ""));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && parsed?.ok === true,
|
||||
action: "platform-infra-pipelines-as-code-webhook-test",
|
||||
mutation: true,
|
||||
target: targetSummary(target),
|
||||
consumer,
|
||||
remote: parsed ?? compactCapture(result, { full: true }),
|
||||
next: nextCommands(target.id),
|
||||
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -346,13 +411,14 @@ async function fetchReleaseManifest(pac: PacConfig): Promise<string> {
|
||||
return text;
|
||||
}
|
||||
|
||||
function remoteScript(action: "apply" | "status" | "webhook-test", pac: PacConfig, target: PacTarget, options: ApplyOptions | WebhookTestOptions, secrets: SecretMaterial, releaseManifest: string): string {
|
||||
function remoteScript(action: "apply" | "status" | "webhook-test", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | WebhookTestOptions, secrets: SecretMaterial, releaseManifest: string): string {
|
||||
const webhookUrl = `${pac.gitea.internalBaseUrl.replace(/\/+$/u, "").replace(/gitea-http\.[^.]+\.svc\.cluster\.local:3000/u, `${pac.release.controllerServiceName}.${pac.release.namespace}.svc.cluster.local:${pac.release.controllerServicePort}`)}`;
|
||||
const env: Record<string, string> = {
|
||||
UNIDESK_PAC_ACTION: action,
|
||||
UNIDESK_PAC_FIELD_MANAGER: fieldManager,
|
||||
UNIDESK_PAC_TARGET_ID: target.id,
|
||||
UNIDESK_PAC_TARGET_NAMESPACE: target.namespace,
|
||||
UNIDESK_PAC_TARGET_NAMESPACE: consumer.namespace,
|
||||
UNIDESK_PAC_CONSUMER_ID: consumer.id,
|
||||
UNIDESK_PAC_RELEASE_NAMESPACE: pac.release.namespace,
|
||||
UNIDESK_PAC_RELEASE_MANIFEST_B64: Buffer.from(releaseManifest, "utf8").toString("base64"),
|
||||
UNIDESK_PAC_CONTROLLER_SERVICE_NAME: pac.release.controllerServiceName,
|
||||
@@ -362,20 +428,22 @@ function remoteScript(action: "apply" | "status" | "webhook-test", pac: PacConfi
|
||||
UNIDESK_PAC_GITEA_ADMIN_USERNAME: secrets.adminUsername,
|
||||
UNIDESK_PAC_GITEA_ADMIN_PASSWORD: secrets.adminPassword,
|
||||
UNIDESK_PAC_GITEA_API_USERNAME: pac.gitea.admin.apiUsername,
|
||||
UNIDESK_PAC_GITEA_OWNER: pac.repository.owner,
|
||||
UNIDESK_PAC_GITEA_REPO: pac.repository.repo,
|
||||
UNIDESK_PAC_GITEA_OWNER: repository.owner,
|
||||
UNIDESK_PAC_GITEA_REPO: repository.repo,
|
||||
UNIDESK_PAC_WEBHOOK_URL: webhookUrl,
|
||||
UNIDESK_PAC_WEBHOOK_SECRET: secrets.webhookSecret,
|
||||
UNIDESK_PAC_REPOSITORY_NAME: pac.repository.name,
|
||||
UNIDESK_PAC_REPOSITORY_URL: pac.repository.url,
|
||||
UNIDESK_PAC_SECRET_NAME: pac.repository.secretName,
|
||||
UNIDESK_PAC_TOKEN_KEY: pac.repository.tokenKey,
|
||||
UNIDESK_PAC_WEBHOOK_SECRET_KEY: pac.repository.webhookSecretKey,
|
||||
UNIDESK_PAC_CONCURRENCY_LIMIT: String(pac.repository.concurrencyLimit),
|
||||
UNIDESK_PAC_PARAMS_JSON: JSON.stringify(pac.repository.params),
|
||||
UNIDESK_PAC_PIPELINE_RUN_PREFIX: pac.consumer.pipelineRunPrefix,
|
||||
UNIDESK_PAC_ARGO_NAMESPACE: pac.consumer.argoNamespace,
|
||||
UNIDESK_PAC_ARGO_APPLICATION: pac.consumer.argoApplication,
|
||||
UNIDESK_PAC_REPOSITORY_NAME: repository.name,
|
||||
UNIDESK_PAC_REPOSITORY_URL: repository.url,
|
||||
UNIDESK_PAC_SECRET_NAME: repository.secretName,
|
||||
UNIDESK_PAC_TOKEN_KEY: repository.tokenKey,
|
||||
UNIDESK_PAC_WEBHOOK_SECRET_KEY: repository.webhookSecretKey,
|
||||
UNIDESK_PAC_CONCURRENCY_LIMIT: String(repository.concurrencyLimit),
|
||||
UNIDESK_PAC_PARAMS_JSON: JSON.stringify(repository.params),
|
||||
UNIDESK_PAC_PIPELINE_RUN_PREFIX: consumer.pipelineRunPrefix,
|
||||
UNIDESK_PAC_ARGO_NAMESPACE: consumer.argoNamespace,
|
||||
UNIDESK_PAC_ARGO_APPLICATION: consumer.argoApplication,
|
||||
UNIDESK_PAC_PART_OF: consumer.id.startsWith("sentinel") ? "hwlab-web-probe-sentinel" : "agentrun",
|
||||
UNIDESK_PAC_SPEC: pac.metadata.relatedIssues.includes(1555) ? "GH-1552/GH-1555" : pac.metadata.spec,
|
||||
};
|
||||
const exports = Object.entries(env).map(([key, value]) => `export ${key}=${shQuote(value)}`).join("\n");
|
||||
return `${exports}\n${readFileSync(remoteScriptFile, "utf8")}`;
|
||||
@@ -470,11 +538,11 @@ function statusSummary(payload: Record<string, unknown>): Record<string, unknown
|
||||
};
|
||||
}
|
||||
|
||||
function policyChecks(pac: PacConfig): Array<Record<string, unknown>> {
|
||||
function policyChecks(repository: PacRepository): Array<Record<string, unknown>> {
|
||||
return [
|
||||
{ name: "single-path", ok: true, detail: "Gitea webhook -> Pipelines-as-Code -> Tekton -> Argo/k8s runtime; no Gitea Actions/act_runner/branch-follower fallback." },
|
||||
{ name: "yaml-source-of-truth", ok: true, detail: `${configLabel} owns PaC release, Repository CR, Gitea webhook and AgentRun JD01 v0.2 params.` },
|
||||
{ name: "gitea-internal-source", ok: pac.repository.cloneUrl.includes(".svc.cluster.local"), detail: "Tekton source clone uses the internal k3s Gitea service URL." },
|
||||
{ name: "yaml-source-of-truth", ok: true, detail: `${configLabel} owns PaC release, Repository CR, Gitea webhook and consumer params.` },
|
||||
{ name: "gitea-internal-source", ok: repository.cloneUrl.includes(".svc.cluster.local"), detail: "Tekton source clone uses the internal k3s Gitea service URL." },
|
||||
{ name: "runtime-zero-docker", ok: true, detail: "Runtime starts at k8s pulling already-built images; CI build may use Tekton/BuildKit." },
|
||||
];
|
||||
}
|
||||
@@ -483,51 +551,60 @@ function targetSummary(target: PacTarget): Record<string, unknown> {
|
||||
return { id: target.id, route: target.route, namespace: target.namespace, role: target.role };
|
||||
}
|
||||
|
||||
function configSummary(pac: PacConfig): Record<string, unknown> {
|
||||
function configSummary(pac: PacConfig, consumer: PacConsumer, repository: PacRepository): Record<string, unknown> {
|
||||
return {
|
||||
path: configLabel,
|
||||
metadata: pac.metadata,
|
||||
release: pac.release,
|
||||
gitea: { configRef: pac.gitea.configRef, internalBaseUrl: pac.gitea.internalBaseUrl, webhookBranch: pac.gitea.webhook.branch },
|
||||
repository: repositorySummary(pac),
|
||||
consumer: pac.consumer,
|
||||
repositories: pac.repositories.map(repositorySummary),
|
||||
consumers: pac.consumers,
|
||||
repository: repositorySummary(repository),
|
||||
consumer,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactConfigSummary(pac: PacConfig): Record<string, unknown> {
|
||||
function compactConfigSummary(pac: PacConfig, consumer: PacConsumer, repository: PacRepository): Record<string, unknown> {
|
||||
return {
|
||||
path: configLabel,
|
||||
releaseVersion: pac.release.version,
|
||||
repository: pac.repository.name,
|
||||
providerType: pac.repository.providerType,
|
||||
sourceUrl: pac.repository.cloneUrl,
|
||||
consumer: `${pac.consumer.node}/${pac.consumer.lane}`,
|
||||
pipeline: pac.consumer.pipeline,
|
||||
repository: repository.name,
|
||||
providerType: repository.providerType,
|
||||
sourceUrl: repository.cloneUrl,
|
||||
consumer: `${consumer.node}/${consumer.lane}`,
|
||||
consumerId: consumer.id,
|
||||
pipeline: consumer.pipeline,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function repositorySummary(pac: PacConfig): Record<string, unknown> {
|
||||
function repositorySummary(repository: PacRepository): Record<string, unknown> {
|
||||
return {
|
||||
name: pac.repository.name,
|
||||
namespace: pac.repository.namespace,
|
||||
providerType: pac.repository.providerType,
|
||||
url: pac.repository.url,
|
||||
cloneUrl: pac.repository.cloneUrl,
|
||||
owner: pac.repository.owner,
|
||||
repo: pac.repository.repo,
|
||||
secretName: pac.repository.secretName,
|
||||
concurrencyLimit: pac.repository.concurrencyLimit,
|
||||
params: Object.keys(pac.repository.params).sort(),
|
||||
id: repository.id,
|
||||
name: repository.name,
|
||||
namespace: repository.namespace,
|
||||
providerType: repository.providerType,
|
||||
url: repository.url,
|
||||
cloneUrl: repository.cloneUrl,
|
||||
owner: repository.owner,
|
||||
repo: repository.repo,
|
||||
secretName: repository.secretName,
|
||||
concurrencyLimit: repository.concurrencyLimit,
|
||||
params: Object.keys(repository.params).sort(),
|
||||
};
|
||||
}
|
||||
|
||||
function nextCommands(targetId: string): Record<string, string> {
|
||||
function consumerSuffix(consumerId: string, defaultConsumerId: string): string {
|
||||
return consumerId === defaultConsumerId ? "" : ` --consumer ${consumerId}`;
|
||||
}
|
||||
|
||||
function nextCommands(targetId: string, consumerId: string, defaultConsumerId: string): Record<string, string> {
|
||||
const suffix = consumerSuffix(consumerId, defaultConsumerId);
|
||||
return {
|
||||
apply: `bun scripts/cli.ts platform-infra pipelines-as-code apply --target ${targetId} --confirm`,
|
||||
status: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId}`,
|
||||
webhookTest: `bun scripts/cli.ts platform-infra pipelines-as-code webhook-test --target ${targetId} --confirm`,
|
||||
apply: `bun scripts/cli.ts platform-infra pipelines-as-code apply --target ${targetId}${suffix} --confirm`,
|
||||
status: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId}${suffix}`,
|
||||
webhookTest: `bun scripts/cli.ts platform-infra pipelines-as-code webhook-test --target ${targetId}${suffix} --confirm`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -535,18 +612,19 @@ function renderPlan(result: Record<string, unknown>): RenderedCliResult {
|
||||
const target = record(result.target);
|
||||
const config = record(result.config);
|
||||
const repository = record(result.repository);
|
||||
const consumer = record(result.consumer);
|
||||
const secrets = arrayRecords(result.secrets);
|
||||
const policy = arrayRecords(result.policy);
|
||||
const lines = [
|
||||
"PLATFORM-INFRA PIPELINES-AS-CODE PLAN",
|
||||
...table(["TARGET", "NAMESPACE", "RELEASE", "REPOSITORY"], [[stringValue(target.id), stringValue(target.namespace), stringValue(record(config.release).version), stringValue(repository.name)]]),
|
||||
...table(["TARGET", "NAMESPACE", "RELEASE", "REPOSITORY", "CONSUMER"], [[stringValue(target.id), stringValue(repository.namespace), stringValue(record(config.release).version), stringValue(repository.name), stringValue(consumer.id)]]),
|
||||
"",
|
||||
"PATH",
|
||||
...table(["STAGE", "AUTHORITY", "DETAIL"], [
|
||||
["source", "Gitea", stringValue(repository.cloneUrl)],
|
||||
["trigger", "Pipelines-as-Code", "Gitea push webhook"],
|
||||
["ci", "Tekton", stringValue(record(config.consumer).pipeline)],
|
||||
["cd", "Argo", stringValue(record(config.consumer).argoApplication)],
|
||||
["ci", "Tekton", stringValue(consumer.pipeline)],
|
||||
["cd", "Argo", stringValue(consumer.argoApplication)],
|
||||
]),
|
||||
"",
|
||||
"SECRETS",
|
||||
@@ -580,13 +658,14 @@ function renderApply(result: Record<string, unknown>): RenderedCliResult {
|
||||
|
||||
function renderStatus(result: Record<string, unknown>): RenderedCliResult {
|
||||
const summary = record(result.summary);
|
||||
const consumer = record(result.consumer);
|
||||
const latest = record(summary.latestPipelineRun);
|
||||
const taskRuns = arrayRecords(summary.taskRuns);
|
||||
const artifact = record(summary.artifact);
|
||||
const argo = record(summary.argo);
|
||||
const lines = [
|
||||
"PLATFORM-INFRA PIPELINES-AS-CODE STATUS",
|
||||
...table(["READY", "CRD", "CONTROLLER", "WEBHOOKS", "REPOSITORY"], [[boolText(summary.ready), boolText(summary.crdPresent), stringValue(summary.controllerReady), stringValue(summary.webhookCount), compactLine(stringValue(summary.repositoryCondition))]]),
|
||||
...table(["CONSUMER", "READY", "CRD", "CONTROLLER", "WEBHOOKS", "REPOSITORY"], [[stringValue(consumer.id), boolText(summary.ready), boolText(summary.crdPresent), stringValue(summary.controllerReady), stringValue(summary.webhookCount), compactLine(stringValue(summary.repositoryCondition))]]),
|
||||
"",
|
||||
"LATEST PIPELINERUN",
|
||||
...table(["NAME", "STATUS", "REASON", "DURATION_S", "SOURCE"], [[stringValue(latest.name), stringValue(latest.status), stringValue(latest.reason), stringValue(latest.durationSeconds), short(stringValue(latest.sourceCommit))]]),
|
||||
@@ -595,13 +674,13 @@ function renderStatus(result: Record<string, unknown>): RenderedCliResult {
|
||||
...(taskRuns.length === 0 ? ["-"] : table(["TASKRUN", "STATUS", "REASON", "DURATION_S"], taskRuns.map((item) => [short(stringValue(item.name), 56), stringValue(item.status), stringValue(item.reason), stringValue(item.durationSeconds)]))),
|
||||
"",
|
||||
"IMAGE / GITOPS",
|
||||
...table(["IMAGE_STATUS", "ENV_ID", "DIGEST", "GITOPS"], [[stringValue(artifact.imageStatus), stringValue(artifact.envIdentity), short(stringValue(artifact.digest), 18), short(stringValue(artifact.gitopsCommit))]]),
|
||||
...table(["IMAGE_STATUS", "ENV_REUSE", "ENV_ID", "DIGEST", "GITOPS"], [[stringValue(artifact.imageStatus), stringValue(artifact.envReuse), stringValue(artifact.envIdentity), short(stringValue(artifact.digest), 18), short(stringValue(artifact.gitopsCommit))]]),
|
||||
"",
|
||||
"ARGO",
|
||||
...table(["SYNC", "HEALTH", "REVISION"], [[stringValue(argo.sync), stringValue(argo.health), short(stringValue(argo.revision))]]),
|
||||
"",
|
||||
"NEXT",
|
||||
` full: bun scripts/cli.ts platform-infra pipelines-as-code status --target ${stringValue(record(result.target).id)} --full`,
|
||||
` full: ${stringValue(record(result.next).status)} --full`,
|
||||
];
|
||||
return rendered(result, "platform-infra pipelines-as-code status", lines);
|
||||
}
|
||||
@@ -630,7 +709,7 @@ function parseApplyOptions(args: string[]): ApplyOptions {
|
||||
else if (arg === "--wait") wait = true;
|
||||
else {
|
||||
commonArgs.push(arg);
|
||||
if (arg === "--target" || arg === "--node") {
|
||||
if (arg === "--target" || arg === "--node" || arg === "--consumer") {
|
||||
commonArgs.push(args[index + 1] ?? "");
|
||||
index += 1;
|
||||
}
|
||||
@@ -648,7 +727,7 @@ function parseWebhookTestOptions(args: string[]): WebhookTestOptions {
|
||||
if (arg === "--confirm") confirm = true;
|
||||
else {
|
||||
commonArgs.push(arg);
|
||||
if (arg === "--target" || arg === "--node") {
|
||||
if (arg === "--target" || arg === "--node" || arg === "--consumer") {
|
||||
commonArgs.push(args[index + 1] ?? "");
|
||||
index += 1;
|
||||
}
|
||||
@@ -659,15 +738,17 @@ function parseWebhookTestOptions(args: string[]): WebhookTestOptions {
|
||||
|
||||
function parseCommonOptions(args: string[]): CommonOptions {
|
||||
let targetId: string | null = null;
|
||||
let consumerId: string | null = null;
|
||||
let full = false;
|
||||
let raw = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--target" || arg === "--node") {
|
||||
if (arg === "--target" || arg === "--node" || arg === "--consumer") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${arg} must be a simple target id`);
|
||||
targetId = value;
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${arg} must be a simple id`);
|
||||
if (arg === "--consumer") consumerId = value;
|
||||
else targetId = value;
|
||||
index += 1;
|
||||
} else if (arg === "--full") {
|
||||
full = true;
|
||||
@@ -678,7 +759,7 @@ function parseCommonOptions(args: string[]): CommonOptions {
|
||||
throw new Error(`unsupported pipelines-as-code option: ${arg}`);
|
||||
}
|
||||
}
|
||||
return { targetId, full, raw };
|
||||
return { targetId, consumerId, full, raw };
|
||||
}
|
||||
|
||||
function positiveInteger(obj: Record<string, unknown>, key: string, path: string): number {
|
||||
|
||||
Reference in New Issue
Block a user