🌳
pt0/deployF/dbF/backupDbF.mts
1import * as _ from 'lodash-es'
3import { kubeCpUp } from '../k8sF/kubeCpF.mts'
7import { pgSvcPort } from '../vanillaPgF.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.extQs
28 } 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}} = resource
43 const suPw = b64ToAscii(superUserPwB64)
44 return replaceStrict(extQs, `app:${password}`, `postgres:${suPw}`)
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/2556
69 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} = podInfo
83 if (runPgOnKlust) {
84 await kubeCpUp({podName, containerName, fromLocalPath: fromFile, toRemotePath})
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 = extQs
101 if (cluster_name == 'minik') {
102 minikQs = replaceStrict(minikQs, `:${nodePort}`, `:${pgSvcPort}`)
103 }
104 cmdA = [...cmdA,
105 '-Upostgres',
106 '-d', minikQs
107 ]
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-cn
126 ]).join(' ')})