mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): add read-only cluster context readiness
This commit is contained in:
@@ -4,8 +4,12 @@ import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { constants } from 'node:os';
|
||||
|
||||
import { resolveQingLong3ClusterProductCommand } from './productCommand';
|
||||
import { QingLong3ClusterProductContextError } from './productContext';
|
||||
import { validateQingLong3ClusterProductContext } from './productContext';
|
||||
import {
|
||||
probeQingLong3ClusterProductContext,
|
||||
QingLong3ClusterProductContextError,
|
||||
QingLong3ClusterProductContextProbeError,
|
||||
validateQingLong3ClusterProductContext,
|
||||
} from './productContext';
|
||||
|
||||
const FORWARDED_SIGNALS = Object.freeze([
|
||||
'SIGINT',
|
||||
@@ -132,6 +136,14 @@ async function main(argv: readonly string[]): Promise<void> {
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
return;
|
||||
}
|
||||
if (resolution.kind === 'context-probe') {
|
||||
const result = await probeQingLong3ClusterProductContext(
|
||||
resolution.contextFile,
|
||||
);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
if (!result.allReady) process.exitCode = 69;
|
||||
return;
|
||||
}
|
||||
invoke(resolution.targetFilePath, resolution.argv);
|
||||
} catch (error) {
|
||||
if (
|
||||
@@ -152,6 +164,24 @@ async function main(argv: readonly string[]): Promise<void> {
|
||||
process.exitCode = 78;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
error instanceof QingLong3ClusterProductContextProbeError ||
|
||||
(error !== null &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
error.code === 'QL3_CLUSTER_PRODUCT_CONTEXT_PROBE_FAILED')
|
||||
) {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify(
|
||||
lowSensitivityFailure(
|
||||
'QL3_CLUSTER_PRODUCT_CONTEXT_PROBE_FAILED',
|
||||
'QingLong 3.0 Cluster operator context probe failed',
|
||||
),
|
||||
)}\n`,
|
||||
);
|
||||
process.exitCode = 69;
|
||||
return;
|
||||
}
|
||||
process.stderr.write(
|
||||
`${JSON.stringify(
|
||||
lowSensitivityFailure(
|
||||
|
||||
@@ -14,6 +14,7 @@ export type QingLong3ClusterProductCommandResolution =
|
||||
| Readonly<{ kind: 'help'; output: string }>
|
||||
| Readonly<{ kind: 'version'; output: string }>
|
||||
| Readonly<{ kind: 'context-validation'; contextFile: string }>
|
||||
| Readonly<{ kind: 'context-probe'; contextFile: string }>
|
||||
| Readonly<{
|
||||
kind: 'invoke';
|
||||
command: QingLong3ClusterProductCommandDefinition;
|
||||
@@ -175,6 +176,7 @@ export function qingLong3ClusterProductHelp(): string {
|
||||
'',
|
||||
'Local operator commands:',
|
||||
' context validate --context=/absolute/operator-context.json',
|
||||
' context probe --context=/absolute/operator-context.json',
|
||||
'',
|
||||
'Use `ql3-cluster-admin <command> --help` for command-specific usage.',
|
||||
'Use `--context=/absolute/operator-context.json` to inject only stable client paths.',
|
||||
@@ -200,7 +202,7 @@ export function resolveQingLong3ClusterProductCommand(
|
||||
if (argv[0] === 'context') {
|
||||
if (
|
||||
argv.length !== 3 ||
|
||||
argv[1] !== 'validate' ||
|
||||
(argv[1] !== 'validate' && argv[1] !== 'probe') ||
|
||||
!argv[2]!.startsWith('--context=') ||
|
||||
argv[2] === '--context='
|
||||
) {
|
||||
@@ -215,7 +217,7 @@ export function resolveQingLong3ClusterProductCommand(
|
||||
resolveInstalledTarget(distRoot, definition);
|
||||
}
|
||||
return Object.freeze({
|
||||
kind: 'context-validation',
|
||||
kind: argv[1] === 'validate' ? 'context-validation' : 'context-probe',
|
||||
contextFile: argv[2]!.slice('--context='.length),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,9 +10,15 @@ import {
|
||||
import { isAbsolute } from 'node:path';
|
||||
import { TextDecoder } from 'node:util';
|
||||
|
||||
import { validateClusterAuthenticatedManagementClientConfiguration } from '../management-support/pluginPackageManagementClient';
|
||||
import {
|
||||
validateClusterAuthenticatedManagementClientConfiguration,
|
||||
} from '../management-support/pluginPackageManagementClient';
|
||||
import type { ClusterAuthenticatedManagementClientKind } from '../management-support/pluginPackageManagementClient';
|
||||
import { validateClusterPluginPackageManagementKubernetesConfiguration } from '../plugin-package/management/pluginPackageManagementKubernetesClient';
|
||||
import { probeClusterAuthenticatedManagementClientReadiness } from '../management-support/managementReadinessProbe';
|
||||
import {
|
||||
probeClusterPluginPackageManagementKubernetesReadiness,
|
||||
validateClusterPluginPackageManagementKubernetesConfiguration,
|
||||
} from '../plugin-package/management/pluginPackageManagementKubernetesClient';
|
||||
|
||||
const MAXIMUM_CONTEXT_BYTES = 64 * 1024;
|
||||
const MAXIMUM_PATH_BYTES = 4_096;
|
||||
@@ -57,6 +63,22 @@ export interface QingLong3ClusterProductContextValidation {
|
||||
readonly mutation: false;
|
||||
}
|
||||
|
||||
export interface QingLong3ClusterProductContextProbe {
|
||||
readonly schemaVersion: 1;
|
||||
readonly component: 'qinglong3-cluster-product-cli';
|
||||
readonly event: 'context_probed';
|
||||
readonly commandCount: number;
|
||||
readonly commands: readonly Readonly<{
|
||||
name: ContextCommandName;
|
||||
transport: 'https' | 'kubernetes-port-forward';
|
||||
status: 'ready' | 'not_ready';
|
||||
}>[];
|
||||
readonly allReady: boolean;
|
||||
readonly requestMethod: 'GET';
|
||||
readonly requestPath: '/readyz';
|
||||
readonly mutation: false;
|
||||
}
|
||||
|
||||
const CONTEXT_COMMAND_CLIENT_KINDS: Readonly<
|
||||
Record<ContextCommandName, ClusterAuthenticatedManagementClientKind>
|
||||
> = Object.freeze({
|
||||
@@ -78,6 +100,15 @@ export class QingLong3ClusterProductContextError extends TypeError {
|
||||
}
|
||||
}
|
||||
|
||||
export class QingLong3ClusterProductContextProbeError extends Error {
|
||||
readonly code = 'QL3_CLUSTER_PRODUCT_CONTEXT_PROBE_FAILED';
|
||||
|
||||
constructor() {
|
||||
super('QingLong 3.0 Cluster operator context probe failed');
|
||||
this.name = 'QingLong3ClusterProductContextProbeError';
|
||||
}
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new QingLong3ClusterProductContextError();
|
||||
}
|
||||
@@ -328,3 +359,62 @@ export async function validateQingLong3ClusterProductContext(
|
||||
throw new QingLong3ClusterProductContextError();
|
||||
}
|
||||
}
|
||||
|
||||
export async function probeQingLong3ClusterProductContext(
|
||||
contextFile: string,
|
||||
): Promise<Readonly<QingLong3ClusterProductContextProbe>> {
|
||||
try {
|
||||
await validateQingLong3ClusterProductContext(contextFile);
|
||||
const context = loadQingLong3ClusterProductContext(contextFile);
|
||||
const commands: Array<
|
||||
QingLong3ClusterProductContextProbe['commands'][number]
|
||||
> = [];
|
||||
for (const name of CONTEXT_COMMANDS) {
|
||||
const command = context.commands[name];
|
||||
if (command === undefined) continue;
|
||||
const result =
|
||||
name === 'package-kubernetes'
|
||||
? await probeClusterPluginPackageManagementKubernetesReadiness(
|
||||
command.configFile,
|
||||
command.kubernetesFile!,
|
||||
)
|
||||
: await probeClusterAuthenticatedManagementClientReadiness(
|
||||
command.configFile,
|
||||
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
||||
);
|
||||
commands.push(
|
||||
Object.freeze({
|
||||
name,
|
||||
transport:
|
||||
name === 'package-kubernetes'
|
||||
? 'kubernetes-port-forward'
|
||||
: result.transport,
|
||||
status: result.ready ? 'ready' : 'not_ready',
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-cluster-product-cli',
|
||||
event: 'context_probed',
|
||||
commandCount: commands.length,
|
||||
commands: Object.freeze(commands),
|
||||
allReady: commands.every(({ status }) => status === 'ready'),
|
||||
requestMethod: 'GET',
|
||||
requestPath: '/readyz',
|
||||
mutation: false,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof QingLong3ClusterProductContextError) throw error;
|
||||
if (
|
||||
error !== null &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
typeof error.code === 'string' &&
|
||||
error.code.endsWith('CONFIG_INVALID')
|
||||
) {
|
||||
throw new QingLong3ClusterProductContextError();
|
||||
}
|
||||
throw new QingLong3ClusterProductContextProbeError();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user