mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): add unified cluster operator cli
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
'use strict';
|
||||
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const IMAGE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,255}$/u;
|
||||
const ENTRYPOINT = [
|
||||
'node',
|
||||
'/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/product-cli/cli.js',
|
||||
];
|
||||
const COMMANDS = Object.freeze([
|
||||
Object.freeze({
|
||||
name: 'package',
|
||||
usage: 'Usage: ql3-plugin-package-client ',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'package-kubernetes',
|
||||
usage: 'Usage: ql3-plugin-package-client-kubernetes ',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'worker-credential',
|
||||
usage: 'Usage: ql3-worker-credential-client ',
|
||||
}),
|
||||
Object.freeze({ name: 'approval', usage: 'Usage: ql3-approval-client ' }),
|
||||
Object.freeze({ name: 'run', usage: 'Usage: ql3-run-client ' }),
|
||||
Object.freeze({ name: 'automation', usage: 'Usage: ql3-automation-client ' }),
|
||||
Object.freeze({
|
||||
name: 'model-credential',
|
||||
usage: 'Usage: ql3-provider-credential-client ',
|
||||
}),
|
||||
]);
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(`ql3 Cluster Admin product live contract failed: ${message}`);
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
if (argv.length !== 1) fail('exactly one --image argument is required');
|
||||
const match = /^--image=(.+)$/u.exec(argv[0]);
|
||||
if (!match || !IMAGE_PATTERN.test(match[1]))
|
||||
fail('image argument is invalid');
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function docker(args, options = {}) {
|
||||
return execFileSync('docker', args, {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 1024 * 1024,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function runImage(image, args) {
|
||||
return docker([
|
||||
'run',
|
||||
'--rm',
|
||||
'--read-only',
|
||||
'--network',
|
||||
'none',
|
||||
'--cap-drop',
|
||||
'ALL',
|
||||
'--security-opt',
|
||||
'no-new-privileges',
|
||||
'--user',
|
||||
'10001:10001',
|
||||
'--pids-limit',
|
||||
'32',
|
||||
'--memory',
|
||||
'128m',
|
||||
'--cpus',
|
||||
'0.25',
|
||||
'--tmpfs',
|
||||
'/tmp:rw,noexec,nosuid,nodev,size=8m,mode=700',
|
||||
image,
|
||||
...args,
|
||||
]);
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (process.env.QL3_CLUSTER_ADMIN_PRODUCT_LIVE !== '1') {
|
||||
fail('QL3_CLUSTER_ADMIN_PRODUCT_LIVE=1 is required');
|
||||
}
|
||||
const image = parseArguments(process.argv.slice(2));
|
||||
const inspected = JSON.parse(docker(['image', 'inspect', image]));
|
||||
if (!Array.isArray(inspected) || inspected.length !== 1) {
|
||||
fail('image inspection shape is invalid');
|
||||
}
|
||||
const fact = inspected[0];
|
||||
if (
|
||||
fact?.Os !== 'linux' ||
|
||||
(fact?.Architecture !== 'amd64' && fact?.Architecture !== 'arm64') ||
|
||||
fact?.Config?.User !== '10001:10001' ||
|
||||
JSON.stringify(fact?.Config?.Entrypoint) !== JSON.stringify(ENTRYPOINT) ||
|
||||
!Number.isSafeInteger(fact?.Size) ||
|
||||
fact.Size <= 0
|
||||
) {
|
||||
fail('image platform, identity, entrypoint or size contract drifted');
|
||||
}
|
||||
|
||||
const help = runImage(image, ['--help']);
|
||||
if (
|
||||
!help.startsWith('Usage: ql3-cluster-admin <command> [arguments]\n') ||
|
||||
!help.includes(
|
||||
'Server, migration, recovery, executor and key-custody authorities remain isolated.',
|
||||
)
|
||||
) {
|
||||
fail('product help contract drifted');
|
||||
}
|
||||
for (const { name, usage } of COMMANDS) {
|
||||
const output = runImage(image, [name, '--help']);
|
||||
if (!output.startsWith(usage)) {
|
||||
fail(`${name} delegation contract drifted`);
|
||||
}
|
||||
}
|
||||
const version = runImage(image, ['--version']).trim();
|
||||
if (version !== '3.0.0-alpha.0') fail('product version contract drifted');
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
image,
|
||||
architecture: fact.Architecture,
|
||||
user: fact.Config.User,
|
||||
imageBytes: fact.Size,
|
||||
commandCount: COMMANDS.length,
|
||||
isolation: Object.freeze({
|
||||
readOnlyRoot: true,
|
||||
network: 'none',
|
||||
capabilities: 'none',
|
||||
noNewPrivileges: true,
|
||||
pids: 32,
|
||||
memoryBytes: 128 * 1024 * 1024,
|
||||
cpus: 0.25,
|
||||
}),
|
||||
compatible: true,
|
||||
})}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
process.stderr.write(
|
||||
`${error instanceof Error ? error.message : 'unknown failure'}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { parseArguments };
|
||||
@@ -68,6 +68,68 @@ function yamlDocuments(readFile, filePath) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function kubernetesYamlFiles(directory) {
|
||||
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const filePath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) return kubernetesYamlFiles(filePath);
|
||||
return entry.isFile() && /\.ya?ml$/u.test(entry.name) ? [filePath] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function podSpecFor(document) {
|
||||
if (document?.kind === 'CronJob') {
|
||||
return document.spec?.jobTemplate?.spec?.template?.spec;
|
||||
}
|
||||
if (
|
||||
['DaemonSet', 'Deployment', 'Job', 'StatefulSet'].includes(document?.kind)
|
||||
) {
|
||||
return document.spec?.template?.spec;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function assertClusterAdminImageCommands(readFile, root, findings) {
|
||||
const kubernetesRoot = path.join(root, 'deploy/kubernetes/ql3-cluster');
|
||||
let references = 0;
|
||||
for (const filePath of kubernetesYamlFiles(kubernetesRoot)) {
|
||||
for (const document of yamlDocuments(readFile, filePath)) {
|
||||
const podSpec = podSpecFor(document);
|
||||
for (const section of ['initContainers', 'containers']) {
|
||||
for (const container of podSpec?.[section] ?? []) {
|
||||
if (container?.image !== 'qinglong3-cluster-admin:3.0.0-alpha.0') {
|
||||
continue;
|
||||
}
|
||||
references += 1;
|
||||
if (
|
||||
!Array.isArray(container.command) ||
|
||||
container.command.length === 0
|
||||
) {
|
||||
findings.push(
|
||||
finding(
|
||||
'QL3_CLUSTER_ADMIN_IMAGE_COMMAND_IMPLICIT',
|
||||
`${path.relative(root, filePath)} ${document.kind}/${
|
||||
document.metadata?.name ?? 'unnamed'
|
||||
} ${section}/${
|
||||
container.name ?? 'unnamed'
|
||||
} must explicitly override the Cluster Admin image command`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (references === 0) {
|
||||
findings.push(
|
||||
finding(
|
||||
'QL3_CLUSTER_ADMIN_IMAGE_REFERENCE_MISSING',
|
||||
'The Kubernetes deployment must contain reviewed Cluster Admin image references',
|
||||
),
|
||||
);
|
||||
}
|
||||
return references;
|
||||
}
|
||||
|
||||
function namedResource(resources, kind, name) {
|
||||
return resources.find(
|
||||
(resource) => resource?.kind === kind && resource?.metadata?.name === name,
|
||||
@@ -294,6 +356,7 @@ function assertExactExternalClosure(readFile, root, findings) {
|
||||
path.join(root, 'packages/ql3-cluster-admin/package.json'),
|
||||
);
|
||||
if (
|
||||
adminManifest.bin?.['ql3-cluster-admin'] !== 'dist/product-cli/cli.js' ||
|
||||
adminManifest.bin?.['ql3-plugin-package-recover'] !==
|
||||
'dist/plugin-package/recovery/pluginPackageRecoveryCli.js' ||
|
||||
adminManifest.bin?.['ql3-plugin-package-manage'] !==
|
||||
@@ -335,7 +398,7 @@ function assertExactExternalClosure(readFile, root, findings) {
|
||||
findings.push(
|
||||
finding(
|
||||
'QL3_CLUSTER_PLUGIN_RECOVERY_ENTRYPOINT_MISSING',
|
||||
'cluster-admin must publish the reviewed Package and Worker management and executor entrypoints',
|
||||
'cluster-admin must publish the reviewed product facade, Package and Worker management and executor entrypoints',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -465,7 +528,7 @@ function assertDockerfile(readFile, root, findings) {
|
||||
'/workspace/packages/ql3-cluster-postgres/dist',
|
||||
'/workspace/packages/ql3-cluster-admin/dist',
|
||||
'USER 10001:10001',
|
||||
'ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/plugin-package/recovery/pluginPackageRecoveryCli.js"]',
|
||||
'ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/product-cli/cli.js"]',
|
||||
];
|
||||
for (const value of adminRequired) {
|
||||
if (!adminDockerfile.includes(value)) {
|
||||
@@ -5014,10 +5077,16 @@ function auditClusterDeployment(options = {}) {
|
||||
const root = path.resolve(options.root ?? path.join(__dirname, '..'));
|
||||
const readFile = options.readFile ?? fs.readFileSync;
|
||||
const findings = [];
|
||||
let clusterAdminImageReferences = 0;
|
||||
try {
|
||||
assertExactExternalClosure(readFile, root, findings);
|
||||
assertDockerfile(readFile, root, findings);
|
||||
assertKubernetes(readFile, root, findings);
|
||||
clusterAdminImageReferences = assertClusterAdminImageCommands(
|
||||
readFile,
|
||||
root,
|
||||
findings,
|
||||
);
|
||||
assertClusterAiComponent(readFile, root, findings);
|
||||
assertPluginPackageManagementDeployment(readFile, root, findings);
|
||||
assertWorkerCredentialManagementDeployment(readFile, root, findings);
|
||||
@@ -5071,6 +5140,7 @@ function auditClusterDeployment(options = {}) {
|
||||
clusterAi: 'optional-projected-authority',
|
||||
clusterAiPromptOutput: 'optional-read-only-projected-keyring',
|
||||
imageReleasePins: 'independent-fail-closed-digests',
|
||||
clusterAdminImageReferences,
|
||||
findings: Object.freeze(findings),
|
||||
compatible: findings.length === 0,
|
||||
});
|
||||
|
||||
@@ -274,6 +274,11 @@ function auditClusterImageCiWorkflow(source) {
|
||||
/unexpected image contract/,
|
||||
'cluster image CI must verify architecture and runtime user',
|
||||
);
|
||||
requirePattern(
|
||||
source,
|
||||
/name: Run the bounded Cluster Admin product facade\s+if: matrix\.image == 'admin'\s+env:\s+IMAGE: qinglong3-cluster-admin:ci-\$\{\{ matrix\.image_arch \}\}\s+QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '1'\s+run: node scripts\/ql3-cluster-admin-product-live-contract\.cjs --image="\$\{IMAGE\}"/,
|
||||
'native admin image CI must run the bounded product facade contract',
|
||||
);
|
||||
requirePattern(
|
||||
source,
|
||||
/^ image-oci:\s*$/m,
|
||||
@@ -314,6 +319,7 @@ function auditClusterImageCiWorkflow(source) {
|
||||
images: ['control', 'control-ai', 'admin', 'local'],
|
||||
nativeArchitectures: ['amd64', 'arm64'],
|
||||
runtimeInventory: true,
|
||||
clusterAdminProductFacade: true,
|
||||
ociAttestations: true,
|
||||
osVulnerabilityScan: {
|
||||
scanner: 'trivy@0.70.0',
|
||||
|
||||
Reference in New Issue
Block a user