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:
@@ -140,6 +140,11 @@
|
||||
"require": "./dist/plugin-package/installation/pluginPackageMaterializedRevisionRepository.js",
|
||||
"default": "./dist/plugin-package/installation/pluginPackageMaterializedRevisionRepository.js"
|
||||
},
|
||||
"./plugin-package-secret-binding": {
|
||||
"types": "./dist/plugin-package/installation/pluginPackageSecretBindingRepository.d.ts",
|
||||
"require": "./dist/plugin-package/installation/pluginPackageSecretBindingRepository.js",
|
||||
"default": "./dist/plugin-package/installation/pluginPackageSecretBindingRepository.js"
|
||||
},
|
||||
"./plugin-package-automation-publication": {
|
||||
"types": "./dist/plugin-package/publication/pluginPackageAutomationPublicationRepository.d.ts",
|
||||
"require": "./dist/plugin-package/publication/pluginPackageAutomationPublicationRepository.js",
|
||||
|
||||
@@ -40,6 +40,7 @@ export {
|
||||
} from '../schema/schemaReadiness';
|
||||
|
||||
export { PostgresPluginPackageMaterializedRevisionRepository } from '../plugin-package/installation/pluginPackageMaterializedRevisionRepository';
|
||||
export { PostgresPluginPackageSecretBindingRepository } from '../plugin-package/installation/pluginPackageSecretBindingRepository';
|
||||
export { PostgresPluginPackageAutomationPublicationRepository } from '../plugin-package/publication/pluginPackageAutomationPublicationRepository';
|
||||
export {
|
||||
CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT,
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
pluginPackageIdentityKeysetLedger,
|
||||
pluginPackageManagementQuotaBuckets,
|
||||
pluginPackageMaterializedRevisions,
|
||||
pluginPackageSecretBindings,
|
||||
pluginPackageQuarantineEvents,
|
||||
pluginPackageWorkflowAdmissions,
|
||||
pluginPackageWorkflowAdmissionSteps,
|
||||
|
||||
@@ -298,5 +298,10 @@ export const postgresqlMainMigrationManifest: MigrationStreamManifest =
|
||||
checksum:
|
||||
'd184324909f1e450f3c1b58d422796e3869a1360df60c3f2dfe4af0bacc37471',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0059-plugin-package-secret-bindings',
|
||||
checksum:
|
||||
'87582d256c868bd7f5af352c4b052fdab9f3714e1e7179e35d33bfa5d62957be',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -61,6 +61,7 @@ import { pg0055RunAttemptLogRetentionMigration } from './pg-0055-run-attempt-log
|
||||
import { pg0056RunManagementBoundaryMigration } from '../run-management/pg-0056-run-management-boundary';
|
||||
import { pg0057RunManagementStopBoundaryMigration } from '../run-management/pg-0057-run-management-stop-boundary';
|
||||
import { pg0058PluginPackageAutomationDispositionEventsMigration } from './pg-0058-plugin-package-automation-disposition-events';
|
||||
import { pg0059PluginPackageSecretBindingsMigration } from './pg-0059-plugin-package-secret-bindings';
|
||||
|
||||
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
|
||||
Object.freeze({
|
||||
@@ -127,5 +128,6 @@ export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMi
|
||||
pg0056RunManagementBoundaryMigration,
|
||||
pg0057RunManagementStopBoundaryMigration,
|
||||
pg0058PluginPackageAutomationDispositionEventsMigration,
|
||||
pg0059PluginPackageSecretBindingsMigration,
|
||||
]),
|
||||
});
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { CAPABILITIES_V57 } from './pg-0058-plugin-package-automation-disposition-events';
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V58 = CAPABILITIES_V57.replace(
|
||||
'"plugin_package_task_reconciliation":1,',
|
||||
'"plugin_package_secret_binding":1,"plugin_package_task_reconciliation":1,',
|
||||
);
|
||||
|
||||
export const pg0059PluginPackageSecretBindingsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0059-plugin-package-secret-bindings',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_secret_bindings" (
|
||||
generation_digest char(64) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
installation_id varchar(128) NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
generation integer NOT NULL,
|
||||
manifest_digest char(64) NOT NULL,
|
||||
authority_kind varchar(32) NOT NULL,
|
||||
evidence_digest char(64) NOT NULL,
|
||||
bound_at_ms bigint NOT NULL,
|
||||
binding_digest char(64) NOT NULL,
|
||||
binding_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_secret_binding_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_secret_binding_install_fk
|
||||
FOREIGN KEY (installation_id)
|
||||
REFERENCES "ql3"."plugin_package_installs" (installation_id)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_secret_binding_identity_check CHECK (
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
|
||||
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
generation BETWEEN 1 AND 2147483647 AND
|
||||
authority_kind IN ('approved-action-execution','local-owner-confirmation') AND
|
||||
bound_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_secret_binding_digest_check CHECK (
|
||||
generation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
lock_digest ~ '^[0-9a-f]{64}$' AND
|
||||
manifest_digest ~ '^[0-9a-f]{64}$' AND
|
||||
evidence_digest ~ '^[0-9a-f]{64}$' AND
|
||||
binding_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_secret_binding_json_check CHECK (
|
||||
jsonb_typeof(binding_json) = 'object' AND
|
||||
octet_length(binding_json::text) BETWEEN 2 AND 65536 AND
|
||||
binding_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-secret-binding@v1',
|
||||
'target', jsonb_build_object(
|
||||
'generationDigest', generation_digest,
|
||||
'projectId', project_id,
|
||||
'packageName', package_name,
|
||||
'installationId', installation_id,
|
||||
'lockDigest', lock_digest,
|
||||
'generation', generation,
|
||||
'manifestDigest', manifest_digest
|
||||
),
|
||||
'authority', jsonb_build_object(
|
||||
'kind', authority_kind,
|
||||
'evidenceDigest', evidence_digest
|
||||
),
|
||||
'boundAtMs', bound_at_ms,
|
||||
'bindingDigest', binding_digest
|
||||
) AND
|
||||
jsonb_typeof(binding_json -> 'entries') = 'array' AND
|
||||
jsonb_array_length(binding_json -> 'entries') BETWEEN 1 AND 64
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_secret_binding_generation_uidx ON "ql3"."plugin_package_secret_bindings" (project_id, package_name, generation)`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_secret_binding_digest_uidx ON "ql3"."plugin_package_secret_bindings" (binding_digest)`,
|
||||
`CREATE INDEX ql3_plugin_package_secret_binding_install_idx ON "ql3"."plugin_package_secret_bindings" (installation_id, generation_digest)`,
|
||||
`REVOKE ALL ON "ql3"."plugin_package_secret_bindings" FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."plugin_package_secret_bindings" TO ql3_package_executor`,
|
||||
`DO $ql3$ BEGIN UPDATE "ql3"."schema_capabilities" SET contract_version = 58, migration_id = 'pg-0059-plugin-package-secret-bindings', capabilities = '${CAPABILITIES_V58}'::jsonb, updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint WHERE contract_name = 'control-core' AND contract_version = 57 AND migration_id = 'pg-0058-plugin-package-automation-disposition-events' AND capabilities = '${CAPABILITIES_V57}'::jsonb; IF NOT FOUND THEN RAISE EXCEPTION 'control-core capability is not at version 57' USING ERRCODE = 'check_violation'; END IF; END $ql3$`,
|
||||
],
|
||||
});
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
import type { PostgresPool } from '@qinglong/runtime-core';
|
||||
import {
|
||||
InvalidPluginPackageSecretBindingError,
|
||||
MAX_PLUGIN_PACKAGE_SECRET_BINDING_JSON_BYTES,
|
||||
PluginPackageSecretBindingConflictError,
|
||||
PluginPackageSecretBindingUnavailableError,
|
||||
normalizePluginPackageSecretBinding,
|
||||
type PluginPackageSecretBinding,
|
||||
type PluginPackageSecretBindingRepository,
|
||||
} from '@qinglong/runtime-core/plugin-package-secret-binding';
|
||||
|
||||
import {
|
||||
postgresRequiredInteger,
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
postgresSqlState,
|
||||
} from '../../repository/definitionRepositorySupport';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
const DIGEST = /^[0-9a-f]{64}$/;
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidPluginPackageSecretBindingError(message);
|
||||
}
|
||||
|
||||
function unavailable(): PluginPackageSecretBindingUnavailableError {
|
||||
return new PluginPackageSecretBindingUnavailableError();
|
||||
}
|
||||
|
||||
function generationDigest(value: unknown): string {
|
||||
if (typeof value !== 'string' || !DIGEST.test(value)) {
|
||||
return invalid('generation digest is invalid');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function serialize(binding: Readonly<PluginPackageSecretBinding>): string {
|
||||
const value = JSON.stringify(binding);
|
||||
if (
|
||||
Buffer.byteLength(value, 'utf8') >
|
||||
MAX_PLUGIN_PACKAGE_SECRET_BINDING_JSON_BYTES
|
||||
) {
|
||||
return invalid('durable JSON byte budget exceeded');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function mapStorageError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof InvalidPluginPackageSecretBindingError ||
|
||||
error instanceof PluginPackageSecretBindingConflictError ||
|
||||
error instanceof PluginPackageSecretBindingUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
const state = postgresSqlState(error);
|
||||
if (state === '23503' || state === '23505' || state === '23514') {
|
||||
return new PluginPackageSecretBindingConflictError(
|
||||
'durable binding identity is already bound',
|
||||
);
|
||||
}
|
||||
return new PluginPackageSecretBindingUnavailableError({
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export class PostgresPluginPackageSecretBindingRepository
|
||||
implements PluginPackageSecretBindingRepository
|
||||
{
|
||||
constructor(private readonly pool: Pick<PostgresPool, 'query'>) {
|
||||
if (!pool || typeof pool.query !== 'function') {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Plugin Package Secret binding repository options are invalid',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private parse(row: Row): Readonly<PluginPackageSecretBinding> {
|
||||
try {
|
||||
const binding = normalizePluginPackageSecretBinding(
|
||||
postgresRequiredJsonObject(row.bindingJson, unavailable),
|
||||
);
|
||||
if (
|
||||
binding.target.generationDigest !==
|
||||
postgresRequiredString(row.generationDigest, unavailable) ||
|
||||
binding.target.projectId !==
|
||||
postgresRequiredString(row.projectId, unavailable) ||
|
||||
binding.target.packageName !==
|
||||
postgresRequiredString(row.packageName, unavailable) ||
|
||||
binding.target.installationId !==
|
||||
postgresRequiredString(row.installationId, unavailable) ||
|
||||
binding.target.lockDigest !==
|
||||
postgresRequiredString(row.lockDigest, unavailable) ||
|
||||
binding.target.generation !==
|
||||
postgresRequiredInteger(row.generation, unavailable) ||
|
||||
binding.target.manifestDigest !==
|
||||
postgresRequiredString(row.manifestDigest, unavailable) ||
|
||||
binding.authority.kind !==
|
||||
postgresRequiredString(row.authorityKind, unavailable) ||
|
||||
binding.authority.evidenceDigest !==
|
||||
postgresRequiredString(row.evidenceDigest, unavailable) ||
|
||||
binding.boundAtMs !==
|
||||
postgresRequiredInteger(row.boundAtMs, unavailable) ||
|
||||
binding.bindingDigest !==
|
||||
postgresRequiredString(row.bindingDigest, unavailable)
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return binding;
|
||||
} catch (error) {
|
||||
if (error instanceof PluginPackageSecretBindingUnavailableError) {
|
||||
throw error;
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
private async findStored(
|
||||
digest: string,
|
||||
): Promise<Readonly<PluginPackageSecretBinding> | null> {
|
||||
const result = await this.pool.query<Row>(
|
||||
`SELECT generation_digest AS "generationDigest",
|
||||
project_id AS "projectId",
|
||||
package_name AS "packageName",
|
||||
installation_id AS "installationId",
|
||||
lock_digest AS "lockDigest",
|
||||
generation,
|
||||
manifest_digest AS "manifestDigest",
|
||||
authority_kind AS "authorityKind",
|
||||
evidence_digest AS "evidenceDigest",
|
||||
bound_at_ms AS "boundAtMs",
|
||||
binding_digest AS "bindingDigest",
|
||||
binding_json AS "bindingJson"
|
||||
FROM "ql3"."plugin_package_secret_bindings"
|
||||
WHERE generation_digest = $1
|
||||
LIMIT 2`,
|
||||
[digest],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
return this.parse(result.rows[0]!);
|
||||
}
|
||||
|
||||
async find(
|
||||
digest: string,
|
||||
): Promise<Readonly<PluginPackageSecretBinding> | null> {
|
||||
try {
|
||||
return await this.findStored(generationDigest(digest));
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async publish(value: Readonly<PluginPackageSecretBinding>): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
binding: Readonly<PluginPackageSecretBinding>;
|
||||
}>
|
||||
> {
|
||||
const binding = normalizePluginPackageSecretBinding(value);
|
||||
const bindingJson = serialize(binding);
|
||||
try {
|
||||
const existing = await this.findStored(binding.target.generationDigest);
|
||||
if (existing) {
|
||||
if (JSON.stringify(existing) !== bindingJson) {
|
||||
throw new PluginPackageSecretBindingConflictError(
|
||||
'generation digest is bound to another Secret mapping',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
binding: existing,
|
||||
});
|
||||
}
|
||||
|
||||
const inserted = await this.pool.query(
|
||||
`INSERT INTO "ql3"."plugin_package_secret_bindings" (
|
||||
generation_digest, project_id, package_name, installation_id,
|
||||
lock_digest, generation, manifest_digest, authority_kind,
|
||||
evidence_digest, bound_at_ms, binding_digest, binding_json
|
||||
)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::jsonb
|
||||
FROM "ql3"."plugin_package_installs" AS install
|
||||
INNER JOIN "ql3"."plugin_package_install_heads" AS head
|
||||
ON head.installation_id = install.installation_id
|
||||
AND head.project_id = install.project_id
|
||||
AND head.package_name = install.package_name
|
||||
WHERE install.installation_id = $4
|
||||
AND install.project_id = $2
|
||||
AND install.package_name = $3
|
||||
AND install.lock_digest = $5
|
||||
AND install.active_lock_digest = $5
|
||||
AND install.target_generation = $6
|
||||
AND install.state = 'active'
|
||||
AND install.lock_json ->> 'manifestDigest' = $7
|
||||
ON CONFLICT (generation_digest) DO NOTHING
|
||||
RETURNING generation_digest`,
|
||||
[
|
||||
binding.target.generationDigest,
|
||||
binding.target.projectId,
|
||||
binding.target.packageName,
|
||||
binding.target.installationId,
|
||||
binding.target.lockDigest,
|
||||
binding.target.generation,
|
||||
binding.target.manifestDigest,
|
||||
binding.authority.kind,
|
||||
binding.authority.evidenceDigest,
|
||||
binding.boundAtMs,
|
||||
binding.bindingDigest,
|
||||
bindingJson,
|
||||
],
|
||||
);
|
||||
const stored = await this.findStored(binding.target.generationDigest);
|
||||
if (!stored) {
|
||||
throw new PluginPackageSecretBindingConflictError(
|
||||
'binding target is not the current active Package generation',
|
||||
);
|
||||
}
|
||||
if (JSON.stringify(stored) !== bindingJson) {
|
||||
throw new PluginPackageSecretBindingConflictError(
|
||||
'generation digest is bound to another Secret mapping',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
status:
|
||||
inserted.rows.length === 1
|
||||
? ('created' as const)
|
||||
: ('existing' as const),
|
||||
binding: stored,
|
||||
});
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,6 +326,66 @@ export const pluginPackageMaterializedRevisions = ql3Schema.table(
|
||||
],
|
||||
);
|
||||
|
||||
export const pluginPackageSecretBindings = ql3Schema.table(
|
||||
'plugin_package_secret_bindings',
|
||||
{
|
||||
generationDigest: char('generation_digest', { length: 64 }).primaryKey(),
|
||||
projectId: varchar('project_id', { length: 128 }).notNull(),
|
||||
packageName: varchar('package_name', { length: 63 }).notNull(),
|
||||
installationId: varchar('installation_id', { length: 128 }).notNull(),
|
||||
lockDigest: char('lock_digest', { length: 64 }).notNull(),
|
||||
generation: integer('generation').notNull(),
|
||||
manifestDigest: char('manifest_digest', { length: 64 }).notNull(),
|
||||
authorityKind: varchar('authority_kind', { length: 32 }).notNull(),
|
||||
evidenceDigest: char('evidence_digest', { length: 64 }).notNull(),
|
||||
boundAtMs: bigint('bound_at_ms', { mode: 'number' }).notNull(),
|
||||
bindingDigest: char('binding_digest', { length: 64 }).notNull(),
|
||||
bindingJson: jsonb('binding_json')
|
||||
.$type<Record<string, unknown>>()
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
name: 'ql3_plugin_package_secret_binding_project_fk',
|
||||
columns: [table.projectId],
|
||||
foreignColumns: [projects.id],
|
||||
})
|
||||
.onDelete('restrict')
|
||||
.onUpdate('restrict'),
|
||||
foreignKey({
|
||||
name: 'ql3_plugin_package_secret_binding_install_fk',
|
||||
columns: [table.installationId],
|
||||
foreignColumns: [pluginPackageInstalls.installationId],
|
||||
})
|
||||
.onDelete('restrict')
|
||||
.onUpdate('restrict'),
|
||||
check(
|
||||
'ql3_plugin_package_secret_binding_identity_check',
|
||||
sql`${table.projectId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.packageName} ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' and ${table.installationId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.generation} between 1 and 2147483647 and ${table.authorityKind} in ('approved-action-execution','local-owner-confirmation') and ${table.boundAtMs} >= 0`,
|
||||
),
|
||||
check(
|
||||
'ql3_plugin_package_secret_binding_digest_check',
|
||||
sql`${table.generationDigest} ~ '^[0-9a-f]{64}$' and ${table.lockDigest} ~ '^[0-9a-f]{64}$' and ${table.manifestDigest} ~ '^[0-9a-f]{64}$' and ${table.evidenceDigest} ~ '^[0-9a-f]{64}$' and ${table.bindingDigest} ~ '^[0-9a-f]{64}$'`,
|
||||
),
|
||||
check(
|
||||
'ql3_plugin_package_secret_binding_json_check',
|
||||
sql`jsonb_typeof(${table.bindingJson}) = 'object' and octet_length(${table.bindingJson}::text) between 2 and 65536 and ${table.bindingJson} @> jsonb_build_object('schema', 'qinglong/plugin-package-secret-binding@v1', 'target', jsonb_build_object('generationDigest', ${table.generationDigest}, 'projectId', ${table.projectId}, 'packageName', ${table.packageName}, 'installationId', ${table.installationId}, 'lockDigest', ${table.lockDigest}, 'generation', ${table.generation}, 'manifestDigest', ${table.manifestDigest}), 'authority', jsonb_build_object('kind', ${table.authorityKind}, 'evidenceDigest', ${table.evidenceDigest}), 'boundAtMs', ${table.boundAtMs}, 'bindingDigest', ${table.bindingDigest}) and jsonb_typeof(${table.bindingJson} -> 'entries') = 'array' and jsonb_array_length(${table.bindingJson} -> 'entries') between 1 and 64`,
|
||||
),
|
||||
uniqueIndex('ql3_plugin_package_secret_binding_generation_uidx').on(
|
||||
table.projectId,
|
||||
table.packageName,
|
||||
table.generation,
|
||||
),
|
||||
uniqueIndex('ql3_plugin_package_secret_binding_digest_uidx').on(
|
||||
table.bindingDigest,
|
||||
),
|
||||
index('ql3_plugin_package_secret_binding_install_idx').on(
|
||||
table.installationId,
|
||||
table.generationDigest,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const projectToolDefinitionSnapshots = ql3Schema.table(
|
||||
'project_tool_definition_snapshots',
|
||||
{
|
||||
@@ -5812,6 +5872,7 @@ export const ql3PostgresTables = [
|
||||
pluginPackageInstallHeads,
|
||||
pluginPackageInstallMutations,
|
||||
pluginPackageMaterializedRevisions,
|
||||
pluginPackageSecretBindings,
|
||||
projectToolDefinitionSnapshots,
|
||||
projectToolDefinitionSnapshotSources,
|
||||
pluginPackageQuarantineEvents,
|
||||
|
||||
@@ -15,8 +15,8 @@ export interface PostgresSchemaContractFunction {
|
||||
export interface PostgresSchemaContract {
|
||||
readonly schema: 'ql3';
|
||||
readonly contractName: 'control-core';
|
||||
readonly contractVersion: 57;
|
||||
readonly migrationId: 'pg-0058-plugin-package-automation-disposition-events';
|
||||
readonly contractVersion: 58;
|
||||
readonly migrationId: 'pg-0059-plugin-package-secret-bindings';
|
||||
readonly minimumServerMajor: 16;
|
||||
readonly maximumServerMajor: 18;
|
||||
readonly capabilities: Readonly<{
|
||||
@@ -55,6 +55,7 @@ export interface PostgresSchemaContract {
|
||||
plugin_package_lifecycle_plan: 1;
|
||||
plugin_package_management_quota: 1;
|
||||
plugin_package_materialized_revision: 1;
|
||||
plugin_package_secret_binding: 1;
|
||||
plugin_package_proposal: 1;
|
||||
plugin_package_publisher_provenance: 1;
|
||||
plugin_package_publisher_trust_authority: 1;
|
||||
@@ -104,8 +105,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
Object.freeze({
|
||||
schema: 'ql3',
|
||||
contractName: 'control-core',
|
||||
contractVersion: 57,
|
||||
migrationId: 'pg-0058-plugin-package-automation-disposition-events',
|
||||
contractVersion: 58,
|
||||
migrationId: 'pg-0059-plugin-package-secret-bindings',
|
||||
minimumServerMajor: 16,
|
||||
maximumServerMajor: 18,
|
||||
capabilities: Object.freeze({
|
||||
@@ -137,6 +138,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
plugin_package_lifecycle_plan: 1,
|
||||
plugin_package_management_quota: 1,
|
||||
plugin_package_materialized_revision: 1,
|
||||
plugin_package_secret_binding: 1,
|
||||
plugin_package_proposal: 1,
|
||||
plugin_package_publisher_provenance: 1,
|
||||
plugin_package_publisher_trust_authority: 1,
|
||||
@@ -242,6 +244,20 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'revision_json',
|
||||
'created_at_ms',
|
||||
]),
|
||||
table('plugin_package_secret_bindings', [
|
||||
'generation_digest',
|
||||
'project_id',
|
||||
'package_name',
|
||||
'installation_id',
|
||||
'lock_digest',
|
||||
'generation',
|
||||
'manifest_digest',
|
||||
'authority_kind',
|
||||
'evidence_digest',
|
||||
'bound_at_ms',
|
||||
'binding_digest',
|
||||
'binding_json',
|
||||
]),
|
||||
table('project_tool_definition_snapshots', [
|
||||
'project_id',
|
||||
'active_vector_digest',
|
||||
@@ -1417,6 +1433,10 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'ql3_plugin_package_materialized_revision_generation_uidx',
|
||||
'ql3_plugin_package_materialized_revision_lock_idx',
|
||||
'ql3_plugin_package_materialized_revision_snapshot_source_uidx',
|
||||
'plugin_package_secret_bindings_pkey',
|
||||
'ql3_plugin_package_secret_binding_generation_uidx',
|
||||
'ql3_plugin_package_secret_binding_digest_uidx',
|
||||
'ql3_plugin_package_secret_binding_install_idx',
|
||||
'project_tool_definition_snapshots_pkey',
|
||||
'ql3_project_tool_snapshot_withdrawal_key',
|
||||
'ql3_project_tool_definition_snapshot_digest_uidx',
|
||||
@@ -1708,6 +1728,9 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'ql3_plugin_package_installs_version_check',
|
||||
'ql3_plugin_package_installs_digest_check',
|
||||
'ql3_plugin_package_installs_record_check',
|
||||
'ql3_plugin_package_secret_binding_identity_check',
|
||||
'ql3_plugin_package_secret_binding_digest_check',
|
||||
'ql3_plugin_package_secret_binding_json_check',
|
||||
'ql3_plugin_package_quarantine_identity_check',
|
||||
'ql3_plugin_package_quarantine_state_check',
|
||||
'ql3_plugin_package_quarantine_subject_check',
|
||||
@@ -2161,6 +2184,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'ql3_plugin_package_install_heads_install_fk',
|
||||
'ql3_plugin_package_install_mutations_install_fk',
|
||||
'ql3_plugin_package_materialized_revision_project_fk',
|
||||
'ql3_plugin_package_secret_binding_project_fk',
|
||||
'ql3_plugin_package_secret_binding_install_fk',
|
||||
'ql3_project_tool_definition_snapshot_project_fk',
|
||||
'ql3_project_tool_definition_snapshot_source_snapshot_fk',
|
||||
'ql3_project_tool_definition_snapshot_source_install_fk',
|
||||
|
||||
@@ -167,6 +167,12 @@ const REQUIRED_RUNTIME_PRIVILEGES = Object.freeze({
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
plugin_package_secret_bindings: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
project_tool_definition_snapshots: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
@@ -680,6 +686,12 @@ const REQUIRED_ADMIN_PRIVILEGES = Object.freeze({
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
plugin_package_secret_bindings: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
project_tool_definition_snapshots: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
@@ -1230,6 +1242,7 @@ const REQUIRED_PACKAGE_EXECUTOR_PRIVILEGES: RequiredPrivileges = Object.freeze(
|
||||
name === 'approved_action_dispatches' ||
|
||||
name === 'plugin_package_install_mutations' ||
|
||||
name === 'plugin_package_materialized_revisions' ||
|
||||
name === 'plugin_package_secret_bindings' ||
|
||||
name === 'project_tool_definition_snapshots' ||
|
||||
name === 'project_tool_definition_snapshot_sources' ||
|
||||
name === 'plugin_package_lifecycle_plans' ||
|
||||
|
||||
@@ -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