mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
fix(ci): restore ql3 multi-profile gates
This commit is contained in:
@@ -55,6 +55,7 @@ const POSTGRES_ROLE_NAMES = Object.freeze([
|
||||
'ql3_migration',
|
||||
'ql3_package_executor',
|
||||
'ql3_package_manager',
|
||||
'ql3_run_manager',
|
||||
'ql3_runtime',
|
||||
'ql3_worker_credential_executor',
|
||||
'ql3_worker_credential_manager',
|
||||
|
||||
@@ -53,6 +53,7 @@ const ROLE_NAMES = Object.freeze([
|
||||
'ql3_migration',
|
||||
'ql3_package_executor',
|
||||
'ql3_package_manager',
|
||||
'ql3_run_manager',
|
||||
'ql3_runtime',
|
||||
'ql3_worker_credential_executor',
|
||||
'ql3_worker_credential_manager',
|
||||
@@ -733,10 +734,14 @@ async function main() {
|
||||
ql3_migration: randomSecret(),
|
||||
ql3_runtime: randomSecret(),
|
||||
ql3_admin: randomSecret(),
|
||||
ql3_ai_credential_manager: randomSecret(),
|
||||
ql3_ai_credential_tester: randomSecret(),
|
||||
ql3_ai_maintenance: randomSecret(),
|
||||
ql3_automation_manager: randomSecret(),
|
||||
ql3_approval_manager: randomSecret(),
|
||||
ql3_package_manager: randomSecret(),
|
||||
ql3_package_executor: randomSecret(),
|
||||
ql3_run_manager: randomSecret(),
|
||||
ql3_worker_credential_manager: randomSecret(),
|
||||
ql3_worker_credential_executor: randomSecret(),
|
||||
ql3_worker_ingress: randomSecret(),
|
||||
|
||||
@@ -16,6 +16,7 @@ const QL3_VERSION = readReleaseIdentity(DEFAULT_ROOT).version;
|
||||
const OCI_INDEX_MEDIA_TYPE = 'application/vnd.oci.image.index.v1+json';
|
||||
const OCI_MANIFEST_MEDIA_TYPE = 'application/vnd.oci.image.manifest.v1+json';
|
||||
const OCI_CONFIG_MEDIA_TYPE = 'application/vnd.oci.image.config.v1+json';
|
||||
const OCI_EMPTY_CONFIG_MEDIA_TYPE = 'application/vnd.oci.empty.v1+json';
|
||||
const OCI_LAYER_MEDIA_TYPE = 'application/vnd.oci.image.layer.v1.tar+gzip';
|
||||
const IN_TOTO_MEDIA_TYPE = 'application/vnd.in-toto+json';
|
||||
const SPDX_PREDICATE_TYPE = 'https://spdx.dev/Document';
|
||||
@@ -447,23 +448,39 @@ function auditAttestation(
|
||||
OCI_MANIFEST_MEDIA_TYPE,
|
||||
MAX_INDEX_BYTES,
|
||||
);
|
||||
const config = blobReader.readJson(
|
||||
manifest.config,
|
||||
OCI_CONFIG_MEDIA_TYPE,
|
||||
MAX_CONFIG_BYTES,
|
||||
);
|
||||
if (
|
||||
config.architecture !== 'unknown' ||
|
||||
config.os !== 'unknown' ||
|
||||
JSON.stringify(config.config) !== JSON.stringify({}) ||
|
||||
!Array.isArray(config.rootfs?.diff_ids) ||
|
||||
!Array.isArray(manifest.layers) ||
|
||||
JSON.stringify(config.rootfs.diff_ids) !==
|
||||
JSON.stringify(manifest.layers.map((layer) => layer.digest))
|
||||
) {
|
||||
throw new Error(
|
||||
'OCI attestation config must bind an empty unknown platform',
|
||||
if (manifest.config?.mediaType === OCI_EMPTY_CONFIG_MEDIA_TYPE) {
|
||||
normalizeDescriptor(
|
||||
manifest.config,
|
||||
OCI_EMPTY_CONFIG_MEDIA_TYPE,
|
||||
MAX_CONFIG_BYTES,
|
||||
);
|
||||
const emptyConfig = blobReader.read(
|
||||
manifest.config,
|
||||
MAX_CONFIG_BYTES,
|
||||
true,
|
||||
);
|
||||
if (emptyConfig.toString('utf8') !== '{}') {
|
||||
throw new Error('OCI attestation empty config must be canonical');
|
||||
}
|
||||
} else {
|
||||
const config = blobReader.readJson(
|
||||
manifest.config,
|
||||
OCI_CONFIG_MEDIA_TYPE,
|
||||
MAX_CONFIG_BYTES,
|
||||
);
|
||||
if (
|
||||
config.architecture !== 'unknown' ||
|
||||
config.os !== 'unknown' ||
|
||||
JSON.stringify(config.config) !== JSON.stringify({}) ||
|
||||
!Array.isArray(config.rootfs?.diff_ids) ||
|
||||
!Array.isArray(manifest.layers) ||
|
||||
JSON.stringify(config.rootfs.diff_ids) !==
|
||||
JSON.stringify(manifest.layers.map((layer) => layer.digest))
|
||||
) {
|
||||
throw new Error(
|
||||
'OCI attestation config must bind an empty unknown platform',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(manifest.layers) || manifest.layers.length !== 2) {
|
||||
throw new Error('OCI attestation must contain exactly SBOM and provenance');
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const repositoryRoot = path.resolve(__dirname, '..');
|
||||
|
||||
const HOST_BOUND_TESTS = Object.freeze({
|
||||
'packages/ql3-local-owner-cli/test/adoptedDeploymentBundle.test.cjs':
|
||||
'requires repository-owned deployment entrypoint material',
|
||||
'packages/ql3-local-owner-cli/test/localDeployment.test.cjs':
|
||||
'exercises mutable deployment staging and host ownership',
|
||||
'packages/ql3-local-owner-cli/test/reconciliationCapturePrepare.test.cjs':
|
||||
'exercises sealed SQLite WAL and SHM capture assets',
|
||||
'packages/ql3-local-owner-cli/test/serviceBridgeRoot.test.cjs':
|
||||
'installs systemd and OpenRC descriptors into host service directories',
|
||||
});
|
||||
|
||||
function collectReadonlyOwnerTests(root = repositoryRoot) {
|
||||
const packagesRoot = path.join(root, 'packages');
|
||||
const packageNames = fs
|
||||
.readdirSync(packagesRoot, { withFileTypes: true })
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.isDirectory() &&
|
||||
(entry.name === 'ql3-local-command-file' ||
|
||||
entry.name.startsWith('ql3-local-owner-')),
|
||||
)
|
||||
.map(({ name }) => name)
|
||||
.sort();
|
||||
const discovered = [];
|
||||
for (const packageName of packageNames) {
|
||||
const testRoot = path.join(packagesRoot, packageName, 'test');
|
||||
if (!fs.existsSync(testRoot)) continue;
|
||||
for (const entry of fs.readdirSync(testRoot, { withFileTypes: true })) {
|
||||
if (entry.isFile() && entry.name.endsWith('.test.cjs')) {
|
||||
discovered.push(
|
||||
path.posix.join('packages', packageName, 'test', entry.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
discovered.sort();
|
||||
const missingClassifications = Object.keys(HOST_BOUND_TESTS).filter(
|
||||
(file) => !discovered.includes(file),
|
||||
);
|
||||
if (missingClassifications.length !== 0) {
|
||||
throw new Error(
|
||||
`reviewed host-bound Owner tests are missing: ${missingClassifications.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
const tests = discovered.filter((file) => !(file in HOST_BOUND_TESTS));
|
||||
if (tests.length === 0) {
|
||||
throw new Error('read-only Owner test plan is empty');
|
||||
}
|
||||
return Object.freeze({
|
||||
tests: Object.freeze(tests),
|
||||
hostBound: HOST_BOUND_TESTS,
|
||||
});
|
||||
}
|
||||
|
||||
function assertWriteRejected(target) {
|
||||
try {
|
||||
fs.writeFileSync(target, 'unexpected-write', { flag: 'wx' });
|
||||
} catch (error) {
|
||||
if (error?.code === 'EROFS' || error?.code === 'EACCES') return;
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
fs.unlinkSync(target);
|
||||
} finally {
|
||||
throw new Error(`read-only boundary accepted a write: ${target}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertContainerBoundary(mode) {
|
||||
if (!['root', 'nonroot'].includes(mode)) {
|
||||
throw new Error(
|
||||
'usage: ql3-local-owner-readonly-contract.cjs --mode=root|nonroot',
|
||||
);
|
||||
}
|
||||
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
||||
if (mode === 'root' ? uid !== 0 : uid === null || uid === 0) {
|
||||
throw new Error(`Owner read-only actor identity is invalid for ${mode}`);
|
||||
}
|
||||
const temporaryProbe = fs.mkdtempSync('/tmp/ql3-owner-readonly-');
|
||||
fs.rmSync(temporaryProbe, { recursive: true });
|
||||
assertWriteRejected(`/ql3-owner-readonly-root-${process.pid}`);
|
||||
assertWriteRejected(
|
||||
path.join(repositoryRoot, `.ql3-owner-readonly-workspace-${process.pid}`),
|
||||
);
|
||||
}
|
||||
|
||||
function main(argv = process.argv.slice(2)) {
|
||||
const modeArgument = argv.find((value) => value.startsWith('--mode='));
|
||||
const mode = modeArgument?.slice('--mode='.length);
|
||||
assertContainerBoundary(mode);
|
||||
const plan = collectReadonlyOwnerTests();
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
['--test', '--test-concurrency=1', ...plan.tests],
|
||||
{ cwd: repositoryRoot, stdio: 'inherit' },
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
if (result.signal) {
|
||||
throw new Error(`read-only Owner tests terminated by ${result.signal}`);
|
||||
}
|
||||
if (result.status !== 0) process.exitCode = result.status ?? 1;
|
||||
}
|
||||
|
||||
if (require.main === module) main();
|
||||
|
||||
module.exports = {
|
||||
HOST_BOUND_TESTS,
|
||||
collectReadonlyOwnerTests,
|
||||
};
|
||||
@@ -31,6 +31,7 @@ const KIND_NODE_IMAGE =
|
||||
const POSTGRES_IMAGE = 'postgres:18.4-bookworm';
|
||||
const POSTGRES_INDEX_DIGEST =
|
||||
'sha256:1961f96e6029a02c3812d7cb329a3b03a3ac2bb067058dec17b0f5596aca9296';
|
||||
const POSTGRES_IMAGE_REFERENCE = `${POSTGRES_IMAGE}@${POSTGRES_INDEX_DIGEST}`;
|
||||
const POSTGRES_REPOSITORY_DIGEST = `postgres@${POSTGRES_INDEX_DIGEST}`;
|
||||
const DEFAULT_ADMIN_IMAGE = 'qinglong3-cluster-admin:ql3-plugin-recovery-e2e';
|
||||
const DEFAULT_CONTROL_IMAGE =
|
||||
@@ -277,13 +278,13 @@ function buildImages(revision) {
|
||||
}
|
||||
|
||||
function ensurePostgresImage() {
|
||||
if (!imageExists(POSTGRES_IMAGE)) {
|
||||
run(DOCKER, ['pull', `${POSTGRES_IMAGE}@${POSTGRES_INDEX_DIGEST}`], {
|
||||
if (!imageExists(POSTGRES_IMAGE_REFERENCE)) {
|
||||
run(DOCKER, ['pull', POSTGRES_IMAGE_REFERENCE], {
|
||||
label: 'pull digest-pinned PostgreSQL 18.4 fixture image',
|
||||
});
|
||||
}
|
||||
const inspection = JSON.parse(
|
||||
run(DOCKER, ['image', 'inspect', POSTGRES_IMAGE], {
|
||||
run(DOCKER, ['image', 'inspect', POSTGRES_IMAGE_REFERENCE], {
|
||||
capture: true,
|
||||
quiet: true,
|
||||
}).stdout,
|
||||
@@ -485,7 +486,7 @@ createdb --username "$POSTGRES_USER" --owner ql3_migration qinglong
|
||||
containers: [
|
||||
{
|
||||
name: 'postgres',
|
||||
image: POSTGRES_IMAGE,
|
||||
image: POSTGRES_IMAGE_REFERENCE,
|
||||
imagePullPolicy: 'Never',
|
||||
env: [
|
||||
{ name: 'POSTGRES_USER', value: 'postgres' },
|
||||
@@ -1726,7 +1727,11 @@ async function main(argv = process.argv.slice(2)) {
|
||||
},
|
||||
);
|
||||
created = true;
|
||||
for (const image of [ADMIN_IMAGE, CONTROL_IMAGE, POSTGRES_IMAGE]) {
|
||||
for (const image of [
|
||||
ADMIN_IMAGE,
|
||||
CONTROL_IMAGE,
|
||||
POSTGRES_IMAGE_REFERENCE,
|
||||
]) {
|
||||
kind(
|
||||
[
|
||||
'load',
|
||||
|
||||
@@ -117,7 +117,7 @@ function auditPostgresHaEvidence(report) {
|
||||
'POSTGRES_REPO_DIGESTS_MISSING',
|
||||
);
|
||||
add(
|
||||
postgres.architecture === 'x64' || postgres.architecture === 'arm64',
|
||||
postgres.architecture === 'amd64' || postgres.architecture === 'arm64',
|
||||
'POSTGRES_ARCHITECTURE_INVALID',
|
||||
);
|
||||
add(
|
||||
|
||||
@@ -81,6 +81,7 @@ const ROLE_NAMES = Object.freeze([
|
||||
'ql3_package_executor',
|
||||
'ql3_automation_manager',
|
||||
'ql3_approval_manager',
|
||||
'ql3_run_manager',
|
||||
'ql3_worker_credential_manager',
|
||||
'ql3_worker_credential_executor',
|
||||
'ql3_worker_ingress',
|
||||
|
||||
Reference in New Issue
Block a user