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,208 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
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 {
|
||||
LocalSqlitePluginPackageSecretBindingRepository,
|
||||
} = require('../dist/plugin-package/pluginPackageSecretBindingRepository');
|
||||
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
|
||||
|
||||
const LOCK_DIGEST = 'a'.repeat(64);
|
||||
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: ['edge'],
|
||||
},
|
||||
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: LOCK_DIGEST,
|
||||
generation: 1,
|
||||
previousActiveLockDigest: null,
|
||||
contentDigest: 'b'.repeat(64),
|
||||
contents: MANIFEST.spec.contents,
|
||||
});
|
||||
const binding = createPluginPackageSecretBinding({
|
||||
generation,
|
||||
manifest: MANIFEST,
|
||||
assignments: [
|
||||
{
|
||||
name: 'TOKEN',
|
||||
secretRef: createSecretRef({
|
||||
projectId: 'project-1',
|
||||
name: 'runtime-token',
|
||||
version: 2,
|
||||
}),
|
||||
},
|
||||
],
|
||||
authority: {
|
||||
kind: 'local-owner-confirmation',
|
||||
evidenceDigest: 'c'.repeat(64),
|
||||
},
|
||||
boundAtMs,
|
||||
});
|
||||
return { binding, generation };
|
||||
}
|
||||
|
||||
async function harness(active = true) {
|
||||
const client = new DatabaseSync(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
await migrateLocalSqliteDatabase(client);
|
||||
const { binding, generation } = fixture();
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3Projects"
|
||||
(id, name, slug, status, version, created_at_ms, updated_at_ms)
|
||||
VALUES ('project-1', 'Project 1', 'project-1', 'active', 1, 1, 1)`,
|
||||
)
|
||||
.run();
|
||||
const recordDigest = 'd'.repeat(64);
|
||||
const lockJson = JSON.stringify({
|
||||
lockDigest: LOCK_DIGEST,
|
||||
projectId: 'project-1',
|
||||
packageName: 'example-monitor',
|
||||
manifestDigest: binding.target.manifestDigest,
|
||||
});
|
||||
const recordJson = JSON.stringify({
|
||||
installationId: 'install-1',
|
||||
projectId: 'project-1',
|
||||
packageName: 'example-monitor',
|
||||
lockDigest: LOCK_DIGEST,
|
||||
state: active ? 'active' : 'failed',
|
||||
version: 1,
|
||||
recordDigest,
|
||||
});
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3PluginPackageInstalls" (
|
||||
installation_id, project_id, package_name, package_version,
|
||||
operation, lock_digest, target_generation,
|
||||
previous_active_lock_digest, active_lock_digest, state, version,
|
||||
last_mutation_id, last_mutation_digest, lock_json, record_json,
|
||||
record_digest, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, '1.0.0', 'install', ?, 1, NULL, ?, ?, 1,
|
||||
'mutation-1', ?, ?, ?, ?, 1, 1)`,
|
||||
)
|
||||
.run(
|
||||
'install-1',
|
||||
'project-1',
|
||||
'example-monitor',
|
||||
LOCK_DIGEST,
|
||||
active ? LOCK_DIGEST : null,
|
||||
active ? 'active' : 'failed',
|
||||
'e'.repeat(64),
|
||||
lockJson,
|
||||
recordJson,
|
||||
recordDigest,
|
||||
);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3PluginPackageInstallHeads"
|
||||
(project_id, package_name, installation_id)
|
||||
VALUES ('project-1', 'example-monitor', 'install-1')`,
|
||||
)
|
||||
.run();
|
||||
return {
|
||||
client,
|
||||
binding,
|
||||
generation,
|
||||
repository: new LocalSqlitePluginPackageSecretBindingRepository(client),
|
||||
};
|
||||
}
|
||||
|
||||
test('publishes and exact-replays one active generation binding', async (t) => {
|
||||
const value = await harness();
|
||||
t.after(() => value.client.close());
|
||||
const created = await value.repository.publish(value.binding);
|
||||
assert.equal(created.status, 'created');
|
||||
assert.deepEqual(created.binding, value.binding);
|
||||
const replay = await value.repository.publish(value.binding);
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.deepEqual(
|
||||
await value.repository.find(value.generation.generationDigest),
|
||||
value.binding,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects inactive targets and conflicting content', async (t) => {
|
||||
const inactive = await harness(false);
|
||||
t.after(() => inactive.client.close());
|
||||
await assert.rejects(
|
||||
inactive.repository.publish(inactive.binding),
|
||||
PluginPackageSecretBindingConflictError,
|
||||
);
|
||||
|
||||
const active = await harness();
|
||||
t.after(() => active.client.close());
|
||||
await active.repository.publish(active.binding);
|
||||
await assert.rejects(
|
||||
active.repository.publish(fixture(101).binding),
|
||||
PluginPackageSecretBindingConflictError,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails closed when durable binding JSON is changed in place', async (t) => {
|
||||
const value = await harness();
|
||||
t.after(() => value.client.close());
|
||||
await value.repository.publish(value.binding);
|
||||
value.client.exec('PRAGMA ignore_check_constraints = ON');
|
||||
value.client
|
||||
.prepare(
|
||||
`UPDATE "QingLong3PluginPackageSecretBindings"
|
||||
SET binding_json = json_set(binding_json, '$.boundAtMs', 999)
|
||||
WHERE generation_digest = ?`,
|
||||
)
|
||||
.run(value.generation.generationDigest);
|
||||
await assert.rejects(
|
||||
value.repository.find(value.generation.generationDigest),
|
||||
PluginPackageSecretBindingUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
test('publishes storage only through the explicit subpath', () => {
|
||||
assert.equal(
|
||||
require('@qinglong/local-sqlite/plugin-package-secret-binding')
|
||||
.LocalSqlitePluginPackageSecretBindingRepository,
|
||||
LocalSqlitePluginPackageSecretBindingRepository,
|
||||
);
|
||||
assert.equal(
|
||||
require('../dist').LocalSqlitePluginPackageSecretBindingRepository,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user