1
- import { Storage } from ' @google-cloud/storage' ;
1
+ import { Storage } from " @google-cloud/storage" ;
2
2
import * as age from "age-encryption" ;
3
- import { execSync } from ' node:child_process' ;
4
- import zlib from ' node:zlib' ;
3
+ import { execSync } from " node:child_process" ;
4
+ import zlib from " node:zlib" ;
5
5
6
6
/**
7
7
* Fetches the newest backup of the production database from gs://vlach-cookbook-backup, and
@@ -16,18 +16,47 @@ import zlib from 'node:zlib';
16
16
* service account key.
17
17
* @param {string } options.AGE_IDENTITY The https://age-encryption.org/ identity file to decrypt the
18
18
* backup.
19
+ */
20
+ export async function replaceStagingDbWithBackup ( {
21
+ DATABASE_URL ,
22
+ dbName,
23
+ GOOGLE_CREDENTIALS ,
24
+ AGE_IDENTITY ,
25
+ } ) {
26
+ const dump = await fetchLatestBackup ( { GOOGLE_CREDENTIALS , AGE_IDENTITY } ) ;
27
+ replaceDbWithBackupContents ( {
28
+ dump,
29
+ dbName,
30
+ DATABASE_URL ,
31
+ webserverRole : "cookbook_staging_webserver" ,
32
+ } ) ;
33
+ }
34
+
35
+ /**
36
+ * Fetches, decrypts, and returns the newest backup of the production database from
37
+ * gs://vlach-cookbook-backup.
19
38
*
39
+ * @param {Object } options
40
+ * @param {string } options.GOOGLE_CREDENTIALS The JSON-serialized contents of the file expected by
41
+ * https://cloud.google.com/docs/authentication/application-default-credentials#GAC. Usually a
42
+ * service account key.
43
+ * @param {string } options.AGE_IDENTITY The https://age-encryption.org/ identity file to decrypt the
44
+ * backup.
45
+ *
46
+ * @returns {Promise<string> }
20
47
*/
21
- export async function replaceDbWithBackup ( { DATABASE_URL , dbName , GOOGLE_CREDENTIALS , AGE_IDENTITY } ) {
48
+ async function fetchLatestBackup ( { GOOGLE_CREDENTIALS , AGE_IDENTITY } ) {
22
49
const storage = new Storage ( {
23
- projectId : ' vlach-cookbook' ,
24
- credentials : JSON . parse ( GOOGLE_CREDENTIALS )
50
+ projectId : " vlach-cookbook" ,
51
+ credentials : JSON . parse ( GOOGLE_CREDENTIALS ) ,
25
52
} ) ;
26
53
27
- const bucket = storage . bucket ( ' vlach-cookbook-backup' ) ;
54
+ const bucket = storage . bucket ( " vlach-cookbook-backup" ) ;
28
55
// Look for backups starting a month ago.
29
56
let earliestBackupTime = new Date ( Date . now ( ) - 30 * 24 * 60 * 60 * 1000 ) ;
30
- const [ recentBackups ] = await bucket . getFiles ( { startOffset : `backup-${ earliestBackupTime . toISOString ( ) } ` } ) ;
57
+ const [ recentBackups ] = await bucket . getFiles ( {
58
+ startOffset : `backup-${ earliestBackupTime . toISOString ( ) } ` ,
59
+ } ) ;
31
60
if ( recentBackups . length === 0 ) {
32
61
throw new Error ( `Couldn't find a backup since ${ earliestBackupTime } .` ) ;
33
62
}
@@ -37,18 +66,41 @@ export async function replaceDbWithBackup({DATABASE_URL, dbName, GOOGLE_CREDENTI
37
66
d . addIdentity ( AGE_IDENTITY ) ;
38
67
const decrypted = await d . decrypt ( contents ) ;
39
68
40
- const dump = zlib . brotliDecompressSync ( decrypted ) . toString ( 'utf-8' ) ;
69
+ const dump = zlib . brotliDecompressSync ( decrypted ) . toString ( "utf-8" ) ;
70
+ return dump ;
71
+ }
41
72
73
+ /**
74
+ * Replaces the database at {@link DATABASE_URL} with the SQL {@link dump}.
75
+ *
76
+ * @param {Object } options
77
+ * @param {string } options.dump Decrypted contents of a backup.
78
+ * @param {string } options.DATABASE_URL A postgres:// URL with credentials.
79
+ * @param {string } options.dbName The name of the database to write into. This should match the end
80
+ * of DATABASE_URL.
81
+ * @param {string } options.webserverRole The database role name that the webserver uses.
82
+ */
83
+ export function replaceDbWithBackupContents ( {
84
+ dump,
85
+ dbName,
86
+ DATABASE_URL ,
87
+ webserverRole,
88
+ } ) {
42
89
// Clear the whole database without actually deleting it. Otherwise, when we
43
90
// add new tables that aren't in Prod yet, the *second* deployment to Staging
44
91
// will fail because the above `pg_dump` won't know to remove those tables.
45
92
const restore = `DROP SCHEMA IF EXISTS public CASCADE;
46
93
CREATE SCHEMA public;
47
- GRANT USAGE ON SCHEMA public TO cookbook_staging_webserver ;
94
+ GRANT USAGE ON SCHEMA public TO ${ webserverRole } ;
48
95
${ dump . replaceAll ( / c o o k b o o k _ p r o d / g, dbName ) } ` ;
49
96
50
97
try {
51
- console . log ( execSync ( `psql -d "${ DATABASE_URL } "` , { input : restore , encoding : 'utf-8' } ) ) ;
98
+ console . log (
99
+ execSync ( `psql -d "${ DATABASE_URL } "` , {
100
+ input : restore ,
101
+ encoding : "utf-8" ,
102
+ } )
103
+ ) ;
52
104
} catch ( e ) {
53
105
console . error ( `Restoring failed with\n>>>>>\n${ restore } \n<<<<<` ) ;
54
106
throw e ;
0 commit comments