Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
210 lines
5.7 KiB
JavaScript
210 lines
5.7 KiB
JavaScript
import { ShellRunner } from './runner.mjs';
|
|
|
|
const DEFAULT_NAMESPACES = ['msp'];
|
|
|
|
function parseJsonOutput(output) {
|
|
try {
|
|
return JSON.parse(output);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function formatDuration(seconds) {
|
|
const value = Math.max(0, Math.floor(Number(seconds) || 0));
|
|
if (value < 60) {
|
|
return `${value}s`;
|
|
}
|
|
if (value < 3600) {
|
|
return `${Math.floor(value / 60)}m`;
|
|
}
|
|
if (value < 86400) {
|
|
return `${Math.floor(value / 3600)}h`;
|
|
}
|
|
return `${Math.floor(value / 86400)}d`;
|
|
}
|
|
|
|
function toTimestamp(text) {
|
|
const value = Date.parse(String(text || ''));
|
|
if (Number.isNaN(value)) {
|
|
return null;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function podReadyCount(item) {
|
|
const statuses = item?.status?.containerStatuses || [];
|
|
const total = statuses.length || item?.spec?.containers?.length || 0;
|
|
const ready = statuses.reduce((count, status) => (status.ready ? count + 1 : count), 0);
|
|
return { ready, total };
|
|
}
|
|
|
|
function podRestartCount(item) {
|
|
const statuses = item?.status?.containerStatuses || [];
|
|
return statuses.reduce((count, status) => count + Number(status.restartCount || 0), 0);
|
|
}
|
|
|
|
function podDisplayStatus(item) {
|
|
if (item?.metadata?.deletionTimestamp) {
|
|
return 'Terminating';
|
|
}
|
|
|
|
for (const status of item?.status?.containerStatuses || []) {
|
|
const waitingReason = status?.state?.waiting?.reason;
|
|
if (waitingReason) {
|
|
return waitingReason;
|
|
}
|
|
const terminatedReason = status?.state?.terminated?.reason;
|
|
if (terminatedReason) {
|
|
return terminatedReason;
|
|
}
|
|
}
|
|
|
|
return item?.status?.phase || 'Unknown';
|
|
}
|
|
|
|
function summarizePod(item, nowMs) {
|
|
const namespace = item?.metadata?.namespace || 'default';
|
|
const name = item?.metadata?.name || 'unknown';
|
|
const startedAt = toTimestamp(item?.status?.startTime) ?? toTimestamp(item?.metadata?.creationTimestamp) ?? nowMs;
|
|
const ageSeconds = Math.max(0, Math.floor((nowMs - startedAt) / 1000));
|
|
const readiness = podReadyCount(item);
|
|
const restarts = podRestartCount(item);
|
|
|
|
return {
|
|
key: `${namespace}/${name}`,
|
|
namespace,
|
|
name,
|
|
phase: item?.status?.phase || 'Unknown',
|
|
status: podDisplayStatus(item),
|
|
ready: `${readiness.ready}/${readiness.total}`,
|
|
restarts,
|
|
age: formatDuration(ageSeconds),
|
|
ageSeconds,
|
|
};
|
|
}
|
|
|
|
export async function listAppliancePods(env, options = {}) {
|
|
const shell = options.runner || new ShellRunner({ cwd: env.runtime.assetRoot });
|
|
const kubeconfig = options.kubeconfig || env.paths.kubeconfig;
|
|
const nowMs = options.nowMs || Date.now();
|
|
const namespaces = options.namespaces || DEFAULT_NAMESPACES;
|
|
|
|
const results = await Promise.all(
|
|
namespaces.map(async (namespace) => {
|
|
const result = await shell.runCapture('kubectl', [
|
|
'--kubeconfig',
|
|
kubeconfig,
|
|
'-n',
|
|
namespace,
|
|
'get',
|
|
'pods',
|
|
'-o',
|
|
'json',
|
|
]);
|
|
return { namespace, ...result, json: result.ok ? parseJsonOutput(result.output) : null };
|
|
}),
|
|
);
|
|
|
|
const pods = [];
|
|
const errors = [];
|
|
for (const result of results) {
|
|
if (!result.ok) {
|
|
errors.push(`namespace ${result.namespace}: ${result.output.trim() || 'kubectl get pods failed'}`);
|
|
continue;
|
|
}
|
|
for (const item of result.json?.items || []) {
|
|
pods.push(summarizePod(item, nowMs));
|
|
}
|
|
}
|
|
|
|
pods.sort((a, b) => a.namespace.localeCompare(b.namespace) || a.name.localeCompare(b.name));
|
|
|
|
return {
|
|
fetchedAt: new Date(nowMs).toISOString(),
|
|
namespaces: [...namespaces],
|
|
pods,
|
|
errors,
|
|
};
|
|
}
|
|
|
|
function parseLogLine(line) {
|
|
const text = String(line || '');
|
|
const match = text.match(/^(\S+)\s(.*)$/);
|
|
if (match) {
|
|
const stamp = Date.parse(match[1]);
|
|
if (!Number.isNaN(stamp)) {
|
|
return {
|
|
timestamp: match[1],
|
|
message: match[2],
|
|
text,
|
|
};
|
|
}
|
|
}
|
|
return {
|
|
timestamp: null,
|
|
message: text,
|
|
text,
|
|
};
|
|
}
|
|
|
|
function splitLogLines(output) {
|
|
return String(output || '')
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trimEnd())
|
|
.filter(Boolean)
|
|
.map(parseLogLine);
|
|
}
|
|
|
|
export async function readPodLogsTail(env, podRef, options = {}) {
|
|
const shell = options.runner || new ShellRunner({ cwd: env.runtime.assetRoot });
|
|
const kubeconfig = options.kubeconfig || env.paths.kubeconfig;
|
|
const tailLines = Number(options.tailLines || 200);
|
|
const args = ['--kubeconfig', kubeconfig, '-n', podRef.namespace, 'logs', podRef.name, '--timestamps', '--tail', String(Math.max(1, tailLines))];
|
|
|
|
if (podRef.container) {
|
|
args.push('-c', podRef.container);
|
|
}
|
|
|
|
if (options.previous) {
|
|
args.push('--previous');
|
|
}
|
|
|
|
const result = await shell.runCapture('kubectl', args);
|
|
return {
|
|
ok: result.ok,
|
|
code: result.code,
|
|
lines: splitLogLines(result.output),
|
|
error: result.ok ? null : result.output.trim() || 'kubectl logs failed',
|
|
};
|
|
}
|
|
|
|
export async function readPodLogsSince(env, podRef, options = {}) {
|
|
const shell = options.runner || new ShellRunner({ cwd: env.runtime.assetRoot });
|
|
const kubeconfig = options.kubeconfig || env.paths.kubeconfig;
|
|
const sinceTime = options.sinceTime;
|
|
if (!sinceTime) {
|
|
return { ok: true, code: 0, lines: [], error: null };
|
|
}
|
|
|
|
const args = ['--kubeconfig', kubeconfig, '-n', podRef.namespace, 'logs', podRef.name, '--timestamps', '--since-time', sinceTime];
|
|
|
|
if (podRef.container) {
|
|
args.push('-c', podRef.container);
|
|
}
|
|
|
|
if (options.previous) {
|
|
args.push('--previous');
|
|
}
|
|
|
|
const result = await shell.runCapture('kubectl', args);
|
|
return {
|
|
ok: result.ok,
|
|
code: result.code,
|
|
lines: splitLogLines(result.output),
|
|
error: result.ok ? null : result.output.trim() || 'kubectl logs failed',
|
|
};
|
|
}
|
|
|
|
export { DEFAULT_NAMESPACES, formatDuration, parseLogLine, summarizePod };
|