107 lines
5.0 KiB
TypeScript
107 lines
5.0 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { codeQueueContainerdImagePreflight, codeQueueManifestImagePreflight } from "./src/deploy";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: JsonRecord = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
function gatewayCheck(diagnostics: JsonRecord): JsonRecord {
|
|
const checks = diagnostics.checks as JsonRecord | undefined;
|
|
const deployment = checks?.deployment as JsonRecord | undefined;
|
|
const endpoint = checks?.endpoint as JsonRecord | undefined;
|
|
const target = checks?.targetService as JsonRecord | undefined;
|
|
const deploymentAvailable = deployment?.available === true;
|
|
const endpointNonEmpty = Number(endpoint?.readyAddressCount ?? 0) > 0;
|
|
return {
|
|
ok: diagnostics.ok === true && deploymentAvailable && endpointNonEmpty && target?.ok === true,
|
|
deploymentAvailable,
|
|
endpointNonEmpty,
|
|
targetServiceOk: target?.ok === true,
|
|
};
|
|
}
|
|
|
|
function schedulerStorageCheck(schedulerHealth: JsonRecord): JsonRecord {
|
|
const queue = schedulerHealth.queue as JsonRecord | undefined;
|
|
const storage = queue?.storage as JsonRecord | undefined;
|
|
const lastError = storage?.lastError ?? null;
|
|
return {
|
|
ok: storage?.postgresReady === true && lastError === null,
|
|
postgresReady: storage?.postgresReady === true,
|
|
lastError,
|
|
};
|
|
}
|
|
|
|
function staleReconcileCheck(schedulerHealth: JsonRecord): JsonRecord {
|
|
const queue = schedulerHealth.queue as JsonRecord | undefined;
|
|
const reconcile = queue?.reconcile as JsonRecord | undefined;
|
|
const recoverable = Number(reconcile?.recoverableOrphanedActiveTaskCount ?? queue?.orphanedActiveTaskCount ?? 0);
|
|
return {
|
|
ok: recoverable === 0,
|
|
recoverableOrphanedActiveTaskCount: recoverable,
|
|
retryWaitTaskCount: Number(reconcile?.retryWaitTaskCount ?? 0),
|
|
};
|
|
}
|
|
|
|
function runIssue3Regression(): JsonRecord {
|
|
const missingEndpointGateway = gatewayCheck({
|
|
ok: false,
|
|
checks: {
|
|
deployment: { ok: true, available: true, availableReplicas: 1 },
|
|
endpoint: { ok: false, readyAddressCount: 0 },
|
|
targetService: { ok: false },
|
|
},
|
|
});
|
|
assertCondition(missingEndpointGateway.ok === false, "microservice:code-queue-egress-gateway-health must fail on empty endpoint", missingEndpointGateway);
|
|
assertCondition(missingEndpointGateway.deploymentAvailable === true, "gateway deployment availability should be reported separately", missingEndpointGateway);
|
|
assertCondition(missingEndpointGateway.endpointNonEmpty === false, "gateway endpoint non-empty check should be explicit", missingEndpointGateway);
|
|
|
|
const storageFailure = schedulerStorageCheck({
|
|
queue: {
|
|
storage: {
|
|
postgresReady: false,
|
|
lastError: "CONNECT_TIMEOUT d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432",
|
|
},
|
|
},
|
|
});
|
|
assertCondition(storageFailure.ok === false, "diagnostics must fail when scheduler storage.lastError reports PostgreSQL route failure", storageFailure);
|
|
assertCondition(String(storageFailure.lastError).includes("CONNECT_TIMEOUT"), "storage failure detail should preserve route error", storageFailure);
|
|
|
|
const staleReconcile = staleReconcileCheck({
|
|
queue: {
|
|
orphanedActiveTaskCount: 1,
|
|
reconcile: {
|
|
recoverableOrphanedActiveTaskCount: 1,
|
|
retryWaitTaskCount: 2,
|
|
},
|
|
},
|
|
});
|
|
assertCondition(staleReconcile.ok === false, "code-queue:stale-active-reconcile must fail while recoverable active tasks remain", staleReconcile);
|
|
assertCondition(staleReconcile.retryWaitTaskCount === 2, "retry_wait reconcile count should be visible", staleReconcile);
|
|
|
|
const missingImage = codeQueueContainerdImagePreflight("docker.io/library/busybox:latest\n", "unidesk-code-queue:d601");
|
|
assertCondition(missingImage.ok === false, "code-queue:containerd-image-preflight must fail when k3s containerd lacks unidesk-code-queue:d601", missingImage);
|
|
const presentImage = codeQueueContainerdImagePreflight("docker.io/library/busybox:latest\nunidesk-code-queue:d601 application/vnd.oci.image.manifest.v1+json\n", "unidesk-code-queue:d601");
|
|
assertCondition(presentImage.ok === true, "code-queue:containerd-image-preflight should pass when the tag is present", presentImage);
|
|
|
|
const manifestPath = "src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml";
|
|
const manifest = readFileSync(manifestPath, "utf8");
|
|
const manifestImage = codeQueueManifestImagePreflight(manifest, "unidesk-code-queue:d601");
|
|
assertCondition(manifestImage.ok === true, "Code Queue k8s manifest deployments must all use unidesk-code-queue:d601", manifestImage);
|
|
|
|
return {
|
|
ok: true,
|
|
checks: [
|
|
{ name: "microservice:code-queue-egress-gateway-health", ok: true },
|
|
{ name: "microservice:code-queue-postgres-route-health", ok: true },
|
|
{ name: "code-queue:containerd-image-preflight", ok: true },
|
|
{ name: "code-queue:stale-active-reconcile", ok: true },
|
|
],
|
|
};
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
process.stdout.write(`${JSON.stringify(runIssue3Regression(), null, 2)}\n`);
|
|
}
|