1import * as _ from 'lodash-es'2import { fs1Promises } from '../../serverF/libFs/fsPromisesF.mts'3import { kubeCpUp } from '../k8sF/kubeCpF.mts'4import { getDbPodInfo } from './doBackupDbF.mts'5import { doKubeExec } from '../k8sF/kubeExecF.mts'6import { replaceStrict } from '../../sharedF/moDashF/replaceStrictF.mts'7import { pgSvcPort } from '../vanillaPgF.mts'8import { deployDbCtx } from '../ctxF/deployDbCtxF.mts'9import { klusterCtx } from '../k8sF/ctxF/klusterCtxF.mts'10import { pathDownJoin } from '../../serverF/pathF.mts'11import { decomposeQsFromSec } from '../../serverF/secretsF/decomposeQsFromSecF.mts'12import { liveSpawnThrow } from '../../serverF/fileutilsF/live1SpawnThrowF.mts'13import { read2Resource } from '../k8sF/read2ResourceF.mts'14import { b64ToAscii } from '../../sharedF/moDashF/b64ToAsciiF.mts'15import { runPgOnKlustCtx } from '../../serverF/dbF/runPgOnKlustCtxF.mts'16import { assertDefined } from '../../sharedF/assertsF/assertDefinedF.mts'17import { getDbBackupDir, remoteWipBackupDir, runPgOnKlust } from './backupDbConstantsF.mts'18import { tsSec } from '../../serverF/secretsF/tsSecF.mts'19import { maskQs } from '../../serverF/dbF/maskQsF.mts'21export const getSuperUserQs = async ({qsName}: {qsName: string}) => {22 let {extQs, svcName: dbName, cluster_name, password, isCnPg} = await decomposeQsFromSec(tsSec(qsName))24 // Try local superuser qs cache first (written by qs action, avoids k8s API)25 try {26 const suInfo = await decomposeQsFromSec(`${dbName}-su_qs` as any)27 if (suInfo?.extQs) return suInfo.extQs28 } catch { /* catch:userapproved — no local su qs cache, fall through to k8s API */ }30 const superuserSecretName = `${dbName}-superuser`31 const resource: any = await read2Resource({cluster_name, resource: {32 kind: 'Secret',33 apiVersion: 'v1',34 metadata: {35 name: superuserSecretName,36 namespace: 'default',37 }38 }})39 if (!resource) {40 throw new Error(`Superuser secret '${superuserSecretName}' not found. Ensure CNPG cluster is running with enableSuperuserAccess: true`)41 }42 const {data: {password: superUserPwB64}} = resource43 const suPw = b64ToAscii(superUserPwB64)44 return replaceStrict(extQs, `app:${password}`, `postgres:${suPw}`)45}47export const restoreDbByQsName = async ({qsName}: {qsName: string}) => {48 runPgOnKlustCtx.enterWith({runPgOnKlust: true} as Record<string, unknown>)49 let {extQs, svcName: dbName, nodePort, cluster_name, password, isCnPg} = await decomposeQsFromSec(tsSec(qsName))51 if (isCnPg) {52 extQs = await getSuperUserQs({qsName})53 }55 const dirPath = getDbBackupDir({dbName})56 const regex = new RegExp(`^.+_(\\d+).sql$`)58 let files: string[] = (await fs1Promises.readdir(dirPath)).filter((fileName: string) => fileName.match(regex))59 files = _.sortBy(files, (fileName: string) => {60 return parseInt(fileName.match(regex)![1])61 })62 const mostRecentBackupFile = _.last(files) 63 assertDefined(mostRecentBackupFile, {dirPath, dbName})65 let extraArg = '--schema-only'66 const {dbType} = deployDbCtx.getStore() ?? {}67 if (_.startsWith(dbType, 'timescale')) {68 // https://github.com/timescale/timescaledb/issues/255669 console.log('fyi migration will look like it fails, but check rows will exist?')70 } else {71 extraArg += ` --clean --if-exists` 72 }74 const fromFile = [dirPath, mostRecentBackupFile].join('/')76 const toRemotePath = pathDownJoin(remoteWipBackupDir, mostRecentBackupFile)77 klusterCtx.enterWith({...klusterCtx.getStore(), cluster_name})79 const podInfo = await getDbPodInfo({svcName: dbName, cluster_name, isCnPg})80 assertDefined(podInfo)81 const {podName, containerName} = podInfo83 if (runPgOnKlust) {85 }87 const restoreDb = async ({extraArg=''}) => {88 let cmdA = [89 'pg_restore', '--verbose',90 ]91 if (isCnPg) {92 cmdA = [...cmdA, `--no-privileges`, `--role=app`]93 }94 cmdA = [95 ...cmdA,96 ..._.compact(extraArg.split(' ')),97 '--no-acl', '--no-owner',98 ]99 if (runPgOnKlust) {100 let minikQs = extQs101 if (cluster_name == 'minik') {102 minikQs = replaceStrict(minikQs, `:${nodePort}`, `:${pgSvcPort}`)103 }104 cmdA = [...cmdA,105 '-Upostgres',106 '-d', minikQs107 ]108 } else {109 cmdA.push('-d')110 cmdA.push(`'${extQs}'`)111 }113 if (runPgOnKlust) {114 cmdA.push(toRemotePath)115 await doKubeExec({cmdA, podName, containerName})116 } else {117 cmdA.push(fromFile)118 await liveSpawnThrow({cmd: cmdA.join(' '), printCmd: cmdA.map(maskQs).join(' '), isQuiet: false}) 119 }120 }122 await restoreDb({extraArg})123 await restoreDb({extraArg: _.compact([124 `--data-only`,125 isCnPg && `--disable-triggers`, // --disable-triggers fixes cn, breaks non-cn126 ]).join(' ')})127}