mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 18:08:20 +08:00
feat(ql3): persist cluster secret transition receipts
This commit is contained in:
+38
@@ -1345,6 +1345,44 @@ export class PostgresPluginPackageInstallRepository
|
||||
JOIN "ql3"."plugin_package_installs" AS install
|
||||
ON install.installation_id = head.installation_id
|
||||
WHERE install.state IN ('queued', 'staged', 'activating')
|
||||
AND (
|
||||
install.state <> 'staged' OR
|
||||
install.previous_active_lock_digest IS NULL OR
|
||||
NOT (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_admission_receipts" AS admission
|
||||
JOIN "ql3"."plugin_package_install_proposals" AS proposal
|
||||
ON proposal.action_ref = admission.action_ref
|
||||
WHERE admission.installation_id = install.installation_id
|
||||
AND jsonb_array_length(
|
||||
proposal.proposal_json #> '{actionInput,manifest,spec,permissions,secrets}'
|
||||
) > 0
|
||||
) OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_installs" AS previous
|
||||
JOIN "ql3"."plugin_package_secret_bindings" AS binding
|
||||
ON binding.installation_id = previous.installation_id
|
||||
WHERE previous.project_id = install.project_id
|
||||
AND previous.package_name = install.package_name
|
||||
AND previous.lock_digest = install.previous_active_lock_digest
|
||||
)
|
||||
) OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_secret_binding_transition_receipts"
|
||||
AS receipt
|
||||
WHERE receipt.project_id = install.project_id
|
||||
AND receipt.package_name = install.package_name
|
||||
AND receipt.installation_id = install.installation_id
|
||||
AND receipt.lock_digest = install.lock_digest
|
||||
AND receipt.generation = install.target_generation
|
||||
AND receipt.manifest_digest = install.lock_json ->> 'manifestDigest'
|
||||
AND receipt.previous_active_lock_digest =
|
||||
install.previous_active_lock_digest
|
||||
)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
import type { PostgresPool } from '@qinglong/runtime-core';
|
||||
import {
|
||||
normalizePluginPackageInstallProposal,
|
||||
type PluginPackageInstallProposal,
|
||||
} from '@qinglong/runtime-core/plugin-package-proposal';
|
||||
import {
|
||||
assertPluginPackageInstallMatchesLock,
|
||||
normalizePluginPackageInstallRecord,
|
||||
normalizePluginPackageLock,
|
||||
type PluginPackageInstallRecord,
|
||||
type PluginPackageLock,
|
||||
} from '@qinglong/runtime-core/plugin-package-install';
|
||||
import type {
|
||||
PluginPackageActivationPrerequisite,
|
||||
PluginPackageActivationPrerequisiteObservation,
|
||||
} from '@qinglong/runtime-core/plugin-package-installation';
|
||||
import { createPluginPackageResourceGenerationFromReferences } from '@qinglong/runtime-core/plugin-package-resource-generation';
|
||||
import { createPluginPackageSecretBindingTarget } from '@qinglong/runtime-core/plugin-package-secret-binding';
|
||||
|
||||
import {
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
} from '../../repository/definitionRepositorySupport';
|
||||
import { PostgresPluginPackageSecretBindingTransitionRepository } from './pluginPackageSecretBindingTransitionRepository';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
function unavailable(cause?: unknown): Error {
|
||||
return new Error('Secret binding activation prerequisite is unavailable', {
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export class PostgresPluginPackageSecretBindingActivationPrerequisite
|
||||
implements PluginPackageActivationPrerequisite
|
||||
{
|
||||
readonly #receipts: PostgresPluginPackageSecretBindingTransitionRepository;
|
||||
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Secret binding activation prerequisite pool is invalid',
|
||||
);
|
||||
}
|
||||
this.#receipts = new PostgresPluginPackageSecretBindingTransitionRepository(
|
||||
pool,
|
||||
);
|
||||
}
|
||||
|
||||
async inspect(
|
||||
recordValue: Readonly<PluginPackageInstallRecord>,
|
||||
lockValue: Readonly<PluginPackageLock>,
|
||||
): Promise<Readonly<PluginPackageActivationPrerequisiteObservation>> {
|
||||
const record = normalizePluginPackageInstallRecord(recordValue);
|
||||
const lock = normalizePluginPackageLock(lockValue);
|
||||
assertPluginPackageInstallMatchesLock(lock, record);
|
||||
if (record.state !== 'staged') {
|
||||
throw new Error(
|
||||
'Secret binding activation prerequisite requires staged install',
|
||||
);
|
||||
}
|
||||
if (record.previousActiveLockDigest === null) {
|
||||
return Object.freeze({ status: 'ready' as const });
|
||||
}
|
||||
try {
|
||||
const result = await this.pool.query<Row>(
|
||||
`SELECT proposal.proposal_json AS "proposalJson",
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_installs" AS previous
|
||||
JOIN "ql3"."plugin_package_secret_bindings" AS binding
|
||||
ON binding.installation_id = previous.installation_id
|
||||
WHERE previous.project_id = install.project_id
|
||||
AND previous.package_name = install.package_name
|
||||
AND previous.lock_digest = install.previous_active_lock_digest
|
||||
) AS "previousBindingPresent"
|
||||
FROM "ql3"."plugin_package_install_heads" AS head
|
||||
JOIN "ql3"."plugin_package_installs" AS install
|
||||
ON install.installation_id = head.installation_id
|
||||
JOIN "ql3"."plugin_package_admission_receipts" AS admission
|
||||
ON admission.installation_id = install.installation_id
|
||||
JOIN "ql3"."plugin_package_install_proposals" AS proposal
|
||||
ON proposal.action_ref = admission.action_ref
|
||||
WHERE head.project_id = $1 AND head.package_name = $2
|
||||
AND install.installation_id = $3 AND install.lock_digest = $4
|
||||
AND install.state = 'staged'
|
||||
AND install.previous_active_lock_digest = $5
|
||||
LIMIT 2`,
|
||||
[
|
||||
record.projectId,
|
||||
record.packageName,
|
||||
record.installationId,
|
||||
record.lockDigest,
|
||||
record.previousActiveLockDigest,
|
||||
],
|
||||
);
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
const proposal = normalizePluginPackageInstallProposal(
|
||||
postgresRequiredJsonObject(
|
||||
result.rows[0]!.proposalJson,
|
||||
unavailable,
|
||||
) as unknown as PluginPackageInstallProposal,
|
||||
);
|
||||
if (
|
||||
proposal.actionDigest !== lock.approval.actionDigest ||
|
||||
proposal.previewDigest !== lock.approval.previewDigest ||
|
||||
proposal.actionInput.targetGeneration !== record.targetGeneration ||
|
||||
proposal.actionInput.source.contentDigest !== lock.source.contentDigest
|
||||
) {
|
||||
throw unavailable(
|
||||
new Error('Secret binding activation prerequisite provenance drift'),
|
||||
);
|
||||
}
|
||||
const required =
|
||||
proposal.actionInput.manifest.spec.permissions.secrets.length > 0 ||
|
||||
result.rows[0]!.previousBindingPresent === true;
|
||||
if (!required) return Object.freeze({ status: 'ready' as const });
|
||||
const generation = createPluginPackageResourceGenerationFromReferences({
|
||||
installationId: record.installationId,
|
||||
projectId: record.projectId,
|
||||
packageName: record.packageName,
|
||||
lockDigest: record.lockDigest,
|
||||
generation: record.targetGeneration,
|
||||
previousActiveLockDigest: record.previousActiveLockDigest,
|
||||
contentDigest: lock.source.contentDigest,
|
||||
resources: lock.resources,
|
||||
});
|
||||
const target = createPluginPackageSecretBindingTarget(
|
||||
generation,
|
||||
proposal.actionInput.manifest,
|
||||
);
|
||||
const receipt = await this.#receipts.find(target.generationDigest);
|
||||
if (
|
||||
!receipt ||
|
||||
JSON.stringify(receipt.transitionPlan.nextTarget) !==
|
||||
JSON.stringify(target) ||
|
||||
receipt.transitionPlan.previousActiveLockDigest !==
|
||||
record.previousActiveLockDigest
|
||||
) {
|
||||
return Object.freeze({
|
||||
status: 'deferred' as const,
|
||||
reason: 'secret_binding_transition_required' as const,
|
||||
});
|
||||
}
|
||||
return Object.freeze({ status: 'ready' as const });
|
||||
} catch (error) {
|
||||
throw unavailable(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
import type { PostgresClient, PostgresPool } from '@qinglong/runtime-core';
|
||||
import {
|
||||
createPluginPackageSecretBindingFromTransitionPlan,
|
||||
createPluginPackageSecretBindingTransitionReceipt,
|
||||
normalizePluginPackageSecretBindingTransitionReceipt,
|
||||
type PluginPackageSecretBindingTransitionReceipt,
|
||||
type PluginPackageSecretBindingTransitionReceiptRepository,
|
||||
} from '@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt';
|
||||
import {
|
||||
normalizePluginPackageSecretBindingTransitionPlan,
|
||||
type PluginPackageSecretBindingTransitionPlan,
|
||||
} from '@qinglong/runtime-core/plugin-package-secret-binding-transition-plan';
|
||||
import {
|
||||
PluginPackageSecretBindingConflictError,
|
||||
PluginPackageSecretBindingUnavailableError,
|
||||
normalizePluginPackageSecretBinding,
|
||||
type PluginPackageSecretBinding,
|
||||
} from '@qinglong/runtime-core/plugin-package-secret-binding';
|
||||
|
||||
import {
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
|
||||
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
|
||||
configurePostgresDefinitionTransaction,
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
postgresSqlState,
|
||||
rollbackPostgresDefinitionTransaction,
|
||||
} from '../../repository/definitionRepositorySupport';
|
||||
import { PostgresPluginPackageSecretBindingRepository } from '../installation/pluginPackageSecretBindingRepository';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
const DIGEST = /^[0-9a-f]{64}$/;
|
||||
|
||||
export interface ApplyPostgresPluginPackageSecretBindingTransitionInput {
|
||||
readonly transitionPlan: Readonly<PluginPackageSecretBindingTransitionPlan>;
|
||||
readonly evidenceDigest: string;
|
||||
readonly committedAtMs: number;
|
||||
}
|
||||
|
||||
export interface ApplyPostgresPluginPackageSecretBindingTransitionResult {
|
||||
readonly status: 'created' | 'existing';
|
||||
readonly binding: Readonly<PluginPackageSecretBinding> | null;
|
||||
readonly receipt: Readonly<PluginPackageSecretBindingTransitionReceipt>;
|
||||
}
|
||||
|
||||
function unavailable(
|
||||
cause?: unknown,
|
||||
): PluginPackageSecretBindingUnavailableError {
|
||||
return new PluginPackageSecretBindingUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function mappedError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof PluginPackageSecretBindingConflictError ||
|
||||
error instanceof PluginPackageSecretBindingUnavailableError ||
|
||||
error instanceof TypeError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
const state = postgresSqlState(error);
|
||||
if (state === '23503' || state === '23505' || state === '23514') {
|
||||
return new PluginPackageSecretBindingConflictError(
|
||||
'durable transition identity or staged generation conflicts',
|
||||
);
|
||||
}
|
||||
return unavailable(error);
|
||||
}
|
||||
|
||||
function digest(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !DIGEST.test(value)) {
|
||||
throw new TypeError(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function timestamp(value: unknown): number {
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
||||
throw new TypeError('Secret binding transition commit time is invalid');
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function same(left: unknown, right: unknown): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function parseReceipt(
|
||||
row: Row,
|
||||
): Readonly<PluginPackageSecretBindingTransitionReceipt> {
|
||||
try {
|
||||
const receipt = normalizePluginPackageSecretBindingTransitionReceipt(
|
||||
postgresRequiredJsonObject(
|
||||
row.receiptJson,
|
||||
unavailable,
|
||||
) as unknown as PluginPackageSecretBindingTransitionReceipt,
|
||||
);
|
||||
if (
|
||||
receipt.receiptDigest !==
|
||||
postgresRequiredString(row.receiptDigest, unavailable)
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return receipt;
|
||||
} catch (error) {
|
||||
if (error instanceof PluginPackageSecretBindingUnavailableError)
|
||||
throw error;
|
||||
throw unavailable(error);
|
||||
}
|
||||
}
|
||||
|
||||
export class PostgresPluginPackageSecretBindingTransitionRepository
|
||||
implements PluginPackageSecretBindingTransitionReceiptRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Secret binding transition pool is invalid',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async findStored(
|
||||
queryable: Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>,
|
||||
generationDigest: string,
|
||||
): Promise<Readonly<PluginPackageSecretBindingTransitionReceipt> | null> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT receipt_json AS "receiptJson", receipt_digest AS "receiptDigest"
|
||||
FROM "ql3"."plugin_package_secret_binding_transition_receipts"
|
||||
WHERE generation_digest = $1
|
||||
LIMIT 2`,
|
||||
[generationDigest],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
return parseReceipt(result.rows[0]!);
|
||||
}
|
||||
|
||||
async find(
|
||||
generationDigestValue: string,
|
||||
): Promise<Readonly<PluginPackageSecretBindingTransitionReceipt> | null> {
|
||||
try {
|
||||
return await this.findStored(
|
||||
this.pool,
|
||||
digest(generationDigestValue, 'generation digest'),
|
||||
);
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async #transaction<T>(
|
||||
work: (client: PostgresClient) => Promise<T>,
|
||||
): Promise<T> {
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await configurePostgresDefinitionTransaction(client);
|
||||
began = true;
|
||||
const result = await work(client);
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return result;
|
||||
} 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;
|
||||
}
|
||||
throw mappedError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
|
||||
apply(
|
||||
input: Readonly<ApplyPostgresPluginPackageSecretBindingTransitionInput>,
|
||||
): Promise<
|
||||
Readonly<ApplyPostgresPluginPackageSecretBindingTransitionResult>
|
||||
> {
|
||||
if (
|
||||
!input ||
|
||||
typeof input !== 'object' ||
|
||||
Array.isArray(input) ||
|
||||
Object.keys(input).sort().join('\0') !==
|
||||
'committedAtMs\0evidenceDigest\0transitionPlan'
|
||||
) {
|
||||
throw new TypeError('Secret binding transition execution is invalid');
|
||||
}
|
||||
const plan = normalizePluginPackageSecretBindingTransitionPlan(
|
||||
input.transitionPlan,
|
||||
);
|
||||
const evidenceDigest = digest(input.evidenceDigest, 'evidence digest');
|
||||
const committedAtMs = timestamp(input.committedAtMs);
|
||||
const proposedBinding = createPluginPackageSecretBindingFromTransitionPlan(
|
||||
plan,
|
||||
'approved-action-execution',
|
||||
evidenceDigest,
|
||||
committedAtMs,
|
||||
);
|
||||
const proposedReceipt = createPluginPackageSecretBindingTransitionReceipt({
|
||||
transitionPlan: plan,
|
||||
authority: {
|
||||
kind: 'approved-action-execution',
|
||||
evidenceDigest,
|
||||
},
|
||||
binding: proposedBinding,
|
||||
committedAtMs,
|
||||
});
|
||||
|
||||
return this.#transaction(async (client) => {
|
||||
const existing = await this.findStored(
|
||||
client,
|
||||
plan.nextTarget.generationDigest,
|
||||
);
|
||||
if (existing) {
|
||||
if (!same(existing, proposedReceipt)) {
|
||||
throw new PluginPackageSecretBindingConflictError(
|
||||
'generation is committed by another transition authority',
|
||||
);
|
||||
}
|
||||
const binding = existing.bindingDigest
|
||||
? await new PostgresPluginPackageSecretBindingRepository(client).find(
|
||||
plan.nextTarget.generationDigest,
|
||||
)
|
||||
: null;
|
||||
if (
|
||||
(existing.bindingDigest === null) !== (binding === null) ||
|
||||
(binding && binding.bindingDigest !== existing.bindingDigest)
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
binding,
|
||||
receipt: existing,
|
||||
});
|
||||
}
|
||||
|
||||
const authority = await client.query<Row>(
|
||||
`SELECT previous_binding.binding_json AS "previousBindingJson"
|
||||
FROM "ql3"."plugin_package_install_heads" AS head
|
||||
JOIN "ql3"."plugin_package_installs" AS install
|
||||
ON install.installation_id = head.installation_id
|
||||
AND install.project_id = head.project_id
|
||||
AND install.package_name = head.package_name
|
||||
JOIN "ql3"."plugin_package_installs" AS previous
|
||||
ON previous.project_id = install.project_id
|
||||
AND previous.package_name = install.package_name
|
||||
AND previous.lock_digest = install.previous_active_lock_digest
|
||||
LEFT JOIN "ql3"."plugin_package_secret_bindings" AS previous_binding
|
||||
ON previous_binding.installation_id = previous.installation_id
|
||||
AND previous_binding.project_id = previous.project_id
|
||||
AND previous_binding.package_name = previous.package_name
|
||||
AND previous_binding.lock_digest = previous.lock_digest
|
||||
AND previous_binding.generation = previous.target_generation
|
||||
WHERE head.project_id = $1 AND head.package_name = $2
|
||||
AND install.installation_id = $3 AND install.lock_digest = $4
|
||||
AND install.target_generation = $5
|
||||
AND install.lock_json ->> 'manifestDigest' = $6
|
||||
AND install.state = 'staged'
|
||||
AND install.previous_active_lock_digest = $7
|
||||
AND install.active_lock_digest = install.previous_active_lock_digest
|
||||
AND install.target_generation = (
|
||||
SELECT MAX(history.target_generation)
|
||||
FROM "ql3"."plugin_package_installs" AS history
|
||||
WHERE history.project_id = install.project_id
|
||||
AND history.package_name = install.package_name
|
||||
)
|
||||
AND previous.state = 'active'
|
||||
AND previous.active_lock_digest = previous.lock_digest
|
||||
AND previous.installation_id = $8
|
||||
AND previous.lock_digest = $7
|
||||
AND previous.target_generation = $9
|
||||
AND previous.lock_json ->> 'manifestDigest' = $10
|
||||
FOR SHARE OF head, install, previous`,
|
||||
[
|
||||
plan.nextTarget.projectId,
|
||||
plan.nextTarget.packageName,
|
||||
plan.nextTarget.installationId,
|
||||
plan.nextTarget.lockDigest,
|
||||
plan.nextTarget.generation,
|
||||
plan.nextTarget.manifestDigest,
|
||||
plan.previousActiveLockDigest,
|
||||
plan.previousTarget.installationId,
|
||||
plan.previousTarget.generation,
|
||||
plan.previousTarget.manifestDigest,
|
||||
],
|
||||
);
|
||||
if (authority.rows.length !== 1) {
|
||||
throw new PluginPackageSecretBindingConflictError(
|
||||
'transition is not the current reviewed staged generation',
|
||||
);
|
||||
}
|
||||
const previousBindingJson = authority.rows[0]!.previousBindingJson;
|
||||
const durablePreviousBinding =
|
||||
previousBindingJson === null
|
||||
? null
|
||||
: normalizePluginPackageSecretBinding(
|
||||
postgresRequiredJsonObject(previousBindingJson, unavailable),
|
||||
);
|
||||
if (!same(durablePreviousBinding, plan.previousBinding)) {
|
||||
throw new PluginPackageSecretBindingConflictError(
|
||||
'previous active binding changed after transition planning',
|
||||
);
|
||||
}
|
||||
|
||||
const bindingResult = proposedBinding
|
||||
? await new PostgresPluginPackageSecretBindingRepository(
|
||||
client,
|
||||
).publish(proposedBinding)
|
||||
: null;
|
||||
const receipt = createPluginPackageSecretBindingTransitionReceipt({
|
||||
transitionPlan: plan,
|
||||
authority: {
|
||||
kind: 'approved-action-execution',
|
||||
evidenceDigest,
|
||||
},
|
||||
binding: bindingResult?.binding ?? null,
|
||||
committedAtMs,
|
||||
});
|
||||
const target = plan.nextTarget;
|
||||
const inserted = await client.query(
|
||||
`INSERT INTO "ql3"."plugin_package_secret_binding_transition_receipts" (
|
||||
generation_digest, transition_digest, project_id, package_name,
|
||||
installation_id, lock_digest, generation, manifest_digest,
|
||||
previous_active_lock_digest, authority_kind, evidence_digest,
|
||||
binding_digest, committed_at_ms, receipt_digest, receipt_json
|
||||
) VALUES (
|
||||
$1::char(64), $2::char(64), $3::varchar(128), $4::varchar(63),
|
||||
$5::varchar(128), $6::char(64), $7::integer, $8::char(64),
|
||||
$9::char(64), $10::varchar(32), $11::char(64), $12::char(64),
|
||||
$13::bigint, $14::char(64), $15::jsonb
|
||||
) ON CONFLICT (generation_digest) DO NOTHING
|
||||
RETURNING generation_digest`,
|
||||
[
|
||||
target.generationDigest,
|
||||
plan.transitionDigest,
|
||||
target.projectId,
|
||||
target.packageName,
|
||||
target.installationId,
|
||||
target.lockDigest,
|
||||
target.generation,
|
||||
target.manifestDigest,
|
||||
plan.previousActiveLockDigest,
|
||||
receipt.authority.kind,
|
||||
receipt.authority.evidenceDigest,
|
||||
receipt.bindingDigest,
|
||||
receipt.committedAtMs,
|
||||
receipt.receiptDigest,
|
||||
JSON.stringify(receipt),
|
||||
],
|
||||
);
|
||||
const stored = await this.findStored(client, target.generationDigest);
|
||||
if (!stored || !same(stored, receipt)) {
|
||||
throw new PluginPackageSecretBindingConflictError(
|
||||
'generation is bound to another transition receipt',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
status:
|
||||
inserted.rows.length === 1
|
||||
? ('created' as const)
|
||||
: ('existing' as const),
|
||||
binding: bindingResult?.binding ?? null,
|
||||
receipt: stored,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user