mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): bind package secrets to generations
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
PluginPackageSecretBindingConflictError,
|
||||
PluginPackageSecretBindingUnavailableError,
|
||||
createPluginPackageSecretBinding,
|
||||
} = require('@qinglong/runtime-core/plugin-package-secret-binding');
|
||||
const {
|
||||
createPluginPackageResourceGeneration,
|
||||
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
|
||||
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
||||
const {
|
||||
PostgresPluginPackageSecretBindingRepository,
|
||||
} = require('../dist/plugin-package/installation/pluginPackageSecretBindingRepository');
|
||||
|
||||
const MANIFEST = {
|
||||
apiVersion: 'qinglong.io/v1alpha1',
|
||||
kind: 'Package',
|
||||
metadata: {
|
||||
name: 'example-monitor',
|
||||
displayName: 'Example Monitor',
|
||||
version: '1.0.0',
|
||||
description: 'Secret binding repository fixture',
|
||||
license: 'Apache-2.0',
|
||||
},
|
||||
spec: {
|
||||
compatibility: {
|
||||
qinglong: '>=3.0.0-0 <4.0.0',
|
||||
architectures: ['arm64'],
|
||||
deploymentProfiles: ['cluster-control'],
|
||||
},
|
||||
runtimes: [],
|
||||
resources: {
|
||||
memory: { recommended: '32Mi' },
|
||||
disk: { install: '4Mi', working: '8Mi' },
|
||||
},
|
||||
permissions: {
|
||||
network: { allowedHosts: [] },
|
||||
secrets: [{ name: 'TOKEN', required: true }],
|
||||
tools: [],
|
||||
},
|
||||
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
|
||||
},
|
||||
};
|
||||
|
||||
function fixture(boundAtMs = 100) {
|
||||
const generation = createPluginPackageResourceGeneration({
|
||||
installationId: 'install-1',
|
||||
projectId: 'project-1',
|
||||
packageName: 'example-monitor',
|
||||
lockDigest: 'a'.repeat(64),
|
||||
generation: 1,
|
||||
previousActiveLockDigest: null,
|
||||
contentDigest: 'b'.repeat(64),
|
||||
contents: MANIFEST.spec.contents,
|
||||
});
|
||||
return createPluginPackageSecretBinding({
|
||||
generation,
|
||||
manifest: MANIFEST,
|
||||
assignments: [
|
||||
{
|
||||
name: 'TOKEN',
|
||||
secretRef: createSecretRef({
|
||||
projectId: 'project-1',
|
||||
name: 'runtime-token',
|
||||
version: 2,
|
||||
}),
|
||||
},
|
||||
],
|
||||
authority: {
|
||||
kind: 'approved-action-execution',
|
||||
evidenceDigest: 'c'.repeat(64),
|
||||
},
|
||||
boundAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
function fakePool(active = true) {
|
||||
let row;
|
||||
const queries = [];
|
||||
return {
|
||||
queries,
|
||||
pool: {
|
||||
async query(text, values = []) {
|
||||
queries.push({ text, values });
|
||||
if (text.startsWith('SELECT')) return { rows: row ? [{ ...row }] : [] };
|
||||
if (text.startsWith('INSERT')) {
|
||||
if (!active || row) return { rows: [] };
|
||||
const binding = JSON.parse(values[11]);
|
||||
row = {
|
||||
generationDigest: values[0],
|
||||
projectId: values[1],
|
||||
packageName: values[2],
|
||||
installationId: values[3],
|
||||
lockDigest: values[4],
|
||||
generation: values[5],
|
||||
manifestDigest: values[6],
|
||||
authorityKind: values[7],
|
||||
evidenceDigest: values[8],
|
||||
boundAtMs: values[9],
|
||||
bindingDigest: values[10],
|
||||
bindingJson: binding,
|
||||
};
|
||||
return { rows: [{ generation_digest: values[0] }] };
|
||||
}
|
||||
throw new Error(`unexpected SQL: ${text}`);
|
||||
},
|
||||
},
|
||||
corrupt() {
|
||||
row.bindingJson.boundAtMs = 999;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('publishes, exact-replays and finds one binding', async () => {
|
||||
const value = fakePool();
|
||||
const repository = new PostgresPluginPackageSecretBindingRepository(
|
||||
value.pool,
|
||||
);
|
||||
const binding = fixture();
|
||||
assert.equal((await repository.publish(binding)).status, 'created');
|
||||
assert.equal((await repository.publish(binding)).status, 'existing');
|
||||
assert.deepEqual(
|
||||
await repository.find(binding.target.generationDigest),
|
||||
binding,
|
||||
);
|
||||
assert.match(
|
||||
value.queries.find(({ text }) => text.startsWith('INSERT')).text,
|
||||
/install\.state = 'active'/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects inactive targets and conflicting content', async () => {
|
||||
const inactive = fakePool(false);
|
||||
await assert.rejects(
|
||||
new PostgresPluginPackageSecretBindingRepository(inactive.pool).publish(
|
||||
fixture(),
|
||||
),
|
||||
PluginPackageSecretBindingConflictError,
|
||||
);
|
||||
const active = fakePool();
|
||||
const repository = new PostgresPluginPackageSecretBindingRepository(
|
||||
active.pool,
|
||||
);
|
||||
await repository.publish(fixture());
|
||||
await assert.rejects(
|
||||
repository.publish(fixture(101)),
|
||||
PluginPackageSecretBindingConflictError,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails closed on corrupted JSON and maps PostgreSQL constraints', async () => {
|
||||
const value = fakePool();
|
||||
const repository = new PostgresPluginPackageSecretBindingRepository(
|
||||
value.pool,
|
||||
);
|
||||
const binding = fixture();
|
||||
await repository.publish(binding);
|
||||
value.corrupt();
|
||||
await assert.rejects(
|
||||
repository.find(binding.target.generationDigest),
|
||||
PluginPackageSecretBindingUnavailableError,
|
||||
);
|
||||
const failing = new PostgresPluginPackageSecretBindingRepository({
|
||||
async query() {
|
||||
const error = new Error('duplicate');
|
||||
error.code = '23505';
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
failing.find('a'.repeat(64)),
|
||||
PluginPackageSecretBindingConflictError,
|
||||
);
|
||||
});
|
||||
|
||||
test('publishes storage through package-executor and explicit subpath', () => {
|
||||
assert.equal(
|
||||
require('@qinglong/cluster-postgres/plugin-package-secret-binding')
|
||||
.PostgresPluginPackageSecretBindingRepository,
|
||||
PostgresPluginPackageSecretBindingRepository,
|
||||
);
|
||||
assert.equal(
|
||||
require('../dist/entrypoints/packageExecutor')
|
||||
.PostgresPluginPackageSecretBindingRepository,
|
||||
PostgresPluginPackageSecretBindingRepository,
|
||||
);
|
||||
});
|
||||
@@ -109,6 +109,7 @@ test('defines the immutable PostgreSQL capability and Run core stream', async ()
|
||||
'pg-0056-run-management-boundary',
|
||||
'pg-0057-run-management-stop-boundary',
|
||||
'pg-0058-plugin-package-automation-disposition-events',
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
],
|
||||
);
|
||||
for (const migration of postgresqlMainMigrationStream.migrations) {
|
||||
@@ -160,6 +161,7 @@ test('keeps local-only and legacy tables out of the cluster baseline', async ()
|
||||
'plugin_package_install_heads',
|
||||
'plugin_package_install_mutations',
|
||||
'plugin_package_materialized_revisions',
|
||||
'plugin_package_secret_bindings',
|
||||
'plugin_package_lifecycle_events',
|
||||
'plugin_package_lifecycle_heads',
|
||||
'plugin_package_lifecycle_receipts',
|
||||
@@ -534,6 +536,11 @@ test('freezes every published PostgreSQL migration checksum', () => {
|
||||
checksum:
|
||||
'd184324909f1e450f3c1b58d422796e3869a1360df60c3f2dfe4af0bacc37471',
|
||||
},
|
||||
{
|
||||
id: 'pg-0059-plugin-package-secret-bindings',
|
||||
checksum:
|
||||
'87582d256c868bd7f5af352c4b052fdab9f3714e1e7179e35d33bfa5d62957be',
|
||||
},
|
||||
];
|
||||
assert.deepEqual(
|
||||
postgresqlMainMigrationStream.migrations.map(({ id, checksum }) => ({
|
||||
@@ -1958,3 +1965,26 @@ test('advances capability v56 with column-scoped Run stop authority', async () =
|
||||
assert.match(sql, /contract_version = 55/);
|
||||
assert.match(sql, /migration_id = 'pg-0056-run-management-boundary'/);
|
||||
});
|
||||
|
||||
test('advances capability v58 with immutable generation-bound Package Secret bindings', async () => {
|
||||
const migration = migrationById('pg-0059-plugin-package-secret-bindings');
|
||||
const statements = [];
|
||||
await migration.up({
|
||||
async query(statement) {
|
||||
statements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const sql = statements.join('\n');
|
||||
assert.match(sql, /CREATE TABLE "ql3"\."plugin_package_secret_bindings"/);
|
||||
assert.match(sql, /qinglong\/plugin-package-secret-binding@v1/);
|
||||
assert.match(sql, /GRANT SELECT, INSERT[^;]+TO ql3_package_executor/);
|
||||
assert.doesNotMatch(sql, /GRANT UPDATE|GRANT DELETE/);
|
||||
assert.match(sql, /contract_version = 58/);
|
||||
assert.match(sql, /"plugin_package_secret_binding":1/);
|
||||
assert.match(sql, /contract_version = 57/);
|
||||
assert.match(
|
||||
sql,
|
||||
/migration_id = 'pg-0058-plugin-package-automation-disposition-events'/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -74,6 +74,7 @@ function validPrivileges() {
|
||||
plugin_package_install_heads: [false, false, false, false],
|
||||
plugin_package_install_mutations: [false, false, false, false],
|
||||
plugin_package_materialized_revisions: [false, false, false, false],
|
||||
plugin_package_secret_bindings: [false, false, false, false],
|
||||
project_tool_definition_snapshots: [false, false, false, false],
|
||||
project_tool_definition_snapshot_sources: [false, false, false, false],
|
||||
plugin_package_quarantine_events: [false, false, false, false],
|
||||
@@ -192,6 +193,7 @@ function validAdminPrivileges() {
|
||||
plugin_package_install_heads: [false, false, false, false],
|
||||
plugin_package_install_mutations: [false, false, false, false],
|
||||
plugin_package_materialized_revisions: [false, false, false, false],
|
||||
plugin_package_secret_bindings: [false, false, false, false],
|
||||
project_tool_definition_snapshots: [false, false, false, false],
|
||||
project_tool_definition_snapshot_sources: [false, false, false, false],
|
||||
plugin_package_quarantine_events: [false, false, false, false],
|
||||
@@ -302,6 +304,7 @@ function packagePrivileges(kind) {
|
||||
'plugin_package_install_heads',
|
||||
'plugin_package_install_mutations',
|
||||
'plugin_package_materialized_revisions',
|
||||
'plugin_package_secret_bindings',
|
||||
'project_tool_definition_snapshots',
|
||||
'project_tool_definition_snapshot_sources',
|
||||
'plugin_package_admission_receipts',
|
||||
@@ -344,6 +347,7 @@ function packagePrivileges(kind) {
|
||||
'plugin_package_install_heads',
|
||||
'plugin_package_install_mutations',
|
||||
'plugin_package_materialized_revisions',
|
||||
'plugin_package_secret_bindings',
|
||||
'plugin_package_automation_publications',
|
||||
'plugin_package_automation_publication_heads',
|
||||
'project_tool_definition_snapshots',
|
||||
@@ -750,7 +754,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
|
||||
serverMajor: 16,
|
||||
currentUser: 'ql3_runtime',
|
||||
contractName: 'control-core',
|
||||
contractVersion: 57,
|
||||
contractVersion: 58,
|
||||
migrationIds: [
|
||||
'pg-0001-schema-capability',
|
||||
'pg-0002-run-core',
|
||||
@@ -810,6 +814,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
|
||||
'pg-0056-run-management-boundary',
|
||||
'pg-0057-run-management-stop-boundary',
|
||||
'pg-0058-plugin-package-automation-disposition-events',
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -840,10 +845,10 @@ test('accepts the exact schema and isolated least-privilege admin role', async (
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_admin');
|
||||
assert.equal(report.contractVersion, 57);
|
||||
assert.equal(report.contractVersion, 58);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0058-plugin-package-automation-disposition-events',
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -856,10 +861,10 @@ test('accepts the isolated least-privilege automation manager role', async () =>
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_automation_manager');
|
||||
assert.equal(report.contractVersion, 57);
|
||||
assert.equal(report.contractVersion, 58);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0058-plugin-package-automation-disposition-events',
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
);
|
||||
|
||||
const widened = automationManagerPrivileges();
|
||||
@@ -888,10 +893,10 @@ test('accepts the isolated least-privilege human Approval manager role', async (
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_approval_manager');
|
||||
assert.equal(report.contractVersion, 57);
|
||||
assert.equal(report.contractVersion, 58);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0058-plugin-package-automation-disposition-events',
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
);
|
||||
|
||||
const widened = approvalManagerPrivileges();
|
||||
@@ -922,10 +927,10 @@ test('accepts the isolated least-privilege Run manager role', async () => {
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_run_manager');
|
||||
assert.equal(report.contractVersion, 57);
|
||||
assert.equal(report.contractVersion, 58);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0058-plugin-package-automation-disposition-events',
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
);
|
||||
|
||||
const widened = runManagerPrivileges();
|
||||
@@ -1057,10 +1062,10 @@ test('accepts the exact schema and isolated Worker ingress role', async () => {
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_worker_ingress');
|
||||
assert.equal(report.contractVersion, 57);
|
||||
assert.equal(report.contractVersion, 58);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0058-plugin-package-automation-disposition-events',
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user