mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): atomically apply cluster legacy env migration
This commit is contained in:
@@ -70,6 +70,11 @@
|
||||
"require": "./dist/reconciliation/clusterLegacyEnvMigrationPlanRepository.js",
|
||||
"default": "./dist/reconciliation/clusterLegacyEnvMigrationPlanRepository.js"
|
||||
},
|
||||
"./cluster-legacy-env-migration-application": {
|
||||
"types": "./dist/reconciliation/clusterLegacyEnvMigrationApplicationRepository.d.ts",
|
||||
"require": "./dist/reconciliation/clusterLegacyEnvMigrationApplicationRepository.js",
|
||||
"default": "./dist/reconciliation/clusterLegacyEnvMigrationApplicationRepository.js"
|
||||
},
|
||||
"./task-start": {
|
||||
"types": "./dist/task-start/taskStartRepository.d.ts",
|
||||
"require": "./dist/task-start/taskStartRepository.js",
|
||||
|
||||
@@ -358,5 +358,10 @@ export const postgresqlMainMigrationManifest: MigrationStreamManifest =
|
||||
checksum:
|
||||
'7cd6d993f48e7bcebcd62c93571a738d5117c9bcde33b974c5ac8962e2a03fe4',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0071-cluster-legacy-env-migration-applications',
|
||||
checksum:
|
||||
'82538b5a244011a22afad7f9c6d266a8997da538bdd2bec2ee524997c3b85996',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -73,6 +73,7 @@ import { pg0067CancellationDispatchManagementMigration } from '../run-management
|
||||
import { pg0068CancellationDispatchProjectKeysetMigration } from '../run-management/pg-0068-cancellation-dispatch-project-keyset';
|
||||
import { pg0069WorkerSessionManagementObservationMigration } from '../remote-execution/pg-0069-worker-session-management-observation';
|
||||
import { pg0070ClusterLegacyEnvMigrationPlansMigration } from '../reconciliation/pg-0070-cluster-legacy-env-migration-plans';
|
||||
import { pg0071ClusterLegacyEnvMigrationApplicationsMigration } from '../reconciliation/pg-0071-cluster-legacy-env-migration-applications';
|
||||
|
||||
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
|
||||
Object.freeze({
|
||||
@@ -151,5 +152,6 @@ export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMi
|
||||
pg0068CancellationDispatchProjectKeysetMigration,
|
||||
pg0069WorkerSessionManagementObservationMigration,
|
||||
pg0070ClusterLegacyEnvMigrationPlansMigration,
|
||||
pg0071ClusterLegacyEnvMigrationApplicationsMigration,
|
||||
]),
|
||||
});
|
||||
|
||||
+1302
File diff suppressed because it is too large
Load Diff
+182
@@ -0,0 +1,182 @@
|
||||
import { definePostgresSqlMigration } from '../migrations/sqlMigration';
|
||||
import { CAPABILITIES_V69 } from './pg-0070-cluster-legacy-env-migration-plans';
|
||||
|
||||
export const CAPABILITIES_V70 = CAPABILITIES_V69.replace(
|
||||
'"cluster_legacy_env_migration_plan":1,',
|
||||
'"cluster_legacy_env_migration_application":1,"cluster_legacy_env_migration_plan":1,',
|
||||
);
|
||||
|
||||
export const pg0071ClusterLegacyEnvMigrationApplicationsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0071-cluster-legacy-env-migration-applications',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."cluster_legacy_env_migration_application_receipts" (
|
||||
application_id varchar(128) PRIMARY KEY,
|
||||
mutation_id uuid NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
plan_id varchar(128) NOT NULL,
|
||||
plan_digest char(64) NOT NULL,
|
||||
environment_bundle_ref varchar(512) NOT NULL,
|
||||
task_revision_set_digest char(64) NOT NULL,
|
||||
trigger_revision_set_digest char(64) NOT NULL,
|
||||
task_mutation_set_digest char(64) NOT NULL,
|
||||
trigger_mutation_set_digest char(64) NOT NULL,
|
||||
task_count integer NOT NULL,
|
||||
trigger_count integer NOT NULL,
|
||||
committed_at_ms bigint NOT NULL,
|
||||
receipt_digest char(64) NOT NULL,
|
||||
receipt_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_plan_fk
|
||||
FOREIGN KEY (plan_id) REFERENCES "ql3"."cluster_legacy_env_migration_plans" (plan_id)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_identity_check CHECK (
|
||||
application_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
plan_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_digest_check CHECK (
|
||||
plan_digest ~ '^[0-9a-f]{64}$' AND
|
||||
task_revision_set_digest ~ '^[0-9a-f]{64}$' AND
|
||||
trigger_revision_set_digest ~ '^[0-9a-f]{64}$' AND
|
||||
task_mutation_set_digest ~ '^[0-9a-f]{64}$' AND
|
||||
trigger_mutation_set_digest ~ '^[0-9a-f]{64}$' AND
|
||||
receipt_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_target_check CHECK (
|
||||
environment_bundle_ref ~ '^qlsecret:v1:[A-Za-z0-9_-]+$' AND
|
||||
octet_length(environment_bundle_ref) BETWEEN 14 AND 512 AND
|
||||
task_count BETWEEN 1 AND 100000 AND
|
||||
trigger_count BETWEEN 0 AND 500000 AND
|
||||
committed_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_json_check CHECK (
|
||||
jsonb_typeof(receipt_json) = 'object' AND
|
||||
octet_length(receipt_json::text) BETWEEN 2 AND 8192 AND
|
||||
receipt_json = jsonb_build_object(
|
||||
'schema', 'qinglong/cluster-legacy-env-migration-application-receipt@v1',
|
||||
'applicationId', application_id,
|
||||
'mutationId', mutation_id::text,
|
||||
'projectId', project_id,
|
||||
'planId', plan_id,
|
||||
'planDigest', plan_digest,
|
||||
'environmentBundleRef', environment_bundle_ref,
|
||||
'taskRevisionSetDigest', task_revision_set_digest,
|
||||
'triggerRevisionSetDigest', trigger_revision_set_digest,
|
||||
'taskMutationSetDigest', task_mutation_set_digest,
|
||||
'triggerMutationSetDigest', trigger_mutation_set_digest,
|
||||
'taskCount', task_count,
|
||||
'triggerCount', trigger_count,
|
||||
'committedAtMs', committed_at_ms,
|
||||
'receiptDigest', receipt_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_cluster_legacy_env_application_mutation_uidx ON "ql3"."cluster_legacy_env_migration_application_receipts" (mutation_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_cluster_legacy_env_application_project_uidx ON "ql3"."cluster_legacy_env_migration_application_receipts" (application_id, project_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_cluster_legacy_env_application_plan_uidx ON "ql3"."cluster_legacy_env_migration_application_receipts" (plan_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_cluster_legacy_env_application_digest_uidx ON "ql3"."cluster_legacy_env_migration_application_receipts" (receipt_digest)`,
|
||||
`CREATE INDEX ql3_cluster_legacy_env_application_project_idx ON "ql3"."cluster_legacy_env_migration_application_receipts" (project_id, committed_at_ms, application_id)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."cluster_legacy_env_migration_application_tasks" (
|
||||
application_id varchar(128) NOT NULL,
|
||||
ordinal integer NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
task_id varchar(128) NOT NULL,
|
||||
previous_revision integer NOT NULL,
|
||||
previous_content_digest char(64) NOT NULL,
|
||||
mutation_id uuid NOT NULL,
|
||||
revision integer NOT NULL,
|
||||
content_digest char(64) NOT NULL,
|
||||
execution_content_digest char(64),
|
||||
item_digest char(64) NOT NULL,
|
||||
CONSTRAINT cluster_legacy_env_migration_application_tasks_pkey
|
||||
PRIMARY KEY (application_id, ordinal),
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_task_receipt_fk
|
||||
FOREIGN KEY (application_id, project_id)
|
||||
REFERENCES "ql3"."cluster_legacy_env_migration_application_receipts"
|
||||
(application_id, project_id)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_task_identity_check CHECK (
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
char_length(task_id) BETWEEN 1 AND 128 AND task_id !~ '[[:cntrl:]]'
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_task_revision_check CHECK (
|
||||
ordinal BETWEEN 0 AND 99999 AND
|
||||
previous_revision BETWEEN 1 AND 2147483646 AND
|
||||
revision = previous_revision + 1
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_task_digest_check CHECK (
|
||||
previous_content_digest ~ '^[0-9a-f]{64}$' AND
|
||||
content_digest ~ '^[0-9a-f]{64}$' AND
|
||||
(execution_content_digest IS NULL OR execution_content_digest ~ '^[0-9a-f]{64}$') AND
|
||||
item_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_cluster_legacy_env_application_task_uidx ON "ql3"."cluster_legacy_env_migration_application_tasks" (application_id, task_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_cluster_legacy_env_application_task_revision_uidx ON "ql3"."cluster_legacy_env_migration_application_tasks" (application_id, project_id, task_id, revision, content_digest)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."cluster_legacy_env_migration_application_triggers" (
|
||||
application_id varchar(128) NOT NULL,
|
||||
ordinal integer NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
trigger_id varchar(128) NOT NULL,
|
||||
task_id varchar(128) NOT NULL,
|
||||
previous_revision integer NOT NULL,
|
||||
previous_content_digest char(64) NOT NULL,
|
||||
previous_task_revision integer NOT NULL,
|
||||
previous_task_content_digest char(64) NOT NULL,
|
||||
mutation_id uuid NOT NULL,
|
||||
revision integer NOT NULL,
|
||||
content_digest char(64) NOT NULL,
|
||||
task_revision integer NOT NULL,
|
||||
task_content_digest char(64) NOT NULL,
|
||||
item_digest char(64) NOT NULL,
|
||||
CONSTRAINT cluster_legacy_env_migration_application_triggers_pkey
|
||||
PRIMARY KEY (application_id, ordinal),
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_trigger_receipt_fk
|
||||
FOREIGN KEY (application_id, project_id)
|
||||
REFERENCES "ql3"."cluster_legacy_env_migration_application_receipts"
|
||||
(application_id, project_id)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_trigger_task_fk
|
||||
FOREIGN KEY (application_id, project_id, task_id, task_revision, task_content_digest)
|
||||
REFERENCES "ql3"."cluster_legacy_env_migration_application_tasks"
|
||||
(application_id, project_id, task_id, revision, content_digest)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_trigger_identity_check CHECK (
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
char_length(trigger_id) BETWEEN 1 AND 128 AND trigger_id !~ '[[:cntrl:]]' AND
|
||||
char_length(task_id) BETWEEN 1 AND 128 AND task_id !~ '[[:cntrl:]]'
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_trigger_revision_check CHECK (
|
||||
ordinal BETWEEN 0 AND 499999 AND
|
||||
previous_revision BETWEEN 1 AND 2147483646 AND
|
||||
revision = previous_revision + 1 AND
|
||||
previous_task_revision BETWEEN 1 AND 2147483647 AND
|
||||
task_revision BETWEEN 2 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_application_trigger_digest_check CHECK (
|
||||
previous_content_digest ~ '^[0-9a-f]{64}$' AND
|
||||
previous_task_content_digest ~ '^[0-9a-f]{64}$' AND
|
||||
content_digest ~ '^[0-9a-f]{64}$' AND
|
||||
task_content_digest ~ '^[0-9a-f]{64}$' AND
|
||||
item_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_cluster_legacy_env_application_trigger_uidx ON "ql3"."cluster_legacy_env_migration_application_triggers" (application_id, trigger_id)`,
|
||||
`REVOKE ALL ON "ql3"."cluster_legacy_env_migration_application_receipts" FROM PUBLIC`,
|
||||
`REVOKE ALL ON "ql3"."cluster_legacy_env_migration_application_tasks" FROM PUBLIC`,
|
||||
`REVOKE ALL ON "ql3"."cluster_legacy_env_migration_application_triggers" FROM PUBLIC`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."cluster_legacy_env_migration_application_receipts" TO ql3_automation_manager`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."cluster_legacy_env_migration_application_tasks" TO ql3_automation_manager`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."cluster_legacy_env_migration_application_triggers" TO ql3_automation_manager`,
|
||||
`DO $ql3$ BEGIN UPDATE "ql3"."schema_capabilities" SET contract_version = 70, migration_id = 'pg-0071-cluster-legacy-env-migration-applications', capabilities = '${CAPABILITIES_V70}'::jsonb, updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint WHERE contract_name = 'control-core' AND contract_version = 69 AND migration_id = 'pg-0070-cluster-legacy-env-migration-plans' AND capabilities = '${CAPABILITIES_V69}'::jsonb; IF NOT FOUND THEN RAISE EXCEPTION 'control-core capability is not at version 69' USING ERRCODE = 'check_violation'; END IF; END $ql3$`,
|
||||
],
|
||||
});
|
||||
@@ -180,6 +180,213 @@ export const clusterLegacyEnvMigrationPlans = ql3Schema.table(
|
||||
],
|
||||
);
|
||||
|
||||
export const clusterLegacyEnvMigrationApplicationReceipts = ql3Schema.table(
|
||||
'cluster_legacy_env_migration_application_receipts',
|
||||
{
|
||||
applicationId: varchar('application_id', { length: 128 }).primaryKey(),
|
||||
mutationId: uuid('mutation_id').notNull(),
|
||||
projectId: varchar('project_id', { length: 128 }).notNull(),
|
||||
planId: varchar('plan_id', { length: 128 }).notNull(),
|
||||
planDigest: char('plan_digest', { length: 64 }).notNull(),
|
||||
environmentBundleRef: varchar('environment_bundle_ref', {
|
||||
length: 512,
|
||||
}).notNull(),
|
||||
taskRevisionSetDigest: char('task_revision_set_digest', {
|
||||
length: 64,
|
||||
}).notNull(),
|
||||
triggerRevisionSetDigest: char('trigger_revision_set_digest', {
|
||||
length: 64,
|
||||
}).notNull(),
|
||||
taskMutationSetDigest: char('task_mutation_set_digest', {
|
||||
length: 64,
|
||||
}).notNull(),
|
||||
triggerMutationSetDigest: char('trigger_mutation_set_digest', {
|
||||
length: 64,
|
||||
}).notNull(),
|
||||
taskCount: integer('task_count').notNull(),
|
||||
triggerCount: integer('trigger_count').notNull(),
|
||||
committedAtMs: bigint('committed_at_ms', { mode: 'number' }).notNull(),
|
||||
receiptDigest: char('receipt_digest', { length: 64 }).notNull(),
|
||||
receiptJson: jsonb('receipt_json')
|
||||
.$type<Record<string, unknown>>()
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
name: 'ql3_cluster_legacy_env_application_project_fk',
|
||||
columns: [table.projectId],
|
||||
foreignColumns: [projects.id],
|
||||
}).onDelete('restrict'),
|
||||
foreignKey({
|
||||
name: 'ql3_cluster_legacy_env_application_plan_fk',
|
||||
columns: [table.planId],
|
||||
foreignColumns: [clusterLegacyEnvMigrationPlans.planId],
|
||||
}).onDelete('restrict'),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_application_identity_check',
|
||||
sql`${table.applicationId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.projectId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.planId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_application_digest_check',
|
||||
sql`${table.planDigest} ~ '^[0-9a-f]{64}$' and ${table.taskRevisionSetDigest} ~ '^[0-9a-f]{64}$' and ${table.triggerRevisionSetDigest} ~ '^[0-9a-f]{64}$' and ${table.taskMutationSetDigest} ~ '^[0-9a-f]{64}$' and ${table.triggerMutationSetDigest} ~ '^[0-9a-f]{64}$' and ${table.receiptDigest} ~ '^[0-9a-f]{64}$'`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_application_target_check',
|
||||
sql`${table.environmentBundleRef} ~ '^qlsecret:v1:[A-Za-z0-9_-]+$' and octet_length(${table.environmentBundleRef}) between 14 and 512 and ${table.taskCount} between 1 and 100000 and ${table.triggerCount} between 0 and 500000 and ${table.committedAtMs} >= 0`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_application_json_check',
|
||||
sql`jsonb_typeof(${table.receiptJson}) = 'object' and octet_length(${table.receiptJson}::text) between 2 and 8192 and ${table.receiptJson} = jsonb_build_object('schema', 'qinglong/cluster-legacy-env-migration-application-receipt@v1', 'applicationId', ${table.applicationId}, 'mutationId', ${table.mutationId}::text, 'projectId', ${table.projectId}, 'planId', ${table.planId}, 'planDigest', ${table.planDigest}, 'environmentBundleRef', ${table.environmentBundleRef}, 'taskRevisionSetDigest', ${table.taskRevisionSetDigest}, 'triggerRevisionSetDigest', ${table.triggerRevisionSetDigest}, 'taskMutationSetDigest', ${table.taskMutationSetDigest}, 'triggerMutationSetDigest', ${table.triggerMutationSetDigest}, 'taskCount', ${table.taskCount}, 'triggerCount', ${table.triggerCount}, 'committedAtMs', ${table.committedAtMs}, 'receiptDigest', ${table.receiptDigest})`,
|
||||
),
|
||||
uniqueIndex('ql3_cluster_legacy_env_application_mutation_uidx').on(
|
||||
table.mutationId,
|
||||
),
|
||||
uniqueIndex('ql3_cluster_legacy_env_application_project_uidx').on(
|
||||
table.applicationId,
|
||||
table.projectId,
|
||||
),
|
||||
uniqueIndex('ql3_cluster_legacy_env_application_plan_uidx').on(
|
||||
table.planId,
|
||||
),
|
||||
uniqueIndex('ql3_cluster_legacy_env_application_digest_uidx').on(
|
||||
table.receiptDigest,
|
||||
),
|
||||
index('ql3_cluster_legacy_env_application_project_idx').on(
|
||||
table.projectId,
|
||||
table.committedAtMs,
|
||||
table.applicationId,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const clusterLegacyEnvMigrationApplicationTasks = ql3Schema.table(
|
||||
'cluster_legacy_env_migration_application_tasks',
|
||||
{
|
||||
applicationId: varchar('application_id', { length: 128 }).notNull(),
|
||||
ordinal: integer('ordinal').notNull(),
|
||||
projectId: varchar('project_id', { length: 128 }).notNull(),
|
||||
taskId: varchar('task_id', { length: 128 }).notNull(),
|
||||
previousRevision: integer('previous_revision').notNull(),
|
||||
previousContentDigest: char('previous_content_digest', {
|
||||
length: 64,
|
||||
}).notNull(),
|
||||
mutationId: uuid('mutation_id').notNull(),
|
||||
revision: integer('revision').notNull(),
|
||||
contentDigest: char('content_digest', { length: 64 }).notNull(),
|
||||
executionContentDigest: char('execution_content_digest', { length: 64 }),
|
||||
itemDigest: char('item_digest', { length: 64 }).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({
|
||||
name: 'cluster_legacy_env_migration_application_tasks_pkey',
|
||||
columns: [table.applicationId, table.ordinal],
|
||||
}),
|
||||
foreignKey({
|
||||
name: 'ql3_cluster_legacy_env_application_task_receipt_fk',
|
||||
columns: [table.applicationId, table.projectId],
|
||||
foreignColumns: [
|
||||
clusterLegacyEnvMigrationApplicationReceipts.applicationId,
|
||||
clusterLegacyEnvMigrationApplicationReceipts.projectId,
|
||||
],
|
||||
}).onDelete('restrict'),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_application_task_identity_check',
|
||||
sql`${table.projectId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and char_length(${table.taskId}) between 1 and 128 and ${table.taskId} !~ '[[:cntrl:]]'`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_application_task_revision_check',
|
||||
sql`${table.ordinal} between 0 and 99999 and ${table.previousRevision} between 1 and 2147483646 and ${table.revision} = ${table.previousRevision} + 1`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_application_task_digest_check',
|
||||
sql`${table.previousContentDigest} ~ '^[0-9a-f]{64}$' and ${table.contentDigest} ~ '^[0-9a-f]{64}$' and (${table.executionContentDigest} is null or ${table.executionContentDigest} ~ '^[0-9a-f]{64}$') and ${table.itemDigest} ~ '^[0-9a-f]{64}$'`,
|
||||
),
|
||||
uniqueIndex('ql3_cluster_legacy_env_application_task_uidx').on(
|
||||
table.applicationId,
|
||||
table.taskId,
|
||||
),
|
||||
uniqueIndex('ql3_cluster_legacy_env_application_task_revision_uidx').on(
|
||||
table.applicationId,
|
||||
table.projectId,
|
||||
table.taskId,
|
||||
table.revision,
|
||||
table.contentDigest,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const clusterLegacyEnvMigrationApplicationTriggers = ql3Schema.table(
|
||||
'cluster_legacy_env_migration_application_triggers',
|
||||
{
|
||||
applicationId: varchar('application_id', { length: 128 }).notNull(),
|
||||
ordinal: integer('ordinal').notNull(),
|
||||
projectId: varchar('project_id', { length: 128 }).notNull(),
|
||||
triggerId: varchar('trigger_id', { length: 128 }).notNull(),
|
||||
taskId: varchar('task_id', { length: 128 }).notNull(),
|
||||
previousRevision: integer('previous_revision').notNull(),
|
||||
previousContentDigest: char('previous_content_digest', {
|
||||
length: 64,
|
||||
}).notNull(),
|
||||
previousTaskRevision: integer('previous_task_revision').notNull(),
|
||||
previousTaskContentDigest: char('previous_task_content_digest', {
|
||||
length: 64,
|
||||
}).notNull(),
|
||||
mutationId: uuid('mutation_id').notNull(),
|
||||
revision: integer('revision').notNull(),
|
||||
contentDigest: char('content_digest', { length: 64 }).notNull(),
|
||||
taskRevision: integer('task_revision').notNull(),
|
||||
taskContentDigest: char('task_content_digest', { length: 64 }).notNull(),
|
||||
itemDigest: char('item_digest', { length: 64 }).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({
|
||||
name: 'cluster_legacy_env_migration_application_triggers_pkey',
|
||||
columns: [table.applicationId, table.ordinal],
|
||||
}),
|
||||
foreignKey({
|
||||
name: 'ql3_cluster_legacy_env_application_trigger_receipt_fk',
|
||||
columns: [table.applicationId, table.projectId],
|
||||
foreignColumns: [
|
||||
clusterLegacyEnvMigrationApplicationReceipts.applicationId,
|
||||
clusterLegacyEnvMigrationApplicationReceipts.projectId,
|
||||
],
|
||||
}).onDelete('restrict'),
|
||||
foreignKey({
|
||||
name: 'ql3_cluster_legacy_env_application_trigger_task_fk',
|
||||
columns: [
|
||||
table.applicationId,
|
||||
table.projectId,
|
||||
table.taskId,
|
||||
table.taskRevision,
|
||||
table.taskContentDigest,
|
||||
],
|
||||
foreignColumns: [
|
||||
clusterLegacyEnvMigrationApplicationTasks.applicationId,
|
||||
clusterLegacyEnvMigrationApplicationTasks.projectId,
|
||||
clusterLegacyEnvMigrationApplicationTasks.taskId,
|
||||
clusterLegacyEnvMigrationApplicationTasks.revision,
|
||||
clusterLegacyEnvMigrationApplicationTasks.contentDigest,
|
||||
],
|
||||
}).onDelete('restrict'),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_application_trigger_identity_check',
|
||||
sql`${table.projectId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and char_length(${table.triggerId}) between 1 and 128 and ${table.triggerId} !~ '[[:cntrl:]]' and char_length(${table.taskId}) between 1 and 128 and ${table.taskId} !~ '[[:cntrl:]]'`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_application_trigger_revision_check',
|
||||
sql`${table.ordinal} between 0 and 499999 and ${table.previousRevision} between 1 and 2147483646 and ${table.revision} = ${table.previousRevision} + 1 and ${table.previousTaskRevision} between 1 and 2147483647 and ${table.taskRevision} between 2 and 2147483647`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_application_trigger_digest_check',
|
||||
sql`${table.previousContentDigest} ~ '^[0-9a-f]{64}$' and ${table.previousTaskContentDigest} ~ '^[0-9a-f]{64}$' and ${table.contentDigest} ~ '^[0-9a-f]{64}$' and ${table.taskContentDigest} ~ '^[0-9a-f]{64}$' and ${table.itemDigest} ~ '^[0-9a-f]{64}$'`,
|
||||
),
|
||||
uniqueIndex('ql3_cluster_legacy_env_application_trigger_uidx').on(
|
||||
table.applicationId,
|
||||
table.triggerId,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const pluginPackageInstalls = ql3Schema.table(
|
||||
'plugin_package_installs',
|
||||
{
|
||||
@@ -6289,6 +6496,9 @@ export const ql3PostgresTables = [
|
||||
schemaCapabilities,
|
||||
projects,
|
||||
clusterLegacyEnvMigrationPlans,
|
||||
clusterLegacyEnvMigrationApplicationReceipts,
|
||||
clusterLegacyEnvMigrationApplicationTasks,
|
||||
clusterLegacyEnvMigrationApplicationTriggers,
|
||||
pluginPackageInstalls,
|
||||
pluginPackageInstallHeads,
|
||||
pluginPackageInstallMutations,
|
||||
|
||||
@@ -21,8 +21,8 @@ export interface PostgresSchemaContractTrigger {
|
||||
export interface PostgresSchemaContract {
|
||||
readonly schema: 'ql3';
|
||||
readonly contractName: 'control-core';
|
||||
readonly contractVersion: 69;
|
||||
readonly migrationId: 'pg-0070-cluster-legacy-env-migration-plans';
|
||||
readonly contractVersion: 70;
|
||||
readonly migrationId: 'pg-0071-cluster-legacy-env-migration-applications';
|
||||
readonly minimumServerMajor: 16;
|
||||
readonly maximumServerMajor: 18;
|
||||
readonly capabilities: Readonly<{
|
||||
@@ -50,6 +50,7 @@ export interface PostgresSchemaContract {
|
||||
cluster_scheduler_admission: 1;
|
||||
database_role_grants: 1;
|
||||
cluster_execution_revision: 1;
|
||||
cluster_legacy_env_migration_application: 1;
|
||||
cluster_legacy_env_migration_plan: 1;
|
||||
identity_admin: 1;
|
||||
plugin_package_admission: 1;
|
||||
@@ -123,8 +124,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
Object.freeze({
|
||||
schema: 'ql3',
|
||||
contractName: 'control-core',
|
||||
contractVersion: 69,
|
||||
migrationId: 'pg-0070-cluster-legacy-env-migration-plans',
|
||||
contractVersion: 70,
|
||||
migrationId: 'pg-0071-cluster-legacy-env-migration-applications',
|
||||
minimumServerMajor: 16,
|
||||
maximumServerMajor: 18,
|
||||
capabilities: Object.freeze({
|
||||
@@ -138,6 +139,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
automation_management_boundary: 1,
|
||||
automation_management_identity_keyset_ledger: 1,
|
||||
cluster_execution_revision: 1,
|
||||
cluster_legacy_env_migration_application: 1,
|
||||
cluster_legacy_env_migration_plan: 1,
|
||||
cluster_recovery: 1,
|
||||
cluster_recovery_claim: 1,
|
||||
@@ -251,6 +253,53 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'planned_at_ms',
|
||||
'plan_json',
|
||||
]),
|
||||
table('cluster_legacy_env_migration_application_receipts', [
|
||||
'application_id',
|
||||
'mutation_id',
|
||||
'project_id',
|
||||
'plan_id',
|
||||
'plan_digest',
|
||||
'environment_bundle_ref',
|
||||
'task_revision_set_digest',
|
||||
'trigger_revision_set_digest',
|
||||
'task_mutation_set_digest',
|
||||
'trigger_mutation_set_digest',
|
||||
'task_count',
|
||||
'trigger_count',
|
||||
'committed_at_ms',
|
||||
'receipt_digest',
|
||||
'receipt_json',
|
||||
]),
|
||||
table('cluster_legacy_env_migration_application_tasks', [
|
||||
'application_id',
|
||||
'ordinal',
|
||||
'project_id',
|
||||
'task_id',
|
||||
'previous_revision',
|
||||
'previous_content_digest',
|
||||
'mutation_id',
|
||||
'revision',
|
||||
'content_digest',
|
||||
'execution_content_digest',
|
||||
'item_digest',
|
||||
]),
|
||||
table('cluster_legacy_env_migration_application_triggers', [
|
||||
'application_id',
|
||||
'ordinal',
|
||||
'project_id',
|
||||
'trigger_id',
|
||||
'task_id',
|
||||
'previous_revision',
|
||||
'previous_content_digest',
|
||||
'previous_task_revision',
|
||||
'previous_task_content_digest',
|
||||
'mutation_id',
|
||||
'revision',
|
||||
'content_digest',
|
||||
'task_revision',
|
||||
'task_content_digest',
|
||||
'item_digest',
|
||||
]),
|
||||
table('plugin_package_installs', [
|
||||
'installation_id',
|
||||
'project_id',
|
||||
@@ -1566,6 +1615,17 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'ql3_cluster_legacy_env_plan_mutation_uidx',
|
||||
'ql3_cluster_legacy_env_plan_digest_uidx',
|
||||
'ql3_cluster_legacy_env_plan_project_idx',
|
||||
'cluster_legacy_env_migration_application_receipts_pkey',
|
||||
'ql3_cluster_legacy_env_application_mutation_uidx',
|
||||
'ql3_cluster_legacy_env_application_project_uidx',
|
||||
'ql3_cluster_legacy_env_application_plan_uidx',
|
||||
'ql3_cluster_legacy_env_application_digest_uidx',
|
||||
'ql3_cluster_legacy_env_application_project_idx',
|
||||
'cluster_legacy_env_migration_application_tasks_pkey',
|
||||
'ql3_cluster_legacy_env_application_task_uidx',
|
||||
'ql3_cluster_legacy_env_application_task_revision_uidx',
|
||||
'cluster_legacy_env_migration_application_triggers_pkey',
|
||||
'ql3_cluster_legacy_env_application_trigger_uidx',
|
||||
'plugin_package_installs_pkey',
|
||||
'ql3_plugin_package_installs_quarantine_target_key',
|
||||
'ql3_plugin_package_installs_recovery_idx',
|
||||
@@ -1896,6 +1956,16 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'ql3_cluster_legacy_env_plan_target_check',
|
||||
'ql3_cluster_legacy_env_plan_time_check',
|
||||
'ql3_cluster_legacy_env_plan_json_check',
|
||||
'ql3_cluster_legacy_env_application_identity_check',
|
||||
'ql3_cluster_legacy_env_application_digest_check',
|
||||
'ql3_cluster_legacy_env_application_target_check',
|
||||
'ql3_cluster_legacy_env_application_json_check',
|
||||
'ql3_cluster_legacy_env_application_task_identity_check',
|
||||
'ql3_cluster_legacy_env_application_task_revision_check',
|
||||
'ql3_cluster_legacy_env_application_task_digest_check',
|
||||
'ql3_cluster_legacy_env_application_trigger_identity_check',
|
||||
'ql3_cluster_legacy_env_application_trigger_revision_check',
|
||||
'ql3_cluster_legacy_env_application_trigger_digest_check',
|
||||
'ql3_plugin_package_installs_identity_check',
|
||||
'ql3_plugin_package_installs_operation_check',
|
||||
'ql3_plugin_package_installs_state_check',
|
||||
@@ -2376,6 +2446,11 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
foreignKeys: Object.freeze([
|
||||
'ql3_schema_capabilities_migration_fk',
|
||||
'ql3_cluster_legacy_env_plan_project_fk',
|
||||
'ql3_cluster_legacy_env_application_project_fk',
|
||||
'ql3_cluster_legacy_env_application_plan_fk',
|
||||
'ql3_cluster_legacy_env_application_task_receipt_fk',
|
||||
'ql3_cluster_legacy_env_application_trigger_receipt_fk',
|
||||
'ql3_cluster_legacy_env_application_trigger_task_fk',
|
||||
'ql3_plugin_package_installs_project_fk',
|
||||
'ql3_plugin_package_install_heads_project_fk',
|
||||
'ql3_plugin_package_install_heads_install_fk',
|
||||
|
||||
@@ -156,6 +156,24 @@ const REQUIRED_RUNTIME_PRIVILEGES = Object.freeze({
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
cluster_legacy_env_migration_application_receipts: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
cluster_legacy_env_migration_application_tasks: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
cluster_legacy_env_migration_application_triggers: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
plugin_package_installs: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
@@ -711,6 +729,24 @@ const REQUIRED_ADMIN_PRIVILEGES = Object.freeze({
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
cluster_legacy_env_migration_application_receipts: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
cluster_legacy_env_migration_application_tasks: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
cluster_legacy_env_migration_application_triggers: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
plugin_package_installs: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
@@ -1430,6 +1466,9 @@ const REQUIRED_AUTOMATION_MANAGER_PRIVILEGES: RequiredPrivileges =
|
||||
}
|
||||
: name === 'security_audit_events' ||
|
||||
name === 'cluster_legacy_env_migration_plans' ||
|
||||
name === 'cluster_legacy_env_migration_application_receipts' ||
|
||||
name === 'cluster_legacy_env_migration_application_tasks' ||
|
||||
name === 'cluster_legacy_env_migration_application_triggers' ||
|
||||
name === 'task_definition_revisions' ||
|
||||
name === 'task_execution_revisions' ||
|
||||
name === 'trigger_revisions'
|
||||
|
||||
@@ -74,6 +74,14 @@ const {
|
||||
const {
|
||||
PostgresClusterLegacyEnvMigrationPlanRepository,
|
||||
} = require('../dist/reconciliation/clusterLegacyEnvMigrationPlanRepository');
|
||||
const {
|
||||
PostgresClusterLegacyEnvMigrationApplicationRepository,
|
||||
} = require('../dist/reconciliation/clusterLegacyEnvMigrationApplicationRepository');
|
||||
const {
|
||||
InvalidClusterLegacyEnvMigrationApplicationError,
|
||||
createClusterLegacyEnvMigrationTaskMutationSetDigester,
|
||||
createClusterLegacyEnvMigrationTriggerMutationSetDigester,
|
||||
} = require('@qinglong/runtime-core/cluster-legacy-env-migration-application');
|
||||
|
||||
function nextMinute(schedule, afterMs) {
|
||||
if (schedule.expression !== '* * * * *' || schedule.timezone !== 'UTC') {
|
||||
@@ -666,6 +674,49 @@ async function observeContractPublisherTrust(pool) {
|
||||
});
|
||||
}
|
||||
|
||||
test('Cluster Legacy Env application rejects accessor stream factories before PostgreSQL', async () => {
|
||||
let getterCalls = 0;
|
||||
let connectCalls = 0;
|
||||
const repository = new PostgresClusterLegacyEnvMigrationApplicationRepository(
|
||||
{
|
||||
async query() {
|
||||
throw new Error('query must not be called');
|
||||
},
|
||||
async connect() {
|
||||
connectCalls += 1;
|
||||
throw new Error('connect must not be called');
|
||||
},
|
||||
},
|
||||
);
|
||||
const streams = {};
|
||||
for (const name of ['taskMutations', 'triggerMutations']) {
|
||||
Object.defineProperty(streams, name, {
|
||||
enumerable: true,
|
||||
get() {
|
||||
getterCalls += 1;
|
||||
return () => [];
|
||||
},
|
||||
});
|
||||
}
|
||||
await assert.rejects(
|
||||
repository.apply(
|
||||
{
|
||||
applicationId: 'accessor-stream-application',
|
||||
mutationId: '10000000-0000-4000-8000-000000000001',
|
||||
projectId: 'accessor-stream-project',
|
||||
planId: 'accessor-stream-plan',
|
||||
planDigest: '1'.repeat(64),
|
||||
taskMutationSetDigest: '2'.repeat(64),
|
||||
triggerMutationSetDigest: '3'.repeat(64),
|
||||
},
|
||||
streams,
|
||||
),
|
||||
InvalidClusterLegacyEnvMigrationApplicationError,
|
||||
);
|
||||
assert.equal(getterCalls, 0);
|
||||
assert.equal(connectCalls, 0);
|
||||
});
|
||||
|
||||
if (!migrationConnectionString) {
|
||||
test('PostgreSQL integration requires QL3_TEST_POSTGRES_URL', {
|
||||
skip: true,
|
||||
@@ -6685,4 +6736,310 @@ if (!migrationConnectionString) {
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('atomically applies one Legacy Env plan to Task, Trigger and schedule receipts', async () => {
|
||||
const projectId = `legacy-env-application-${process.pid}`;
|
||||
const taskId = `legacy task ${process.pid}`;
|
||||
const triggerId = `legacy trigger ${process.pid}`;
|
||||
const planId = `legacy-env-application-plan-${process.pid}`;
|
||||
const planMutationId = `legacy-env-application-plan-mutation-${process.pid}`;
|
||||
const applicationId = `legacy-env-application-receipt-${process.pid}`;
|
||||
const taskMutationId = '719f7900-0000-4000-8000-000000000001';
|
||||
const triggerMutationId = '719f7900-0000-4000-8000-000000000002';
|
||||
const applicationMutationId = '719f7900-0000-4000-8000-000000000003';
|
||||
const migrationDatabase = await open('migration');
|
||||
const automationDatabase = await open('automation-manager');
|
||||
const runtimeDatabase = await open('runtime');
|
||||
const adminDatabase = await open('admin');
|
||||
try {
|
||||
await runPostgresMigrations({ pool: migrationDatabase.pool });
|
||||
await migrationDatabase.pool.query(
|
||||
`TRUNCATE TABLE
|
||||
"ql3"."cluster_legacy_env_migration_application_triggers",
|
||||
"ql3"."cluster_legacy_env_migration_application_tasks",
|
||||
"ql3"."cluster_legacy_env_migration_application_receipts",
|
||||
"ql3"."cluster_legacy_env_migration_plans",
|
||||
"ql3"."trigger_schedules", "ql3"."triggers",
|
||||
"ql3"."task_execution_revisions", "ql3"."task_definitions"
|
||||
CASCADE`,
|
||||
);
|
||||
await migrationDatabase.pool.query(
|
||||
`INSERT INTO "ql3"."projects"
|
||||
(id, name, slug, status, version, created_at_ms, updated_at_ms)
|
||||
VALUES ($1, $1, $2, 'active', 1, 1, 1)
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
[projectId, `legacy-env-application-${process.pid}`],
|
||||
);
|
||||
const clock = await migrationDatabase.pool.query(
|
||||
`SELECT floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
|
||||
AS "observedAtMs"`,
|
||||
);
|
||||
const occurredAtMs = Number(clock.rows[0].observedAtMs) - 1000;
|
||||
const initialTask = (
|
||||
await new PostgresTaskDefinitionRepository(
|
||||
migrationDatabase.pool,
|
||||
).appendTaskDefinitionRevision({
|
||||
projectId,
|
||||
taskId,
|
||||
expectedRevision: null,
|
||||
mutationId: '719f7900-0000-4000-8000-000000000010',
|
||||
name: 'Legacy command',
|
||||
description: 'preserved by atomic migration',
|
||||
kind: 'command',
|
||||
spec: {
|
||||
schema: 'qinglong/command@v1',
|
||||
config: {
|
||||
command: {
|
||||
kind: 'argv',
|
||||
file: '/bin/echo',
|
||||
args: ['legacy'],
|
||||
},
|
||||
timeoutMs: 30_000,
|
||||
},
|
||||
},
|
||||
labels: { 'qinglong.io/source': 'legacy' },
|
||||
enabled: true,
|
||||
occurredAtMs,
|
||||
})
|
||||
).definition;
|
||||
const trigger = (
|
||||
await new PostgresTriggerRepository(
|
||||
migrationDatabase.pool,
|
||||
).appendTriggerRevision({
|
||||
projectId,
|
||||
triggerId,
|
||||
expectedRevision: null,
|
||||
mutationId: '719f7900-0000-4000-8000-000000000011',
|
||||
taskId,
|
||||
taskRevision: initialTask.revision,
|
||||
taskContentDigest: initialTask.contentDigest,
|
||||
spec: {
|
||||
schema: 'qinglong/cron@v1',
|
||||
config: {
|
||||
expression: '* * * * *',
|
||||
timezone: 'UTC',
|
||||
misfirePolicy: 'skip',
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
occurredAtMs: occurredAtMs + 1,
|
||||
})
|
||||
).trigger;
|
||||
const task = (
|
||||
await new PostgresTaskDefinitionRepository(
|
||||
migrationDatabase.pool,
|
||||
).appendTaskDefinitionRevision({
|
||||
projectId,
|
||||
taskId,
|
||||
expectedRevision: initialTask.revision,
|
||||
mutationId: '719f7900-0000-4000-8000-000000000012',
|
||||
name: initialTask.name,
|
||||
description: initialTask.description,
|
||||
kind: initialTask.kind,
|
||||
spec: initialTask.spec,
|
||||
labels: initialTask.labels,
|
||||
enabled: initialTask.enabled,
|
||||
occurredAtMs: occurredAtMs + 2,
|
||||
})
|
||||
).definition;
|
||||
|
||||
const taskMutations = [
|
||||
{
|
||||
ordinal: 0,
|
||||
taskId,
|
||||
previousRevision: task.revision,
|
||||
previousContentDigest: task.contentDigest,
|
||||
mutationId: taskMutationId,
|
||||
},
|
||||
];
|
||||
const triggerMutations = [
|
||||
{
|
||||
ordinal: 0,
|
||||
triggerId,
|
||||
taskId,
|
||||
previousRevision: trigger.revision,
|
||||
previousContentDigest: trigger.contentDigest,
|
||||
previousTaskRevision: initialTask.revision,
|
||||
previousTaskContentDigest: initialTask.contentDigest,
|
||||
mutationId: triggerMutationId,
|
||||
},
|
||||
];
|
||||
const taskDigester =
|
||||
createClusterLegacyEnvMigrationTaskMutationSetDigester();
|
||||
taskMutations.forEach((value) => taskDigester.update(value));
|
||||
const taskSet = taskDigester.finish();
|
||||
const triggerDigester =
|
||||
createClusterLegacyEnvMigrationTriggerMutationSetDigester();
|
||||
triggerMutations.forEach((value) => triggerDigester.update(value));
|
||||
const triggerSet = triggerDigester.finish();
|
||||
const secretRef = createSecretRef({
|
||||
projectId,
|
||||
name: 'legacy-env-bundle',
|
||||
version: 1,
|
||||
});
|
||||
const plan = (
|
||||
await new PostgresClusterLegacyEnvMigrationPlanRepository(
|
||||
automationDatabase.pool,
|
||||
).publish({
|
||||
planId,
|
||||
mutationId: planMutationId,
|
||||
projectId,
|
||||
source: {
|
||||
reconciliationBundleDigest: '1'.repeat(64),
|
||||
decisionDigest: '2'.repeat(64),
|
||||
candidateSetDigest: '3'.repeat(64),
|
||||
sourceRowCount: 1,
|
||||
activeRowCount: 1,
|
||||
disabledRowCount: 0,
|
||||
effectiveBindingCount: 1,
|
||||
},
|
||||
target: {
|
||||
secretRef,
|
||||
taskRevisionSetDigest: taskSet.revisionSetDigest,
|
||||
triggerRevisionSetDigest: triggerSet.revisionSetDigest,
|
||||
taskCount: taskSet.count,
|
||||
triggerCount: triggerSet.count,
|
||||
totalEffectiveBytes: 128,
|
||||
},
|
||||
})
|
||||
).plan;
|
||||
const intent = {
|
||||
applicationId,
|
||||
mutationId: applicationMutationId,
|
||||
projectId,
|
||||
planId,
|
||||
planDigest: plan.planDigest,
|
||||
taskMutationSetDigest: taskSet.mutationSetDigest,
|
||||
triggerMutationSetDigest: triggerSet.mutationSetDigest,
|
||||
};
|
||||
let taskStreamCalls = 0;
|
||||
let triggerStreamCalls = 0;
|
||||
const streams = {
|
||||
taskMutations() {
|
||||
taskStreamCalls += 1;
|
||||
return taskMutations;
|
||||
},
|
||||
triggerMutations() {
|
||||
triggerStreamCalls += 1;
|
||||
return triggerMutations;
|
||||
},
|
||||
};
|
||||
const applications =
|
||||
new PostgresClusterLegacyEnvMigrationApplicationRepository(
|
||||
automationDatabase.pool,
|
||||
);
|
||||
const applied = await applications.apply(intent, streams);
|
||||
assert.equal(applied.status, 'applied');
|
||||
assert.equal(taskStreamCalls, 1);
|
||||
assert.equal(triggerStreamCalls, 1);
|
||||
assert.deepEqual(
|
||||
await applications.findByApplicationId(applicationId),
|
||||
applied.receipt,
|
||||
);
|
||||
|
||||
const state = await automationDatabase.pool.query(
|
||||
`SELECT task.current_revision AS "taskRevision",
|
||||
task_revision.spec_json AS "taskSpec",
|
||||
task_revision.name AS "taskName",
|
||||
task_revision.description AS "taskDescription",
|
||||
task_revision.labels_json AS "taskLabels",
|
||||
execution.plan_json AS "executionPlan",
|
||||
trigger.current_revision AS "triggerRevision",
|
||||
trigger_revision.task_revision AS "triggerTaskRevision",
|
||||
trigger_revision.task_content_digest AS "triggerTaskContentDigest",
|
||||
schedule.trigger_revision AS "scheduleRevision",
|
||||
schedule.next_fire_at_ms AS "nextFireAtMs",
|
||||
schedule.last_scheduled_at_ms AS "lastScheduledAtMs",
|
||||
schedule.state_version AS "scheduleStateVersion",
|
||||
schedule.claim_version AS "scheduleClaimVersion"
|
||||
FROM "ql3"."task_definitions" AS task
|
||||
JOIN "ql3"."task_definition_revisions" AS task_revision
|
||||
ON task_revision.project_id = task.project_id
|
||||
AND task_revision.task_id = task.task_id
|
||||
AND task_revision.revision = task.current_revision
|
||||
JOIN "ql3"."task_execution_revisions" AS execution
|
||||
ON execution.project_id = task.project_id
|
||||
AND execution.task_id = task.task_id
|
||||
AND execution.source_revision = task.current_revision
|
||||
JOIN "ql3"."triggers" AS trigger
|
||||
ON trigger.project_id = task.project_id
|
||||
AND trigger.task_id = task.task_id
|
||||
JOIN "ql3"."trigger_revisions" AS trigger_revision
|
||||
ON trigger_revision.project_id = trigger.project_id
|
||||
AND trigger_revision.trigger_id = trigger.trigger_id
|
||||
AND trigger_revision.revision = trigger.current_revision
|
||||
JOIN "ql3"."trigger_schedules" AS schedule
|
||||
ON schedule.project_id = trigger.project_id
|
||||
AND schedule.trigger_id = trigger.trigger_id
|
||||
WHERE task.project_id = $1 AND task.task_id = $2
|
||||
AND trigger.trigger_id = $3`,
|
||||
[projectId, taskId, triggerId],
|
||||
);
|
||||
assert.equal(state.rowCount, 1);
|
||||
const row = state.rows[0];
|
||||
assert.equal(row.taskRevision, 3);
|
||||
assert.equal(row.taskSpec.config.environmentBundleRef, secretRef);
|
||||
assert.equal(row.taskSpec.config.timeoutMs, 30_000);
|
||||
assert.equal(row.taskName, task.name);
|
||||
assert.equal(row.taskDescription, task.description);
|
||||
assert.deepEqual(row.taskLabels, task.labels);
|
||||
assert.equal(row.executionPlan.environmentBundleRef, secretRef);
|
||||
assert.equal(row.triggerRevision, 2);
|
||||
assert.equal(row.triggerTaskRevision, 3);
|
||||
assert.match(row.triggerTaskContentDigest, /^[0-9a-f]{64}$/);
|
||||
assert.equal(row.scheduleRevision, 2);
|
||||
assert.equal(row.nextFireAtMs, null);
|
||||
assert.equal(row.lastScheduledAtMs, null);
|
||||
assert.equal(row.scheduleStateVersion, 1);
|
||||
assert.equal(row.scheduleClaimVersion, 1);
|
||||
|
||||
const replay = await applications.apply(intent, streams);
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.deepEqual(replay.receipt, applied.receipt);
|
||||
assert.equal(taskStreamCalls, 1);
|
||||
assert.equal(triggerStreamCalls, 1);
|
||||
|
||||
const ledger = await automationDatabase.pool.query(
|
||||
`SELECT
|
||||
(SELECT count(*)::integer
|
||||
FROM "ql3"."cluster_legacy_env_migration_application_receipts"
|
||||
WHERE application_id = $1) AS receipts,
|
||||
(SELECT count(*)::integer
|
||||
FROM "ql3"."cluster_legacy_env_migration_application_tasks"
|
||||
WHERE application_id = $1) AS tasks,
|
||||
(SELECT count(*)::integer
|
||||
FROM "ql3"."cluster_legacy_env_migration_application_triggers"
|
||||
WHERE application_id = $1) AS triggers`,
|
||||
[applicationId],
|
||||
);
|
||||
assert.deepEqual(ledger.rows, [{ receipts: 1, tasks: 1, triggers: 1 }]);
|
||||
await assert.rejects(
|
||||
automationDatabase.pool.query(
|
||||
`UPDATE "ql3"."cluster_legacy_env_migration_application_receipts"
|
||||
SET committed_at_ms = committed_at_ms
|
||||
WHERE application_id = $1`,
|
||||
[applicationId],
|
||||
),
|
||||
(error) => error?.code === '42501',
|
||||
);
|
||||
for (const database of [runtimeDatabase, adminDatabase]) {
|
||||
await assert.rejects(
|
||||
database.pool.query(
|
||||
`SELECT application_id
|
||||
FROM "ql3"."cluster_legacy_env_migration_application_receipts"
|
||||
WHERE application_id = $1`,
|
||||
[applicationId],
|
||||
),
|
||||
(error) => error?.code === '42501',
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await Promise.all([
|
||||
adminDatabase.close(),
|
||||
runtimeDatabase.close(),
|
||||
automationDatabase.close(),
|
||||
migrationDatabase.close(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ test('defines the immutable PostgreSQL capability and Run core stream', async ()
|
||||
'pg-0068-cancellation-dispatch-project-keyset',
|
||||
'pg-0069-worker-session-management-observation',
|
||||
'pg-0070-cluster-legacy-env-migration-plans',
|
||||
'pg-0071-cluster-legacy-env-migration-applications',
|
||||
],
|
||||
);
|
||||
for (const migration of postgresqlMainMigrationStream.migrations) {
|
||||
@@ -609,6 +610,11 @@ test('freezes every published PostgreSQL migration checksum', () => {
|
||||
checksum:
|
||||
'7cd6d993f48e7bcebcd62c93571a738d5117c9bcde33b974c5ac8962e2a03fe4',
|
||||
},
|
||||
{
|
||||
id: 'pg-0071-cluster-legacy-env-migration-applications',
|
||||
checksum:
|
||||
'82538b5a244011a22afad7f9c6d266a8997da538bdd2bec2ee524997c3b85996',
|
||||
},
|
||||
];
|
||||
assert.deepEqual(
|
||||
postgresqlMainMigrationStream.migrations.map(({ id, checksum }) => ({
|
||||
@@ -2489,3 +2495,48 @@ test('advances capability v69 with a content-free Legacy Env plan ledger', async
|
||||
/migration_id = 'pg-0069-worker-session-management-observation'/,
|
||||
);
|
||||
});
|
||||
|
||||
test('advances capability v70 with atomic Legacy Env application receipts', async () => {
|
||||
const migration = migrationById(
|
||||
'pg-0071-cluster-legacy-env-migration-applications',
|
||||
);
|
||||
const statements = [];
|
||||
await migration.up({
|
||||
async query(statement) {
|
||||
statements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const sql = statements.join('\n');
|
||||
for (const table of [
|
||||
'cluster_legacy_env_migration_application_receipts',
|
||||
'cluster_legacy_env_migration_application_tasks',
|
||||
'cluster_legacy_env_migration_application_triggers',
|
||||
]) {
|
||||
assert.match(sql, new RegExp(`CREATE TABLE "ql3"\\."${table}"`));
|
||||
assert.match(
|
||||
sql,
|
||||
new RegExp(
|
||||
`GRANT SELECT, INSERT ON "ql3"\\."${table}" TO ql3_automation_manager`,
|
||||
),
|
||||
);
|
||||
}
|
||||
assert.match(sql, /task_count BETWEEN 1 AND 100000/);
|
||||
assert.match(sql, /trigger_count BETWEEN 0 AND 500000/);
|
||||
assert.match(sql, /revision = previous_revision \+ 1/);
|
||||
assert.match(
|
||||
sql,
|
||||
/qinglong\/cluster-legacy-env-migration-application-receipt@v1/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
sql,
|
||||
/GRANT (?:UPDATE|DELETE|TRUNCATE)[^;]+cluster_legacy_env_migration_application/,
|
||||
);
|
||||
assert.match(sql, /contract_version = 70/);
|
||||
assert.match(sql, /"cluster_legacy_env_migration_application":1/);
|
||||
assert.match(sql, /contract_version = 69/);
|
||||
assert.match(
|
||||
sql,
|
||||
/migration_id = 'pg-0070-cluster-legacy-env-migration-plans'/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -34,6 +34,24 @@ function validPrivileges() {
|
||||
schema_capabilities: [true, false, false, false],
|
||||
projects: [true, true, true, false],
|
||||
cluster_legacy_env_migration_plans: [false, false, false, false],
|
||||
cluster_legacy_env_migration_application_receipts: [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
],
|
||||
cluster_legacy_env_migration_application_tasks: [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
],
|
||||
cluster_legacy_env_migration_application_triggers: [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
],
|
||||
task_definitions: [true, false, false, false],
|
||||
task_definition_revisions: [true, false, false, false],
|
||||
task_execution_revisions: [true, false, false, false],
|
||||
@@ -169,6 +187,24 @@ function validAdminPrivileges() {
|
||||
schema_capabilities: [true, false, false, false],
|
||||
projects: [true, false, false, false],
|
||||
cluster_legacy_env_migration_plans: [false, false, false, false],
|
||||
cluster_legacy_env_migration_application_receipts: [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
],
|
||||
cluster_legacy_env_migration_application_tasks: [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
],
|
||||
cluster_legacy_env_migration_application_triggers: [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
],
|
||||
task_definitions: [false, false, false, false],
|
||||
task_definition_revisions: [false, false, false, false],
|
||||
task_execution_revisions: [false, false, false, false],
|
||||
@@ -457,6 +493,9 @@ function automationManagerPrivileges() {
|
||||
'plugin_package_task_ownerships',
|
||||
'plugin_package_identity_keyset_ledger',
|
||||
'cluster_legacy_env_migration_plans',
|
||||
'cluster_legacy_env_migration_application_receipts',
|
||||
'cluster_legacy_env_migration_application_tasks',
|
||||
'cluster_legacy_env_migration_application_triggers',
|
||||
'security_audit_events',
|
||||
'task_definitions',
|
||||
'task_definition_revisions',
|
||||
@@ -475,6 +514,9 @@ function automationManagerPrivileges() {
|
||||
'trigger_schedules',
|
||||
'plugin_package_identity_keyset_ledger',
|
||||
'cluster_legacy_env_migration_plans',
|
||||
'cluster_legacy_env_migration_application_receipts',
|
||||
'cluster_legacy_env_migration_application_tasks',
|
||||
'cluster_legacy_env_migration_application_triggers',
|
||||
]);
|
||||
return postgresqlControlSchemaContract.tables.map(({ name: tableName }) => ({
|
||||
tableName,
|
||||
@@ -838,7 +880,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
|
||||
serverMajor: 16,
|
||||
currentUser: 'ql3_runtime',
|
||||
contractName: 'control-core',
|
||||
contractVersion: 69,
|
||||
contractVersion: 70,
|
||||
migrationIds: [
|
||||
'pg-0001-schema-capability',
|
||||
'pg-0002-run-core',
|
||||
@@ -910,6 +952,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
|
||||
'pg-0068-cancellation-dispatch-project-keyset',
|
||||
'pg-0069-worker-session-management-observation',
|
||||
'pg-0070-cluster-legacy-env-migration-plans',
|
||||
'pg-0071-cluster-legacy-env-migration-applications',
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -940,10 +983,10 @@ test('accepts the exact schema and isolated least-privilege admin role', async (
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_admin');
|
||||
assert.equal(report.contractVersion, 69);
|
||||
assert.equal(report.contractVersion, 70);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0070-cluster-legacy-env-migration-plans',
|
||||
'pg-0071-cluster-legacy-env-migration-applications',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -956,10 +999,10 @@ test('accepts the isolated least-privilege automation manager role', async () =>
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_automation_manager');
|
||||
assert.equal(report.contractVersion, 69);
|
||||
assert.equal(report.contractVersion, 70);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0070-cluster-legacy-env-migration-plans',
|
||||
'pg-0071-cluster-legacy-env-migration-applications',
|
||||
);
|
||||
|
||||
const widened = automationManagerPrivileges();
|
||||
@@ -988,10 +1031,10 @@ test('accepts the isolated least-privilege human Approval manager role', async (
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_approval_manager');
|
||||
assert.equal(report.contractVersion, 69);
|
||||
assert.equal(report.contractVersion, 70);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0070-cluster-legacy-env-migration-plans',
|
||||
'pg-0071-cluster-legacy-env-migration-applications',
|
||||
);
|
||||
|
||||
const widened = approvalManagerPrivileges();
|
||||
@@ -1022,10 +1065,10 @@ test('accepts the isolated least-privilege Run manager role', async () => {
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_run_manager');
|
||||
assert.equal(report.contractVersion, 69);
|
||||
assert.equal(report.contractVersion, 70);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0070-cluster-legacy-env-migration-plans',
|
||||
'pg-0071-cluster-legacy-env-migration-applications',
|
||||
);
|
||||
|
||||
const widened = runManagerPrivileges();
|
||||
@@ -1186,10 +1229,10 @@ test('accepts the exact schema and isolated Worker ingress role', async () => {
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_worker_ingress');
|
||||
assert.equal(report.contractVersion, 69);
|
||||
assert.equal(report.contractVersion, 70);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0070-cluster-legacy-env-migration-plans',
|
||||
'pg-0071-cluster-legacy-env-migration-applications',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user