forked from boos/BoosAPI
Fix update helper health for ready failed release (#361)
This commit is contained in:
@@ -351,4 +351,78 @@ describe('update helper service', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('treats ready workloads as healthy even when the latest helm revision is marked failed', async () => {
|
||||||
|
const status = await getUpdateHelperStatus(
|
||||||
|
{
|
||||||
|
namespace: 'ai',
|
||||||
|
releaseName: 'metapi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
runCommand: async ({ command, args }) => {
|
||||||
|
if (command === 'helm' && args[0] === 'status') {
|
||||||
|
return {
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
version: 22,
|
||||||
|
info: {
|
||||||
|
status: 'failed',
|
||||||
|
description: 'Upgrade "metapi" failed: context deadline exceeded',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (command === 'helm' && args[0] === 'get') {
|
||||||
|
return {
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
image: {
|
||||||
|
repository: '1467078763/metapi',
|
||||||
|
tag: 'latest',
|
||||||
|
digest: 'sha256:1523dfcbaca146b8a64a3a022878165027900ee4cb396a45e474aa7a969dacb1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (command === 'helm' && args[0] === 'history') {
|
||||||
|
return {
|
||||||
|
stdout: JSON.stringify([
|
||||||
|
{ revision: '21', updated: '2026-03-31T14:07:40.115551823Z', status: 'deployed', description: 'Rollback to 17' },
|
||||||
|
{ revision: '22', updated: '2026-03-31T14:13:24.726024175Z', status: 'failed', description: 'Upgrade "metapi" failed: context deadline exceeded' },
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (command === 'kubectl' && args[0] === 'get') {
|
||||||
|
return {
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
status: {
|
||||||
|
phase: 'Running',
|
||||||
|
containerStatuses: [
|
||||||
|
{
|
||||||
|
ready: true,
|
||||||
|
imageID: 'docker-pullable://1467078763/metapi@sha256:1523dfcbaca146b8a64a3a022878165027900ee4cb396a45e474aa7a969dacb1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error('unexpected command');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(status).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
releaseName: 'metapi',
|
||||||
|
namespace: 'ai',
|
||||||
|
revision: '22',
|
||||||
|
imageRepository: '1467078763/metapi',
|
||||||
|
imageTag: 'latest',
|
||||||
|
imageDigest: 'sha256:1523dfcbaca146b8a64a3a022878165027900ee4cb396a45e474aa7a969dacb1',
|
||||||
|
healthy: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ type ParsedReleaseValues = {
|
|||||||
imageDigest: string | null;
|
imageDigest: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ParsedRuntimePodState = {
|
||||||
|
imageDigest: string | null;
|
||||||
|
allPodsReady: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
async function runCommand(input: RunCommandInput): Promise<RunCommandResult> {
|
async function runCommand(input: RunCommandInput): Promise<RunCommandResult> {
|
||||||
return await new Promise<RunCommandResult>((resolve, reject) => {
|
return await new Promise<RunCommandResult>((resolve, reject) => {
|
||||||
const child = spawn(input.command, input.args, {
|
const child = spawn(input.command, input.args, {
|
||||||
@@ -172,12 +177,14 @@ function parseReleaseValues(stdout: string): ParsedReleaseValues {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseRuntimeImageDigest(stdout: string): string | null {
|
function parseRuntimePodState(stdout: string): ParsedRuntimePodState {
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(stdout || '{}') as {
|
const payload = JSON.parse(stdout || '{}') as {
|
||||||
items?: Array<{
|
items?: Array<{
|
||||||
status?: {
|
status?: {
|
||||||
|
phase?: string | null;
|
||||||
containerStatuses?: Array<{
|
containerStatuses?: Array<{
|
||||||
|
ready?: boolean | null;
|
||||||
imageID?: string | null;
|
imageID?: string | null;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
}>;
|
}>;
|
||||||
@@ -185,18 +192,42 @@ function parseRuntimeImageDigest(stdout: string): string | null {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let imageDigest: string | null = null;
|
||||||
|
const items = payload.items || [];
|
||||||
|
if (items.length <= 0) {
|
||||||
|
return {
|
||||||
|
imageDigest: null,
|
||||||
|
allPodsReady: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let allPodsReady = true;
|
||||||
|
|
||||||
for (const item of payload.items || []) {
|
for (const item of payload.items || []) {
|
||||||
for (const status of item.status?.containerStatuses || []) {
|
const phase = String(item.status?.phase || '').trim().toLowerCase();
|
||||||
|
const containerStatuses = item.status?.containerStatuses || [];
|
||||||
|
if (phase !== 'running' || containerStatuses.length <= 0 || containerStatuses.some((status) => status.ready !== true)) {
|
||||||
|
allPodsReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const status of containerStatuses) {
|
||||||
const candidate = `${status.imageID || ''} ${status.image || ''}`;
|
const candidate = `${status.imageID || ''} ${status.image || ''}`;
|
||||||
const match = candidate.match(/(sha256:[a-f0-9]{64})/i);
|
const match = candidate.match(/(sha256:[a-f0-9]{64})/i);
|
||||||
if (match?.[1]) {
|
if (!imageDigest && match?.[1]) {
|
||||||
return match[1].toLowerCase();
|
imageDigest = match[1].toLowerCase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
return {
|
||||||
|
imageDigest,
|
||||||
|
allPodsReady,
|
||||||
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return {
|
||||||
|
imageDigest: null,
|
||||||
|
allPodsReady: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,8 +419,9 @@ export async function getUpdateHelperStatus(
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
const currentValues = parseReleaseValues(values.stdout);
|
const currentValues = parseReleaseValues(values.stdout);
|
||||||
const runtimeDigest = parseRuntimeImageDigest(pods.stdout);
|
const runtimeState = parseRuntimePodState(pods.stdout);
|
||||||
const historyRows = parseHistoryRows(history.stdout).slice(0, 5);
|
const historyRows = parseHistoryRows(history.stdout).slice(0, 5);
|
||||||
|
const releaseStatus = String(statusPayload.info?.status || '').trim().toLowerCase();
|
||||||
|
|
||||||
const historyEntries: UpdateHelperHistoryEntry[] = [];
|
const historyEntries: UpdateHelperHistoryEntry[] = [];
|
||||||
for (const row of historyRows) {
|
for (const row of historyRows) {
|
||||||
@@ -426,8 +458,8 @@ export async function getUpdateHelperStatus(
|
|||||||
revision: statusPayload.version === undefined || statusPayload.version === null ? null : String(statusPayload.version),
|
revision: statusPayload.version === undefined || statusPayload.version === null ? null : String(statusPayload.version),
|
||||||
imageRepository: currentValues.imageRepository,
|
imageRepository: currentValues.imageRepository,
|
||||||
imageTag: currentValues.imageTag,
|
imageTag: currentValues.imageTag,
|
||||||
imageDigest: runtimeDigest || currentValues.imageDigest,
|
imageDigest: runtimeState.imageDigest || currentValues.imageDigest,
|
||||||
healthy: String(statusPayload.info?.status || '').toLowerCase() === 'deployed',
|
healthy: releaseStatus === 'deployed' || (releaseStatus === 'failed' && runtimeState.allPodsReady),
|
||||||
history: historyEntries,
|
history: historyEntries,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user