mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-26 11:34:58 +02:00
UBERF-13485: Restore v6 from storage tool (#9777)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
+209
-24
@@ -55,7 +55,7 @@ import { type MongoClient } from 'mongodb'
|
||||
import type postgres from 'postgres'
|
||||
import { type Row } from 'postgres'
|
||||
import { getToolToken } from './utils'
|
||||
import { createFileBackupStorage, restore } from '@hcengineering/server-backup'
|
||||
import { type BackupStorage, createFileBackupStorage, restore } from '@hcengineering/server-backup'
|
||||
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
|
||||
import { getPlatformQueue } from '@hcengineering/kafka'
|
||||
import {
|
||||
@@ -1167,7 +1167,8 @@ export async function migrateTrustedV6Accounts (
|
||||
accountDB: AccountDB,
|
||||
mongoDb: v6MongoAccountDB,
|
||||
dryRun: boolean,
|
||||
skipWorkspaces: Set<string>
|
||||
skipWorkspaces: Set<string>,
|
||||
conflictSuffix?: string
|
||||
): Promise<void> {
|
||||
// Mapping between <ObjectId, UUID>
|
||||
const accountsIdToUuid: Record<string, AccountUuid> = {}
|
||||
@@ -1229,13 +1230,16 @@ export async function migrateTrustedV6Accounts (
|
||||
}
|
||||
|
||||
try {
|
||||
const workspaceUuid = await migrateWorkspace(
|
||||
workspace,
|
||||
accountDB,
|
||||
accountsIdToUuid,
|
||||
accountsEmailToUuid,
|
||||
dryRun
|
||||
)
|
||||
const [workspaceUuid] =
|
||||
(await migrateWorkspace(
|
||||
workspace,
|
||||
accountDB,
|
||||
accountsIdToUuid,
|
||||
accountsEmailToUuid,
|
||||
dryRun,
|
||||
'manual-creation',
|
||||
conflictSuffix
|
||||
)) ?? []
|
||||
|
||||
if (workspaceUuid !== undefined) {
|
||||
workspacesIdToUuid[workspace.workspace] = workspaceUuid
|
||||
@@ -1394,8 +1398,12 @@ async function migrateWorkspace (
|
||||
accountsIdToUuid: Record<string, AccountUuid>,
|
||||
accountsEmailToUuid: Record<string, AccountUuid>,
|
||||
dryRun = true,
|
||||
forcedMode?: WorkspaceMode
|
||||
): Promise<WorkspaceUuid | undefined> {
|
||||
forcedMode?: WorkspaceMode,
|
||||
conflictSuffix?: string,
|
||||
throwExisting?: boolean,
|
||||
region?: string,
|
||||
branding?: string
|
||||
): Promise<[WorkspaceUuid, string] | undefined> {
|
||||
if (workspace.workspaceUrl == null) {
|
||||
console.log('No workspace url, skipping', workspace.workspace)
|
||||
return
|
||||
@@ -1406,14 +1414,19 @@ async function migrateWorkspace (
|
||||
console.log('No account found for workspace', workspace.workspace, 'created by', workspace.createdBy)
|
||||
}
|
||||
|
||||
const existingByUrl = await accountDB.workspace.findOne({ url: workspace.workspaceUrl })
|
||||
let existingByUrl = await accountDB.workspace.findOne({ url: workspace.workspaceUrl })
|
||||
const existingByUuid = await accountDB.workspace.findOne({ uuid: workspace.uuid })
|
||||
|
||||
let workspaceUuid: WorkspaceUuid
|
||||
let url = workspace.workspaceUrl
|
||||
|
||||
if (existingByUuid == null) {
|
||||
let url = workspace.workspaceUrl
|
||||
if (existingByUrl != null && (conflictSuffix ?? '') !== '') {
|
||||
url = `${url}-${conflictSuffix}`
|
||||
existingByUrl = await accountDB.workspace.findOne({ url })
|
||||
}
|
||||
if (existingByUrl != null) {
|
||||
console.log('Conflicting workspace url', url)
|
||||
// generate new url
|
||||
url = `${url}-${generateId('-')}`
|
||||
console.log('Generating new url', url)
|
||||
@@ -1424,8 +1437,8 @@ async function migrateWorkspace (
|
||||
name: workspace.workspaceName,
|
||||
url,
|
||||
dataId: workspace.workspace,
|
||||
branding: workspace.branding,
|
||||
region: workspace.region,
|
||||
branding: branding ?? workspace.branding,
|
||||
region: region ?? workspace.region,
|
||||
createdBy,
|
||||
billingAccount: createdBy,
|
||||
createdOn: workspace.createdOn ?? Date.now()
|
||||
@@ -1438,6 +1451,10 @@ async function migrateWorkspace (
|
||||
workspaceUuid = generateUuid() as WorkspaceUuid
|
||||
}
|
||||
} else {
|
||||
if (throwExisting === true) {
|
||||
throw new Error(`Workspace with the same uuid ${workspace.uuid} already exists`)
|
||||
}
|
||||
|
||||
workspaceUuid = existingByUuid.uuid
|
||||
}
|
||||
|
||||
@@ -1488,7 +1505,7 @@ async function migrateWorkspace (
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceUuid
|
||||
return [workspaceUuid, url]
|
||||
}
|
||||
|
||||
export async function restoreFromv6All (
|
||||
@@ -1581,14 +1598,15 @@ export async function restoreFromv6All (
|
||||
|
||||
try {
|
||||
// Create active workspaces as archived until they are actually restored
|
||||
const workspaceUuid = await migrateWorkspace(
|
||||
workspace,
|
||||
accountDB,
|
||||
accountsIdToUuid,
|
||||
accountsEmailToUuid,
|
||||
false,
|
||||
isActive ? 'archived' : undefined
|
||||
)
|
||||
const [workspaceUuid] =
|
||||
(await migrateWorkspace(
|
||||
workspace,
|
||||
accountDB,
|
||||
accountsIdToUuid,
|
||||
accountsEmailToUuid,
|
||||
false,
|
||||
isActive ? 'archived' : undefined
|
||||
)) ?? []
|
||||
|
||||
if (workspaceUuid !== undefined) {
|
||||
workspacesIdToUuid[workspace.workspace] = workspaceUuid
|
||||
@@ -1720,3 +1738,170 @@ export async function restoreFromv6All (
|
||||
ctx.error('Failed to restore v6 dump', { err })
|
||||
}
|
||||
}
|
||||
|
||||
export async function restoreTrustedV6Workspace (
|
||||
ctx: MeasureMetricsContext,
|
||||
accountDB: AccountDB,
|
||||
workspace: OldWorkspace,
|
||||
accounts: OldAccount[],
|
||||
invites: any[],
|
||||
backupWsStorage: BackupStorage,
|
||||
workspaceStorage: StorageAdapter,
|
||||
txes: Tx[],
|
||||
dbUrl: string,
|
||||
opts?: {
|
||||
conflictSuffix?: string
|
||||
region?: string
|
||||
branding?: string
|
||||
force?: boolean
|
||||
}
|
||||
): Promise<void> {
|
||||
const { conflictSuffix, region, branding, force } = opts ?? {}
|
||||
// Mapping between <ObjectId, UUID>
|
||||
const accountsIdToUuid: Record<string, AccountUuid> = {}
|
||||
// Mapping between <email, UUID>
|
||||
const accountsEmailToUuid: Record<string, AccountUuid> = {}
|
||||
let workspaceUuid: WorkspaceUuid | undefined
|
||||
let newWorkspaceUrl: string | undefined
|
||||
|
||||
ctx.info('Restoring workspace accounts...')
|
||||
|
||||
let accountsProcessed = 0
|
||||
for (const account of accounts) {
|
||||
try {
|
||||
const accountUuid = await migrateAccount(account, accountDB, false)
|
||||
if (accountUuid == null) {
|
||||
ctx.warn('Account not restored', account)
|
||||
continue
|
||||
}
|
||||
|
||||
accountsIdToUuid[account._id.toString()] = accountUuid
|
||||
accountsEmailToUuid[account.email] = accountUuid
|
||||
|
||||
accountsProcessed++
|
||||
if (accountsProcessed % 100 === 0) {
|
||||
ctx.info('Processed accounts:', { accountsProcessed })
|
||||
}
|
||||
} catch (err: any) {
|
||||
ctx.error('Failed to restore account', { _id: account._id, email: account.email, err })
|
||||
}
|
||||
}
|
||||
|
||||
ctx.info('Total accounts processed:', { accountsProcessed })
|
||||
|
||||
const oldMode = workspace.mode
|
||||
|
||||
try {
|
||||
// Create workspace with manual-creation mode until it is restored
|
||||
;[workspaceUuid, newWorkspaceUrl] =
|
||||
(await migrateWorkspace(
|
||||
workspace,
|
||||
accountDB,
|
||||
accountsIdToUuid,
|
||||
accountsEmailToUuid,
|
||||
false,
|
||||
'manual-creation',
|
||||
conflictSuffix,
|
||||
force !== true,
|
||||
region,
|
||||
branding
|
||||
)) ?? []
|
||||
|
||||
if (workspaceUuid === undefined) {
|
||||
ctx.error('Workspace uuid not set', { workspace: workspace.workspace })
|
||||
throw new Error(`Workspace uuid not set ${workspace.workspace}`)
|
||||
}
|
||||
|
||||
if (newWorkspaceUrl == null) {
|
||||
ctx.error('Workspace url not set', { workspace: workspace.workspace })
|
||||
throw new Error(`Workspace url not set ${workspace.workspace}`)
|
||||
}
|
||||
|
||||
let invitesProcessed = 0
|
||||
for (const invite of invites) {
|
||||
try {
|
||||
if (workspace.workspace !== invite.workspace.name) {
|
||||
ctx.error(
|
||||
`Invite workspace ${invite.workspace.name} doesn't match workspace being restored ${workspace.workspace}`
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const existing = await accountDB.invite.findOne({ migratedFrom: invite._id.toString() })
|
||||
if (existing != null) {
|
||||
continue
|
||||
}
|
||||
|
||||
const inviteRecord = {
|
||||
migratedFrom: invite._id.toString(),
|
||||
workspaceUuid,
|
||||
expiresOn: invite.exp,
|
||||
emailPattern: invite.emailMask,
|
||||
remainingUses: invite.limit,
|
||||
role: invite.role ?? AccountRole.User
|
||||
}
|
||||
|
||||
await accountDB.invite.insertOne(inviteRecord)
|
||||
|
||||
invitesProcessed++
|
||||
if (invitesProcessed % 100 === 0) {
|
||||
ctx.info('Processed invites:', { invitesProcessed })
|
||||
}
|
||||
} catch (err: any) {
|
||||
ctx.error('Failed to restore invite', { _id: invite._id, err })
|
||||
}
|
||||
}
|
||||
|
||||
ctx.info('Total invites processed:', { invitesProcessed })
|
||||
|
||||
const dataId = workspace.workspace
|
||||
const url = newWorkspaceUrl
|
||||
const uuid = workspaceUuid
|
||||
|
||||
const wsIds = {
|
||||
uuid,
|
||||
dataId,
|
||||
url
|
||||
}
|
||||
|
||||
const queue = getPlatformQueue('tool', workspace.region)
|
||||
const wsProducer = queue.getProducer<QueueWorkspaceMessage>(ctx, QueueTopic.Workspace)
|
||||
|
||||
await wsProducer.send(ctx, uuid, [workspaceEvents.restoring()])
|
||||
|
||||
let pipeline: Pipeline | undefined
|
||||
try {
|
||||
pipeline = await createBackupPipeline(ctx, dbUrl, txes, {
|
||||
externalStorage: workspaceStorage,
|
||||
usePassedCtx: true
|
||||
})(ctx, wsIds, createEmptyBroadcastOps(), null)
|
||||
if (pipeline === undefined) {
|
||||
ctx.error('failed to restore, pipeline is undefined', { dataId })
|
||||
return
|
||||
}
|
||||
await sendTransactorEvent(uuid, 'force-maintenance')
|
||||
|
||||
await restore(ctx, pipeline, wsIds, backupWsStorage, {
|
||||
date: -1,
|
||||
merge: false,
|
||||
parallel: 1,
|
||||
recheck: false
|
||||
})
|
||||
|
||||
await sendTransactorEvent(uuid, 'force-close')
|
||||
|
||||
ctx.info('workspace restored', { dataId })
|
||||
await wsProducer.send(ctx, uuid, [workspaceEvents.restored()])
|
||||
|
||||
await accountDB.workspaceStatus.update({ workspaceUuid: uuid }, { mode: oldMode })
|
||||
} catch (err) {
|
||||
ctx.error('failed to restore backup of the workspace', { url, dataId, err })
|
||||
} finally {
|
||||
await pipeline?.close()
|
||||
await queue.shutdown()
|
||||
await workspaceStorage?.close()
|
||||
}
|
||||
} catch (err: any) {
|
||||
ctx.error('Failed to restore workspace', { url: workspace.workspaceUrl, workspace: workspace.workspace, err })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user