mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 10:32:40 +08:00
feat(ql3): add cluster legacy env migration plan ledger
This commit is contained in:
@@ -353,5 +353,10 @@ export const postgresqlMainMigrationManifest: MigrationStreamManifest =
|
||||
checksum:
|
||||
'1191255575589abc2686b391827607abddb4edb78007245dbaaf45dc1c4e5e8b',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0070-cluster-legacy-env-migration-plans',
|
||||
checksum:
|
||||
'7cd6d993f48e7bcebcd62c93571a738d5117c9bcde33b974c5ac8962e2a03fe4',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -72,6 +72,7 @@ import { pg0066CancellationDispatchMigration } from '../run/migrations/pg-0066-c
|
||||
import { pg0067CancellationDispatchManagementMigration } from '../run-management/pg-0067-cancellation-dispatch-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';
|
||||
|
||||
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
|
||||
Object.freeze({
|
||||
@@ -149,5 +150,6 @@ export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMi
|
||||
pg0067CancellationDispatchManagementMigration,
|
||||
pg0068CancellationDispatchProjectKeysetMigration,
|
||||
pg0069WorkerSessionManagementObservationMigration,
|
||||
pg0070ClusterLegacyEnvMigrationPlansMigration,
|
||||
]),
|
||||
});
|
||||
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
import {
|
||||
ClusterLegacyEnvMigrationPlanConflictError,
|
||||
ClusterLegacyEnvMigrationPlanUnavailableError,
|
||||
assertClusterLegacyEnvMigrationPlanIdentifier,
|
||||
clusterLegacyEnvMigrationPlanMatchesIntent,
|
||||
createClusterLegacyEnvMigrationPlan,
|
||||
normalizeClusterLegacyEnvMigrationPlan,
|
||||
normalizeClusterLegacyEnvMigrationPlanIntent,
|
||||
type ClusterLegacyEnvMigrationPlan,
|
||||
type ClusterLegacyEnvMigrationPlanIntent,
|
||||
type ClusterLegacyEnvMigrationPlanRepository,
|
||||
} from '@qinglong/runtime-core/cluster-legacy-env-migration-plan';
|
||||
import type { PostgresClient, PostgresPool } from '@qinglong/runtime-core';
|
||||
|
||||
import {
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
|
||||
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
|
||||
configurePostgresDefinitionTransaction,
|
||||
postgresRequiredInteger,
|
||||
postgresRequiredJsonObject,
|
||||
postgresSqlState,
|
||||
rollbackPostgresDefinitionTransaction,
|
||||
} from '../repository/definitionRepositorySupport';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
|
||||
|
||||
export interface PostgresClusterLegacyEnvMigrationPlanTransactionContext {
|
||||
readonly intent: Readonly<ClusterLegacyEnvMigrationPlanIntent>;
|
||||
readonly replay: Readonly<ClusterLegacyEnvMigrationPlan> | null;
|
||||
readonly plan: Readonly<ClusterLegacyEnvMigrationPlan>;
|
||||
}
|
||||
|
||||
export type PostgresClusterLegacyEnvMigrationPlanTransactionHook = (
|
||||
client: PostgresClient,
|
||||
context: Readonly<PostgresClusterLegacyEnvMigrationPlanTransactionContext>,
|
||||
) => Promise<void>;
|
||||
|
||||
function unavailable(): ClusterLegacyEnvMigrationPlanUnavailableError {
|
||||
return new ClusterLegacyEnvMigrationPlanUnavailableError();
|
||||
}
|
||||
|
||||
function planFromRow(row: Row): Readonly<ClusterLegacyEnvMigrationPlan> {
|
||||
try {
|
||||
return normalizeClusterLegacyEnvMigrationPlan(
|
||||
postgresRequiredJsonObject(
|
||||
row.planJson,
|
||||
unavailable,
|
||||
) as unknown as ClusterLegacyEnvMigrationPlan,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ClusterLegacyEnvMigrationPlanUnavailableError) {
|
||||
throw error;
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
async function findByPlanId(
|
||||
queryable: Queryable,
|
||||
planId: string,
|
||||
): Promise<Readonly<ClusterLegacyEnvMigrationPlan> | null> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT plan_json AS "planJson"
|
||||
FROM "ql3"."cluster_legacy_env_migration_plans"
|
||||
WHERE plan_id = $1
|
||||
LIMIT 2`,
|
||||
[planId],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
const plan = planFromRow(result.rows[0]!);
|
||||
if (plan.planId !== planId) throw unavailable();
|
||||
return plan;
|
||||
}
|
||||
|
||||
async function findByMutationId(
|
||||
queryable: Queryable,
|
||||
mutationId: string,
|
||||
): Promise<Readonly<ClusterLegacyEnvMigrationPlan> | null> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT plan_json AS "planJson"
|
||||
FROM "ql3"."cluster_legacy_env_migration_plans"
|
||||
WHERE mutation_id = $1
|
||||
LIMIT 2`,
|
||||
[mutationId],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
const plan = planFromRow(result.rows[0]!);
|
||||
if (plan.mutationId !== mutationId) throw unavailable();
|
||||
return plan;
|
||||
}
|
||||
|
||||
function mappedError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof ClusterLegacyEnvMigrationPlanConflictError ||
|
||||
error instanceof ClusterLegacyEnvMigrationPlanUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
const state = postgresSqlState(error);
|
||||
if (state === '23503' || state === '23505' || state === '23514') {
|
||||
return new ClusterLegacyEnvMigrationPlanConflictError();
|
||||
}
|
||||
return unavailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Automation-manager-only append authority for content-free Cluster Legacy Env
|
||||
* migration plans. It does not materialize Secrets or mutate Task/Trigger heads.
|
||||
*/
|
||||
export class PostgresClusterLegacyEnvMigrationPlanRepository
|
||||
implements ClusterLegacyEnvMigrationPlanRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Cluster Legacy Env migration pool is invalid',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async findByPlanId(
|
||||
planIdValue: string,
|
||||
): Promise<Readonly<ClusterLegacyEnvMigrationPlan> | null> {
|
||||
const planId = assertClusterLegacyEnvMigrationPlanIdentifier(
|
||||
planIdValue,
|
||||
'planId',
|
||||
);
|
||||
try {
|
||||
return await findByPlanId(this.pool, planId);
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async publish(
|
||||
intentValue: Readonly<ClusterLegacyEnvMigrationPlanIntent>,
|
||||
transactionHook?: PostgresClusterLegacyEnvMigrationPlanTransactionHook,
|
||||
): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
plan: Readonly<ClusterLegacyEnvMigrationPlan>;
|
||||
}>
|
||||
> {
|
||||
if (
|
||||
transactionHook !== undefined &&
|
||||
typeof transactionHook !== 'function'
|
||||
) {
|
||||
throw new TypeError(
|
||||
'Cluster Legacy Env migration transaction hook is invalid',
|
||||
);
|
||||
}
|
||||
const intent = normalizeClusterLegacyEnvMigrationPlanIntent(intentValue);
|
||||
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
let began = false;
|
||||
let transactionHookError: unknown;
|
||||
try {
|
||||
await configurePostgresDefinitionTransaction(client);
|
||||
began = true;
|
||||
|
||||
const replay = await findByMutationId(client, intent.mutationId);
|
||||
if (replay) {
|
||||
if (!clusterLegacyEnvMigrationPlanMatchesIntent(replay, intent)) {
|
||||
throw new ClusterLegacyEnvMigrationPlanConflictError();
|
||||
}
|
||||
if (transactionHook) {
|
||||
try {
|
||||
const hookResult = await transactionHook(
|
||||
client,
|
||||
Object.freeze({ intent, replay, plan: replay }),
|
||||
);
|
||||
if (hookResult !== undefined) {
|
||||
throw new TypeError(
|
||||
'Cluster Legacy Env migration transaction hook must not return a value',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
transactionHookError = error;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({ status: 'existing', plan: replay });
|
||||
}
|
||||
|
||||
const project = await client.query<{ status: unknown }>(
|
||||
`SELECT status
|
||||
FROM "ql3"."projects"
|
||||
WHERE id = $1`,
|
||||
[intent.projectId],
|
||||
);
|
||||
if (project.rows.length !== 1 || project.rows[0]?.status !== 'active') {
|
||||
throw new ClusterLegacyEnvMigrationPlanConflictError();
|
||||
}
|
||||
|
||||
const occupied = await findByPlanId(client, intent.planId);
|
||||
if (occupied) throw new ClusterLegacyEnvMigrationPlanConflictError();
|
||||
|
||||
const clock = await client.query<{ plannedAtMs: unknown }>(
|
||||
`SELECT floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint AS "plannedAtMs"`,
|
||||
);
|
||||
if (clock.rows.length !== 1) throw unavailable();
|
||||
const plan = createClusterLegacyEnvMigrationPlan(
|
||||
intent,
|
||||
postgresRequiredInteger(clock.rows[0]?.plannedAtMs, unavailable),
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."cluster_legacy_env_migration_plans" (
|
||||
plan_id, mutation_id, project_id, plan_digest,
|
||||
reconciliation_bundle_digest, decision_digest,
|
||||
candidate_set_digest, source_row_count, active_row_count,
|
||||
disabled_row_count, effective_binding_count, secret_ref,
|
||||
task_revision_set_digest, trigger_revision_set_digest,
|
||||
task_count, trigger_count, total_effective_bytes,
|
||||
planned_at_ms, plan_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
$11, $12, $13, $14, $15, $16, $17, $18, $19::jsonb
|
||||
)`,
|
||||
[
|
||||
plan.planId,
|
||||
plan.mutationId,
|
||||
plan.projectId,
|
||||
plan.planDigest,
|
||||
plan.source.reconciliationBundleDigest,
|
||||
plan.source.decisionDigest,
|
||||
plan.source.candidateSetDigest,
|
||||
plan.source.sourceRowCount,
|
||||
plan.source.activeRowCount,
|
||||
plan.source.disabledRowCount,
|
||||
plan.source.effectiveBindingCount,
|
||||
plan.target.secretRef,
|
||||
plan.target.taskRevisionSetDigest,
|
||||
plan.target.triggerRevisionSetDigest,
|
||||
plan.target.taskCount,
|
||||
plan.target.triggerCount,
|
||||
plan.target.totalEffectiveBytes,
|
||||
plan.plannedAtMs,
|
||||
JSON.stringify(plan),
|
||||
],
|
||||
);
|
||||
|
||||
if (transactionHook) {
|
||||
try {
|
||||
const hookResult = await transactionHook(
|
||||
client,
|
||||
Object.freeze({ intent, replay: null, plan }),
|
||||
);
|
||||
if (hookResult !== undefined) {
|
||||
throw new TypeError(
|
||||
'Cluster Legacy Env migration transaction hook must not return a value',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
transactionHookError = error;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({ status: 'created', plan });
|
||||
} catch (error) {
|
||||
if (began) await rollbackPostgresDefinitionTransaction(client);
|
||||
const state = postgresSqlState(error);
|
||||
if (
|
||||
state &&
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
|
||||
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (error === transactionHookError && error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw mappedError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { CAPABILITIES_V68 } from '../remote-execution/pg-0069-worker-session-management-observation';
|
||||
import { definePostgresSqlMigration } from '../migrations/sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V69 = CAPABILITIES_V68.replace(
|
||||
'"cluster_execution_revision":1,',
|
||||
'"cluster_execution_revision":1,"cluster_legacy_env_migration_plan":1,',
|
||||
);
|
||||
|
||||
export const pg0070ClusterLegacyEnvMigrationPlansMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0070-cluster-legacy-env-migration-plans',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."cluster_legacy_env_migration_plans" (
|
||||
plan_id varchar(128) PRIMARY KEY,
|
||||
mutation_id varchar(128) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
plan_digest char(64) NOT NULL,
|
||||
reconciliation_bundle_digest char(64) NOT NULL,
|
||||
decision_digest char(64) NOT NULL,
|
||||
candidate_set_digest char(64) NOT NULL,
|
||||
source_row_count integer NOT NULL,
|
||||
active_row_count integer NOT NULL,
|
||||
disabled_row_count integer NOT NULL,
|
||||
effective_binding_count integer NOT NULL,
|
||||
secret_ref varchar(512) NOT NULL,
|
||||
task_revision_set_digest char(64) NOT NULL,
|
||||
trigger_revision_set_digest char(64) NOT NULL,
|
||||
task_count integer NOT NULL,
|
||||
trigger_count integer NOT NULL,
|
||||
total_effective_bytes integer NOT NULL,
|
||||
planned_at_ms bigint NOT NULL,
|
||||
plan_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_cluster_legacy_env_plan_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_cluster_legacy_env_plan_identity_check CHECK (
|
||||
plan_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_plan_digest_check CHECK (
|
||||
plan_digest ~ '^[0-9a-f]{64}$' AND
|
||||
reconciliation_bundle_digest ~ '^[0-9a-f]{64}$' AND
|
||||
decision_digest ~ '^[0-9a-f]{64}$' AND
|
||||
candidate_set_digest ~ '^[0-9a-f]{64}$' AND
|
||||
task_revision_set_digest ~ '^[0-9a-f]{64}$' AND
|
||||
trigger_revision_set_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_plan_source_check CHECK (
|
||||
source_row_count BETWEEN 1 AND 100000 AND
|
||||
active_row_count BETWEEN 1 AND 100000 AND
|
||||
disabled_row_count BETWEEN 0 AND 100000 AND
|
||||
source_row_count = active_row_count + disabled_row_count AND
|
||||
effective_binding_count BETWEEN 1 AND active_row_count
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_plan_target_check CHECK (
|
||||
secret_ref ~ '^qlsecret:v1:[A-Za-z0-9_-]+$' AND
|
||||
octet_length(secret_ref) BETWEEN 14 AND 512 AND
|
||||
task_count BETWEEN 1 AND 100000 AND
|
||||
trigger_count BETWEEN 0 AND 500000 AND
|
||||
total_effective_bytes BETWEEN 1 AND 65536
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_plan_time_check CHECK (
|
||||
planned_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_cluster_legacy_env_plan_json_check CHECK (
|
||||
jsonb_typeof(plan_json) = 'object' AND
|
||||
octet_length(plan_json::text) BETWEEN 2 AND 8192 AND
|
||||
plan_json = jsonb_build_object(
|
||||
'schema', 'qinglong/cluster-legacy-env-migration-plan@v1',
|
||||
'planId', plan_id,
|
||||
'mutationId', mutation_id,
|
||||
'projectId', project_id,
|
||||
'source', jsonb_build_object(
|
||||
'reconciliationBundleDigest', reconciliation_bundle_digest,
|
||||
'decisionDigest', decision_digest,
|
||||
'candidateSetDigest', candidate_set_digest,
|
||||
'sourceRowCount', source_row_count,
|
||||
'activeRowCount', active_row_count,
|
||||
'disabledRowCount', disabled_row_count,
|
||||
'effectiveBindingCount', effective_binding_count
|
||||
),
|
||||
'target', jsonb_build_object(
|
||||
'secretRef', secret_ref,
|
||||
'taskRevisionSetDigest', task_revision_set_digest,
|
||||
'triggerRevisionSetDigest', trigger_revision_set_digest,
|
||||
'taskCount', task_count,
|
||||
'triggerCount', trigger_count,
|
||||
'totalEffectiveBytes', total_effective_bytes
|
||||
),
|
||||
'plannedAtMs', planned_at_ms,
|
||||
'planDigest', plan_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_cluster_legacy_env_plan_mutation_uidx ON "ql3"."cluster_legacy_env_migration_plans" (mutation_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_cluster_legacy_env_plan_digest_uidx ON "ql3"."cluster_legacy_env_migration_plans" (plan_digest)`,
|
||||
`CREATE INDEX ql3_cluster_legacy_env_plan_project_idx ON "ql3"."cluster_legacy_env_migration_plans" (project_id, planned_at_ms, plan_id)`,
|
||||
`REVOKE ALL ON "ql3"."cluster_legacy_env_migration_plans" FROM PUBLIC`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."cluster_legacy_env_migration_plans" TO ql3_automation_manager`,
|
||||
`DO $ql3$ BEGIN UPDATE "ql3"."schema_capabilities" SET contract_version = 69, migration_id = 'pg-0070-cluster-legacy-env-migration-plans', capabilities = '${CAPABILITIES_V69}'::jsonb, updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint WHERE contract_name = 'control-core' AND contract_version = 68 AND migration_id = 'pg-0069-worker-session-management-observation' AND capabilities = '${CAPABILITIES_V68}'::jsonb; IF NOT FOUND THEN RAISE EXCEPTION 'control-core capability is not at version 68' USING ERRCODE = 'check_violation'; END IF; END $ql3$`,
|
||||
],
|
||||
});
|
||||
@@ -109,6 +109,77 @@ export const projects = ql3Schema.table(
|
||||
],
|
||||
);
|
||||
|
||||
export const clusterLegacyEnvMigrationPlans = ql3Schema.table(
|
||||
'cluster_legacy_env_migration_plans',
|
||||
{
|
||||
planId: varchar('plan_id', { length: 128 }).primaryKey(),
|
||||
mutationId: varchar('mutation_id', { length: 128 }).notNull(),
|
||||
projectId: varchar('project_id', { length: 128 }).notNull(),
|
||||
planDigest: char('plan_digest', { length: 64 }).notNull(),
|
||||
reconciliationBundleDigest: char('reconciliation_bundle_digest', {
|
||||
length: 64,
|
||||
}).notNull(),
|
||||
decisionDigest: char('decision_digest', { length: 64 }).notNull(),
|
||||
candidateSetDigest: char('candidate_set_digest', { length: 64 }).notNull(),
|
||||
sourceRowCount: integer('source_row_count').notNull(),
|
||||
activeRowCount: integer('active_row_count').notNull(),
|
||||
disabledRowCount: integer('disabled_row_count').notNull(),
|
||||
effectiveBindingCount: integer('effective_binding_count').notNull(),
|
||||
secretRef: varchar('secret_ref', { length: 512 }).notNull(),
|
||||
taskRevisionSetDigest: char('task_revision_set_digest', {
|
||||
length: 64,
|
||||
}).notNull(),
|
||||
triggerRevisionSetDigest: char('trigger_revision_set_digest', {
|
||||
length: 64,
|
||||
}).notNull(),
|
||||
taskCount: integer('task_count').notNull(),
|
||||
triggerCount: integer('trigger_count').notNull(),
|
||||
totalEffectiveBytes: integer('total_effective_bytes').notNull(),
|
||||
plannedAtMs: bigint('planned_at_ms', { mode: 'number' }).notNull(),
|
||||
planJson: jsonb('plan_json').$type<Record<string, unknown>>().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
name: 'ql3_cluster_legacy_env_plan_project_fk',
|
||||
columns: [table.projectId],
|
||||
foreignColumns: [projects.id],
|
||||
}).onDelete('restrict'),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_plan_identity_check',
|
||||
sql`${table.planId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.mutationId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.projectId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_plan_digest_check',
|
||||
sql`${table.planDigest} ~ '^[0-9a-f]{64}$' and ${table.reconciliationBundleDigest} ~ '^[0-9a-f]{64}$' and ${table.decisionDigest} ~ '^[0-9a-f]{64}$' and ${table.candidateSetDigest} ~ '^[0-9a-f]{64}$' and ${table.taskRevisionSetDigest} ~ '^[0-9a-f]{64}$' and ${table.triggerRevisionSetDigest} ~ '^[0-9a-f]{64}$'`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_plan_source_check',
|
||||
sql`${table.sourceRowCount} between 1 and 100000 and ${table.activeRowCount} between 1 and 100000 and ${table.disabledRowCount} between 0 and 100000 and ${table.sourceRowCount} = ${table.activeRowCount} + ${table.disabledRowCount} and ${table.effectiveBindingCount} between 1 and ${table.activeRowCount}`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_plan_target_check',
|
||||
sql`${table.secretRef} ~ '^qlsecret:v1:[A-Za-z0-9_-]+$' and octet_length(${table.secretRef}) between 14 and 512 and ${table.taskCount} between 1 and 100000 and ${table.triggerCount} between 0 and 500000 and ${table.totalEffectiveBytes} between 1 and 65536`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_plan_time_check',
|
||||
sql`${table.plannedAtMs} >= 0`,
|
||||
),
|
||||
check(
|
||||
'ql3_cluster_legacy_env_plan_json_check',
|
||||
sql`jsonb_typeof(${table.planJson}) = 'object' and octet_length(${table.planJson}::text) between 2 and 8192 and ${table.planJson} = jsonb_build_object('schema', 'qinglong/cluster-legacy-env-migration-plan@v1', 'planId', ${table.planId}, 'mutationId', ${table.mutationId}, 'projectId', ${table.projectId}, 'source', jsonb_build_object('reconciliationBundleDigest', ${table.reconciliationBundleDigest}, 'decisionDigest', ${table.decisionDigest}, 'candidateSetDigest', ${table.candidateSetDigest}, 'sourceRowCount', ${table.sourceRowCount}, 'activeRowCount', ${table.activeRowCount}, 'disabledRowCount', ${table.disabledRowCount}, 'effectiveBindingCount', ${table.effectiveBindingCount}), 'target', jsonb_build_object('secretRef', ${table.secretRef}, 'taskRevisionSetDigest', ${table.taskRevisionSetDigest}, 'triggerRevisionSetDigest', ${table.triggerRevisionSetDigest}, 'taskCount', ${table.taskCount}, 'triggerCount', ${table.triggerCount}, 'totalEffectiveBytes', ${table.totalEffectiveBytes}), 'plannedAtMs', ${table.plannedAtMs}, 'planDigest', ${table.planDigest})`,
|
||||
),
|
||||
uniqueIndex('ql3_cluster_legacy_env_plan_mutation_uidx').on(
|
||||
table.mutationId,
|
||||
),
|
||||
uniqueIndex('ql3_cluster_legacy_env_plan_digest_uidx').on(table.planDigest),
|
||||
index('ql3_cluster_legacy_env_plan_project_idx').on(
|
||||
table.projectId,
|
||||
table.plannedAtMs,
|
||||
table.planId,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const pluginPackageInstalls = ql3Schema.table(
|
||||
'plugin_package_installs',
|
||||
{
|
||||
@@ -520,7 +591,9 @@ export const pluginPackageSecretBindingTransitionApprovalPlans =
|
||||
'plugin_package_secret_binding_transition_approval_plans',
|
||||
{
|
||||
actionRef: varchar('action_ref', { length: 255 }).primaryKey(),
|
||||
approvalPlanDigest: char('approval_plan_digest', { length: 64 }).notNull(),
|
||||
approvalPlanDigest: char('approval_plan_digest', {
|
||||
length: 64,
|
||||
}).notNull(),
|
||||
transitionDigest: char('transition_digest', { length: 64 }).notNull(),
|
||||
generationDigest: char('generation_digest', { length: 64 }).notNull(),
|
||||
projectId: varchar('project_id', { length: 128 }).notNull(),
|
||||
@@ -1045,7 +1118,9 @@ export const approvedActionManualRecoveryResolutions = ql3Schema.table(
|
||||
resolvedById: varchar('resolved_by_id', { length: 255 }).notNull(),
|
||||
authenticationId: varchar('authentication_id', { length: 128 }).notNull(),
|
||||
assurance: varchar('assurance', { length: 32 }).notNull(),
|
||||
authenticatedAtMs: bigint('authenticated_at_ms', { mode: 'number' }).notNull(),
|
||||
authenticatedAtMs: bigint('authenticated_at_ms', {
|
||||
mode: 'number',
|
||||
}).notNull(),
|
||||
projectVersion: integer('project_version').notNull(),
|
||||
bindingVersion: integer('binding_version').notNull(),
|
||||
auditEventId: uuid('audit_event_id').notNull(),
|
||||
@@ -6213,6 +6288,7 @@ export const ql3PostgresTables = [
|
||||
schemaMigrations,
|
||||
schemaCapabilities,
|
||||
projects,
|
||||
clusterLegacyEnvMigrationPlans,
|
||||
pluginPackageInstalls,
|
||||
pluginPackageInstallHeads,
|
||||
pluginPackageInstallMutations,
|
||||
|
||||
@@ -21,8 +21,8 @@ export interface PostgresSchemaContractTrigger {
|
||||
export interface PostgresSchemaContract {
|
||||
readonly schema: 'ql3';
|
||||
readonly contractName: 'control-core';
|
||||
readonly contractVersion: 68;
|
||||
readonly migrationId: 'pg-0069-worker-session-management-observation';
|
||||
readonly contractVersion: 69;
|
||||
readonly migrationId: 'pg-0070-cluster-legacy-env-migration-plans';
|
||||
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_plan: 1;
|
||||
identity_admin: 1;
|
||||
plugin_package_admission: 1;
|
||||
plugin_package_authority_split: 1;
|
||||
@@ -122,8 +123,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
Object.freeze({
|
||||
schema: 'ql3',
|
||||
contractName: 'control-core',
|
||||
contractVersion: 68,
|
||||
migrationId: 'pg-0069-worker-session-management-observation',
|
||||
contractVersion: 69,
|
||||
migrationId: 'pg-0070-cluster-legacy-env-migration-plans',
|
||||
minimumServerMajor: 16,
|
||||
maximumServerMajor: 18,
|
||||
capabilities: Object.freeze({
|
||||
@@ -137,6 +138,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
automation_management_boundary: 1,
|
||||
automation_management_identity_keyset_ledger: 1,
|
||||
cluster_execution_revision: 1,
|
||||
cluster_legacy_env_migration_plan: 1,
|
||||
cluster_recovery: 1,
|
||||
cluster_recovery_claim: 1,
|
||||
cluster_scheduler_admission: 1,
|
||||
@@ -228,6 +230,27 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'created_at_ms',
|
||||
'updated_at_ms',
|
||||
]),
|
||||
table('cluster_legacy_env_migration_plans', [
|
||||
'plan_id',
|
||||
'mutation_id',
|
||||
'project_id',
|
||||
'plan_digest',
|
||||
'reconciliation_bundle_digest',
|
||||
'decision_digest',
|
||||
'candidate_set_digest',
|
||||
'source_row_count',
|
||||
'active_row_count',
|
||||
'disabled_row_count',
|
||||
'effective_binding_count',
|
||||
'secret_ref',
|
||||
'task_revision_set_digest',
|
||||
'trigger_revision_set_digest',
|
||||
'task_count',
|
||||
'trigger_count',
|
||||
'total_effective_bytes',
|
||||
'planned_at_ms',
|
||||
'plan_json',
|
||||
]),
|
||||
table('plugin_package_installs', [
|
||||
'installation_id',
|
||||
'project_id',
|
||||
@@ -1539,6 +1562,10 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'schema_capabilities_pkey',
|
||||
'projects_pkey',
|
||||
'ql3_projects_slug_uidx',
|
||||
'cluster_legacy_env_migration_plans_pkey',
|
||||
'ql3_cluster_legacy_env_plan_mutation_uidx',
|
||||
'ql3_cluster_legacy_env_plan_digest_uidx',
|
||||
'ql3_cluster_legacy_env_plan_project_idx',
|
||||
'plugin_package_installs_pkey',
|
||||
'ql3_plugin_package_installs_quarantine_target_key',
|
||||
'ql3_plugin_package_installs_recovery_idx',
|
||||
@@ -1863,6 +1890,12 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'ql3_projects_version_check',
|
||||
'ql3_projects_created_at_check',
|
||||
'ql3_projects_updated_at_check',
|
||||
'ql3_cluster_legacy_env_plan_identity_check',
|
||||
'ql3_cluster_legacy_env_plan_digest_check',
|
||||
'ql3_cluster_legacy_env_plan_source_check',
|
||||
'ql3_cluster_legacy_env_plan_target_check',
|
||||
'ql3_cluster_legacy_env_plan_time_check',
|
||||
'ql3_cluster_legacy_env_plan_json_check',
|
||||
'ql3_plugin_package_installs_identity_check',
|
||||
'ql3_plugin_package_installs_operation_check',
|
||||
'ql3_plugin_package_installs_state_check',
|
||||
@@ -2342,6 +2375,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
]),
|
||||
foreignKeys: Object.freeze([
|
||||
'ql3_schema_capabilities_migration_fk',
|
||||
'ql3_cluster_legacy_env_plan_project_fk',
|
||||
'ql3_plugin_package_installs_project_fk',
|
||||
'ql3_plugin_package_install_heads_project_fk',
|
||||
'ql3_plugin_package_install_heads_install_fk',
|
||||
|
||||
@@ -150,6 +150,12 @@ const REQUIRED_RUNTIME_PRIVILEGES = Object.freeze({
|
||||
update: true,
|
||||
delete: false,
|
||||
}),
|
||||
cluster_legacy_env_migration_plans: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
plugin_package_installs: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
@@ -699,6 +705,12 @@ const REQUIRED_ADMIN_PRIVILEGES = Object.freeze({
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
cluster_legacy_env_migration_plans: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
}),
|
||||
plugin_package_installs: Object.freeze({
|
||||
select: false,
|
||||
insert: false,
|
||||
@@ -1417,6 +1429,7 @@ const REQUIRED_AUTOMATION_MANAGER_PRIVILEGES: RequiredPrivileges =
|
||||
update: true,
|
||||
}
|
||||
: name === 'security_audit_events' ||
|
||||
name === 'cluster_legacy_env_migration_plans' ||
|
||||
name === 'task_definition_revisions' ||
|
||||
name === 'task_execution_revisions' ||
|
||||
name === 'trigger_revisions'
|
||||
|
||||
Reference in New Issue
Block a user