import { spawnSync , SpawnSyncOptions } from 'child_process' ; import chalk from 'chalk' ; import { generateDBEndpoints } from 'oa-shared' ; /***********************************************************************************
 * Constants
 ************************************************************************************/ /** Gcloud/Firebase project to copy data from */ const SOURCE_PROJECT = 'onearmyworld' ; /** Gcloud/Firebase project to copy data to */ const TARGET_PROJECT = 'onearmy-next' ; /** Target project as referred to when using firebase cli (e.g. `firebase projects list`) */ const TARGET_PROJECT_ALIAS = 'next' ; /** Intermediate storage bucket used to  */ const STORAGE_BUCKET = 'onearmyworld-exports' ; /** Path to a service account file that will be used for operations */ const SERVICE_ACCOUNT_JSON_PATH = 'config/onearmy-migrator-service-account.json' ; /** Timestamp suffix that will be appended to exports (e.g. 2021-01-26T0750) */ const timestamp = new Date ( ) . toISOString ( ) . substring ( 0 , 16 ) . replace ( ':' , '' ) ; /** Storage path that will be used to store the exported data */ const EXPORT_TARGET = `${ STORAGE_BUCKET }/${ timestamp }` ; const DB_ENDPOINTS = generateDBEndpoints ( ) ; const COLLECTION_IDS = Object . values ( DB_ENDPOINTS ) . concat ( 'revisions' , 'stats' ) . join ( ',' ) ; /**
 * WARNING - the following methods have the ability to entirely wipe data from projects,
 * so should not be used unless sure of what you are doing! (and you have the required permissions)
 *
 * This function will backup a firebase firestore database locally
 * https://cloud.google.com/firestore/docs/manage-data/move-data
 *
 * It uses a combination of firebase scripts and gcloud scripts to achieve this
 *
 * Prerequisites
 * - gcloud installed locally
 * - gsutil running locally
 * - firebase cli installed locally
 * - service worker setup with following permissions:
 *      - read access to source project (basic viewer role)
 *      - admin access to storage bucket used (storage admin role)
 *      - admin access to target firebase datastore and functions
 *
 * Recommendations
 * - disable write-access to source project (if want to ensure perfect clone)
 *
 * Example execution
 * ```
   ts-node --project scripts/tsconfig.json scripts/maintenance/reset-staging-site.ts
   ```
   Also available as a github action in the /.github/reset-staging-site.yml
 */ function main ( ) { setupServiceAccount ( SERVICE_ACCOUNT_JSON_PATH ) ; exportFirestoreData ( SOURCE_PROJECT , EXPORT_TARGET , COLLECTION_IDS ) ; deleteFirebaseFunctions ( TARGET_PROJECT , TARGET_PROJECT_ALIAS ) ; wipeTargetFirestoreDB ( TARGET_PROJECT_ALIAS ) ; importFirestoreData ( TARGET_PROJECT , EXPORT_TARGET , COLLECTION_IDS ) ; redeployFirebaseFunctions ( TARGET_PROJECT_ALIAS ) ; } main ( ) ; /** log into a pre-configured service account using credentials stored in local json file */ function setupServiceAccount ( serviceAccountJsonPath ) { process . env . GOOGLE_APPLICATION_CREDENTIALS = serviceAccountJsonPath ; cli ( `gcloud auth activate-service-account --key-file=${ serviceAccountJsonPath }` ) ; } /** delete all functions (no native disable method) */ function deleteFirebaseFunctions ( projectId , projectAlias ) { cli ( `gcloud config set project ${ projectId }` ) ; cli ( `firebase use ${ projectAlias }` ) ; console . log ( chalk . green ( `[${ projectId }] Disabling firebase functions` ) ) ; const res = cli ( `gcloud functions list --format="json"` , 'pipe' ) ; const functionsJson = JSON . parse ( String ( res . stdout ) ) ; const functionsList = functionsJson . map ( ( v ) => v . entryPoint ) . join ( ' ' ) ; if ( functionsList . length > 0 ) { cli ( `firebase functions:delete ${ functionsList } --force` ) ; } } /** forcefully deletes all collections and subcollections in the target project*/ function wipeTargetFirestoreDB ( projectAlias ) { cli ( `firebase use ${ projectAlias }` ) ; console . log ( chalk . green ( `[Firebase Alias: ${ projectAlias }] Wiping firestore` ) ) ; cli ( `firebase firestore:delete --all-collections --yes` ) ; } /** Export specific collection list from firestore database to a storage object */ function exportFirestoreData ( projectId , storagePath , collectionIds ) { cli ( `gcloud config set project ${ projectId }` ) ; console . log ( chalk . green ( `[${ projectId }] Exporting to ${ storagePath }` ) ) ; cli ( `gcloud firestore export gs://${ storagePath } --collection-ids=${ collectionIds }` ) ; } /** Import data from a previously exported storage path into a target project*/ function importFirestoreData ( projectId , storagePath , collectionIds ) { const storageBucket = storagePath . split ( '/' ) [ 0 ] ; console . log ( chalk . green ( `[${ projectId }] check access to [${ storageBucket }]` ) ) ; cli ( `gsutil iam ch serviceAccount:${ projectId }@appspot.gserviceaccount.com:admin gs://${ storageBucket }` ) ; cli ( `gcloud config set project ${ projectId }` ) ; console . log ( chalk . green ( `[${ projectId }] Importing firestore data` ) ) ; cli ( `gcloud firestore import gs://${ storagePath } --collection-ids=${ collectionIds }` ) ; } /** call the firebase deploy method to deploy the target project functions */ function redeployFirebaseFunctions ( projectAlias ) { cli ( `firebase use ${ projectAlias }` ) ; console . log ( chalk . green ( `Firebase [${ projectAlias }] Deploying functions` ) ) ; cli ( `firebase deploy --only functions` ) ; } param command param stdio function cli ( command , stdio ? ) { const child = spawnSync ( command , { shell : true , stdio : stdio || [ 'inherit' , 'inherit' , 'pipe' ] , } ) ; if ( child . stderr ) { const msg = String ( child . stderr ) ; if ( msg . match ( /error:|warning:/gi ) || child . status === 1 ) { console . log ( chalk . red ( msg ) ) ; process . exit ( 1 ) ; } else { console . log ( msg ) ; } } return child ; }