dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x
This commit is contained in:
commit
e6dcef9bf5
302 files changed
+75178
No files matched your search
@@ -0,0 +1,143 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { canonicalJson } from '../../../../platform/lib/canonical-json.mjs'
|
||||
import { TrustError } from '../../../../platform/lib/validation.mjs'
|
||||
|
||||
function deploymentIdentity(deployment) {
|
||||
return {
|
||||
dsh: deployment.dsh,
|
||||
environment: deployment.environment,
|
||||
runtime: deployment.runtime,
|
||||
receiptTokens: [...deployment.receiptTokens].sort(),
|
||||
}
|
||||
}
|
||||
|
||||
function recordIdentity(record) {
|
||||
return deploymentIdentity({
|
||||
dsh: record.dshVersion,
|
||||
environment: record.environmentVersion,
|
||||
runtime: record.id,
|
||||
receiptTokens: record.receiptTokens,
|
||||
})
|
||||
}
|
||||
|
||||
function recordMatchesDeployment(record, deployment) {
|
||||
return record.dshVersion === deployment.dsh
|
||||
&& record.environmentVersion === deployment.environment
|
||||
&& [...record.receiptTokens].sort().join('\0') === [...deployment.receiptTokens].sort().join('\0')
|
||||
}
|
||||
|
||||
function sameRecordContent(left, right) {
|
||||
return left.authority === right.authority
|
||||
&& left.targetSequence === right.targetSequence
|
||||
&& left.dshVersion === right.dshVersion
|
||||
&& left.environmentVersion === right.environmentVersion
|
||||
&& left.environment.sha256 === right.environment.sha256
|
||||
&& left.pristine.sha256 === right.pristine.sha256
|
||||
&& left.runtime.sha256 === right.runtime.sha256
|
||||
&& left.systemPlugins.sha256 === right.systemPlugins.sha256
|
||||
}
|
||||
|
||||
function bindPlan(value) {
|
||||
return Object.freeze({
|
||||
...value,
|
||||
planId: createHash('sha256').update(canonicalJson(value)).digest('hex'),
|
||||
})
|
||||
}
|
||||
|
||||
export class CompleteStateRecovery {
|
||||
constructor({ journal, snapshots, activator }) {
|
||||
this.journal = journal
|
||||
this.snapshots = snapshots
|
||||
this.activator = activator
|
||||
}
|
||||
|
||||
async plan() {
|
||||
const transaction = await this.journal.read()
|
||||
if (transaction?.phase === 'committed' && transaction.snapshotId !== null) {
|
||||
const current = await this.activator.currentDeployment()
|
||||
if (
|
||||
current.dsh === transaction.to.dsh
|
||||
&& current.environment === transaction.to.environment
|
||||
&& current.runtime === transaction.to.runtime
|
||||
) {
|
||||
let previous = deploymentIdentity(transaction.from)
|
||||
if (this.activator.rollbackDeployments !== undefined) {
|
||||
const slots = await this.activator.rollbackDeployments()
|
||||
if (
|
||||
slots.current?.id !== current.runtime
|
||||
|| slots.previous === null
|
||||
|| !recordMatchesDeployment(slots.previous, transaction.from)
|
||||
) return null
|
||||
previous = recordIdentity(slots.previous)
|
||||
}
|
||||
const snapshot = await this.snapshots.inspect(transaction.snapshotId)
|
||||
if (
|
||||
snapshot.runtimeId !== transaction.from.runtime
|
||||
|| snapshot.environmentVersion !== transaction.from.environment
|
||||
|| snapshot.dshVersion !== transaction.from.dsh
|
||||
) throw new TrustError('rollback snapshot does not describe the previous deployment')
|
||||
return bindPlan({
|
||||
transactionId: transaction.transactionId,
|
||||
mode: transaction.mode,
|
||||
current: deploymentIdentity(current),
|
||||
previous,
|
||||
snapshot: {
|
||||
id: snapshot.id,
|
||||
createdAt: snapshot.createdAt,
|
||||
sha256: snapshot.archiveSha256,
|
||||
size: snapshot.archiveSize,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if (this.activator.rollbackDeployments === undefined) return null
|
||||
const { current, previous } = await this.activator.rollbackDeployments()
|
||||
if (current === null || previous === null) return null
|
||||
if (sameRecordContent(current, previous)) return null
|
||||
return bindPlan({
|
||||
transactionId: null,
|
||||
mode: 'stable',
|
||||
current: recordIdentity(current),
|
||||
previous: recordIdentity(previous),
|
||||
snapshot: null,
|
||||
})
|
||||
}
|
||||
|
||||
async restore(planId, {
|
||||
confirmDataLoss = false,
|
||||
requireConfirmation = false,
|
||||
onProgress = async () => {},
|
||||
} = {}) {
|
||||
if (requireConfirmation && confirmDataLoss !== true) {
|
||||
throw new TrustError('return to Stable requires explicit data-loss confirmation')
|
||||
}
|
||||
const plan = await this.plan()
|
||||
if (plan === null || plan.planId !== planId) throw new TrustError('rollback plan is stale or unavailable')
|
||||
if (requireConfirmation && plan.snapshot === null) {
|
||||
throw new TrustError('return to Stable requires a verified data snapshot')
|
||||
}
|
||||
if (plan.snapshot === null) {
|
||||
await onProgress('switching', 35, {})
|
||||
await this.activator.rollback(plan.previous.runtime)
|
||||
await onProgress('verifying', 90, {})
|
||||
return Object.freeze({ status: 'rolled-back', transactionId: null })
|
||||
}
|
||||
let transaction = await this.journal.transition('restoring-data', { error: 'manual complete-state rollback' })
|
||||
await onProgress('stopping', 15, {})
|
||||
await this.activator.suspendDsh()
|
||||
try {
|
||||
await onProgress('switching', 35, {})
|
||||
await this.activator.restoreDeployment({ ...transaction.from, runtime: plan.previous.runtime }, { resume: false })
|
||||
await onProgress('restoring-data', 65, {})
|
||||
await this.snapshots.restore(transaction.snapshotId, {
|
||||
onProgress: metrics => onProgress('restoring-data', 65, metrics),
|
||||
})
|
||||
await onProgress('verifying', 85, {})
|
||||
await this.activator.resumeDsh()
|
||||
transaction = await this.journal.transition('rolled-back', { error: 'manual complete-state rollback completed' })
|
||||
return Object.freeze({ status: 'rolled-back', transactionId: transaction.transactionId })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user