PSA/ee/appliance/operator/lib/workloads.mjs
Hermes 284313f908
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
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

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 };