|
|
|
@@ -117,8 +117,11 @@ function parseOptions(args: string[]): ParsedOptions {
|
|
|
|
|
options.dryRun = true;
|
|
|
|
|
} else if (arg === "--wait") {
|
|
|
|
|
options.wait = true;
|
|
|
|
|
} else if (arg === "--in-cluster") {
|
|
|
|
|
options.inCluster = true;
|
|
|
|
|
} else if (arg === "--controller") {
|
|
|
|
|
options.controller = true;
|
|
|
|
|
if (isInClusterRuntime()) options.inCluster = true;
|
|
|
|
|
else if (options.action === "status") options.live = true;
|
|
|
|
|
} else if (arg === "--live") {
|
|
|
|
|
options.live = true;
|
|
|
|
|
} else if (arg === "--no-live") {
|
|
|
|
@@ -162,6 +165,10 @@ function parseOptions(args: string[]): ParsedOptions {
|
|
|
|
|
return options;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isInClusterRuntime(): boolean {
|
|
|
|
|
return Boolean(process.env.KUBERNETES_SERVICE_HOST && process.env.KUBERNETES_SERVICE_PORT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function defaultOptions(action: BranchFollowerAction, _args: string[]): ParsedOptions {
|
|
|
|
|
return {
|
|
|
|
|
action,
|
|
|
|
@@ -171,7 +178,7 @@ function defaultOptions(action: BranchFollowerAction, _args: string[]): ParsedOp
|
|
|
|
|
confirm: false,
|
|
|
|
|
dryRun: false,
|
|
|
|
|
wait: false,
|
|
|
|
|
controller: false,
|
|
|
|
|
inCluster: false,
|
|
|
|
|
live: false,
|
|
|
|
|
noLive: false,
|
|
|
|
|
full: false,
|
|
|
|
@@ -525,9 +532,9 @@ async function applyController(registry: BranchFollowerRegistry, options: Parsed
|
|
|
|
|
async function buildStatus(registry: BranchFollowerRegistry, options: ParsedOptions): Promise<Record<string, unknown>> {
|
|
|
|
|
let k8s = readK8sState(registry, options);
|
|
|
|
|
const wantsLive = options.live || (!options.noLive && Object.keys(k8s.stateByFollower).length === 0);
|
|
|
|
|
const refresh = wantsLive && !options.controller ? runControllerReconcileJob(registry, options, { dryRun: true, wait: true, recordState: true }) : null;
|
|
|
|
|
const refresh = wantsLive && !options.inCluster ? runControllerReconcileJob(registry, options, { dryRun: true, wait: true, recordState: true }) : null;
|
|
|
|
|
if (refresh !== null) k8s = readK8sState(registry, options);
|
|
|
|
|
const shouldLive = wantsLive && options.controller;
|
|
|
|
|
const shouldLive = wantsLive && options.inCluster;
|
|
|
|
|
const selected = selectFollowers(registry, options, { includeDisabled: true });
|
|
|
|
|
const followers = [];
|
|
|
|
|
for (const follower of selected) {
|
|
|
|
@@ -553,7 +560,7 @@ async function buildStatus(registry: BranchFollowerRegistry, options: ParsedOpti
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runOnce(registry: BranchFollowerRegistry, options: ParsedOptions): Promise<Record<string, unknown>> {
|
|
|
|
|
if (!options.controller) {
|
|
|
|
|
if (!options.inCluster) {
|
|
|
|
|
const refresh = runControllerReconcileJob(registry, options, { dryRun: options.dryRun, wait: true, recordState: true });
|
|
|
|
|
const k8s = readK8sState(registry, options);
|
|
|
|
|
const selected = selectFollowers(registry, options, { includeDisabled: false });
|
|
|
|
@@ -599,7 +606,7 @@ async function runOnce(registry: BranchFollowerRegistry, options: ParsedOptions)
|
|
|
|
|
dryRun: options.dryRun,
|
|
|
|
|
confirm: options.confirm,
|
|
|
|
|
wait: options.wait,
|
|
|
|
|
controller: options.controller,
|
|
|
|
|
controller: options.inCluster,
|
|
|
|
|
registry: registrySummary(registry),
|
|
|
|
|
followers: results,
|
|
|
|
|
warnings: stateWriteWarnings,
|
|
|
|
@@ -663,7 +670,7 @@ async function runFollowerDrillDown(registry: BranchFollowerRegistry, options: P
|
|
|
|
|
}
|
|
|
|
|
const follower = registry.followers.find((item) => item.id === options.followerId);
|
|
|
|
|
if (follower === undefined) throw new Error(`unknown follower ${options.followerId}`);
|
|
|
|
|
if (!options.controller) {
|
|
|
|
|
if (!options.inCluster) {
|
|
|
|
|
const refresh = runControllerReconcileJob(registry, options, { dryRun: true, wait: true, recordState: true });
|
|
|
|
|
const k8s = readK8sState(registry, options);
|
|
|
|
|
const stored = k8s.stateByFollower[follower.id] ?? {};
|
|
|
|
@@ -781,7 +788,7 @@ async function decideAndMaybeTrigger(
|
|
|
|
|
if (options.confirm && (phase === "PendingTrigger" || phase === "Superseded" || (phase === "Observed" && observedSha !== null))) {
|
|
|
|
|
const trigger = await executeTrigger(registry, follower, observedSha, options);
|
|
|
|
|
triggerCommand = trigger.command;
|
|
|
|
|
phase = trigger.ok ? (options.wait || options.controller ? "ClosingOut" : "Triggering") : "Failed";
|
|
|
|
|
phase = trigger.ok ? (options.wait || options.inCluster ? "ClosingOut" : "Triggering") : "Failed";
|
|
|
|
|
decision = trigger.ok ? `trigger submitted for ${shortSha(observedSha)}` : `trigger failed for ${shortSha(observedSha)}`;
|
|
|
|
|
inFlightJob = trigger.jobId ?? live.inFlightJob;
|
|
|
|
|
lastTriggeredSha = observedSha;
|
|
|
|
@@ -839,7 +846,7 @@ async function decideAndMaybeTrigger(
|
|
|
|
|
inFlightJob,
|
|
|
|
|
budgetSource: follower.budgets,
|
|
|
|
|
controller: {
|
|
|
|
|
mode: options.controller ? "k8s-controller" : "local-cli",
|
|
|
|
|
mode: options.inCluster ? "k8s-controller" : "local-cli",
|
|
|
|
|
stateConfigMap: registry.controller.stateConfigMapName,
|
|
|
|
|
leaseName: registry.controller.leaseName,
|
|
|
|
|
},
|
|
|
|
@@ -861,16 +868,16 @@ async function decideAndMaybeTrigger(
|
|
|
|
|
async function executeTrigger(registry: BranchFollowerRegistry, follower: FollowerSpec, observedSha: string | null, options: ParsedOptions): Promise<TriggerResult> {
|
|
|
|
|
const spec = follower.commands.trigger;
|
|
|
|
|
const timeoutSeconds = options.timeoutSeconds ?? spec.timeoutSeconds;
|
|
|
|
|
if (follower.adapter === "hwlab-node-runtime" && options.controller) {
|
|
|
|
|
if (follower.adapter === "hwlab-node-runtime" && options.inCluster) {
|
|
|
|
|
return await executeNativeHwlabNodeTrigger(registry, follower, observedSha, options, timeoutSeconds);
|
|
|
|
|
}
|
|
|
|
|
if (follower.adapter === "agentrun-yaml-lane" && options.controller) {
|
|
|
|
|
if (follower.adapter === "agentrun-yaml-lane" && options.inCluster) {
|
|
|
|
|
return await executeNativeAgentRunTrigger(registry, follower, observedSha, options, timeoutSeconds);
|
|
|
|
|
}
|
|
|
|
|
if (follower.adapter === "web-probe-sentinel-cicd" && options.controller) {
|
|
|
|
|
if (follower.adapter === "web-probe-sentinel-cicd" && options.inCluster) {
|
|
|
|
|
return await executeNativeSentinelTrigger(registry, follower, observedSha, options, timeoutSeconds);
|
|
|
|
|
}
|
|
|
|
|
if (!options.wait && !options.controller) {
|
|
|
|
|
if (!options.wait && !options.inCluster) {
|
|
|
|
|
const job = startJob(`cicd_branch_follower_${safeJobSegment(follower.id)}`, spec.argv, `Trigger ${follower.id} for observed sha ${observedSha ?? "unknown"}`);
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
@@ -1957,7 +1964,7 @@ function kubePodList(registry: BranchFollowerRegistry, options: ParsedOptions, s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function runKubeScript(registry: BranchFollowerRegistry, options: ParsedOptions, script: string, input: string, timeoutMs: number): CommandResult {
|
|
|
|
|
if (options.controller) {
|
|
|
|
|
if (options.inCluster) {
|
|
|
|
|
return runCommand(["sh", "-lc", script], repoRoot, { input, timeoutMs });
|
|
|
|
|
}
|
|
|
|
|
return runCommand([transPath(), registry.controller.kubeRoute, "sh"], repoRoot, { input: `${script}\n`, timeoutMs });
|
|
|
|
@@ -2349,7 +2356,7 @@ function roundSeconds(value: number): number {
|
|
|
|
|
function writeFollowerState(registry: BranchFollowerRegistry, state: FollowerState, options: ParsedOptions): CommandResult {
|
|
|
|
|
const json = JSON.stringify(compactFollowerStateForConfigMap(state));
|
|
|
|
|
const dataPatch = JSON.stringify({ data: { [state.id]: json, _updatedAt: new Date().toISOString(), _specRef: SPEC_REF } });
|
|
|
|
|
if (options.controller) {
|
|
|
|
|
if (options.inCluster) {
|
|
|
|
|
const patchBase64 = Buffer.from(dataPatch, "utf8").toString("base64");
|
|
|
|
|
const createBase64 = Buffer.from(JSON.stringify({
|
|
|
|
|
metadata: {
|
|
|
|
|