|
| 1 | +import * as clc from "colorette"; |
| 2 | + |
| 3 | +import { Command } from "../command"; |
| 4 | +import * as fsi from "../firestore/api"; |
| 5 | +import * as types from "../firestore/api-types"; |
| 6 | +import { getCurrentMinuteAsIsoString, parseDatabaseName } from "../firestore/util"; |
| 7 | +import { logger } from "../logger"; |
| 8 | +import { requirePermissions } from "../requirePermissions"; |
| 9 | +import { Emulators } from "../emulator/types"; |
| 10 | +import { warnEmulatorNotSupported } from "../emulator/commandUtils"; |
| 11 | +import { EncryptionType, FirestoreOptions } from "../firestore/options"; |
| 12 | +import { PrettyPrint } from "../firestore/pretty-print"; |
| 13 | +import { FirebaseError } from "../error"; |
| 14 | + |
| 15 | +export const command = new Command("firestore:databases:clone <sourceDatabase> <targetDatabase>") |
| 16 | + .description("clone one Firestore database to another") |
| 17 | + .option( |
| 18 | + "-e, --encryption-type <encryptionType>", |
| 19 | + `encryption method of the cloned database; one of ${EncryptionType.USE_SOURCE_ENCRYPTION} (default), ` + |
| 20 | + `${EncryptionType.CUSTOMER_MANAGED_ENCRYPTION}, ${EncryptionType.GOOGLE_DEFAULT_ENCRYPTION}`, |
| 21 | + ) |
| 22 | + // TODO(b/356137854): Remove allowlist only message once feature is public GA. |
| 23 | + .option( |
| 24 | + "-k, --kms-key-name <kmsKeyName>", |
| 25 | + "resource ID of the Cloud KMS key to encrypt the cloned database. This " + |
| 26 | + "feature is allowlist only in initial launch", |
| 27 | + ) |
| 28 | + .option( |
| 29 | + "-s, --snapshot-time <snapshotTime>", |
| 30 | + "snapshot time of the source database to use, in ISO 8601 format. Can be any minutely snapshot after the database's earliest version time. If unspecified, takes the most recent available snapshot", |
| 31 | + ) |
| 32 | + .before(requirePermissions, ["datastore.databases.clone"]) |
| 33 | + .before(warnEmulatorNotSupported, Emulators.FIRESTORE) |
| 34 | + .action(async (sourceDatabase: string, targetDatabase: string, options: FirestoreOptions) => { |
| 35 | + const api = new fsi.FirestoreApi(); |
| 36 | + const printer = new PrettyPrint(); |
| 37 | + const helpCommandText = "See firebase firestore:databases:clone --help for more info."; |
| 38 | + |
| 39 | + if (options.database) { |
| 40 | + throw new FirebaseError(`Please do not use --database for this command. ${helpCommandText}`); |
| 41 | + } |
| 42 | + |
| 43 | + let snapshotTime: string; |
| 44 | + if (options.snapshotTime) { |
| 45 | + snapshotTime = options.snapshotTime; |
| 46 | + } else { |
| 47 | + snapshotTime = getCurrentMinuteAsIsoString(); |
| 48 | + } |
| 49 | + |
| 50 | + let encryptionConfig: types.EncryptionConfig | undefined = undefined; |
| 51 | + switch (options.encryptionType) { |
| 52 | + case EncryptionType.GOOGLE_DEFAULT_ENCRYPTION: |
| 53 | + throwIfKmsKeyNameIsSet(options.kmsKeyName); |
| 54 | + encryptionConfig = { googleDefaultEncryption: {} }; |
| 55 | + break; |
| 56 | + case EncryptionType.USE_SOURCE_ENCRYPTION: |
| 57 | + throwIfKmsKeyNameIsSet(options.kmsKeyName); |
| 58 | + encryptionConfig = { useSourceEncryption: {} }; |
| 59 | + break; |
| 60 | + case EncryptionType.CUSTOMER_MANAGED_ENCRYPTION: |
| 61 | + encryptionConfig = { |
| 62 | + customerManagedEncryption: { kmsKeyName: getKmsKeyOrThrow(options.kmsKeyName) }, |
| 63 | + }; |
| 64 | + break; |
| 65 | + case undefined: |
| 66 | + throwIfKmsKeyNameIsSet(options.kmsKeyName); |
| 67 | + break; |
| 68 | + default: |
| 69 | + throw new FirebaseError(`Invalid value for flag --encryption-type. ${helpCommandText}`); |
| 70 | + } |
| 71 | + |
| 72 | + // projects must be the same |
| 73 | + const targetDatabaseName = parseDatabaseName(targetDatabase); |
| 74 | + const parentProject = targetDatabaseName.projectId; |
| 75 | + const targetDatabaseId = targetDatabaseName.databaseId; |
| 76 | + const sourceProject = parseDatabaseName(sourceDatabase).projectId; |
| 77 | + if (parentProject !== sourceProject) { |
| 78 | + throw new FirebaseError(`Source and target projects must match.`); |
| 79 | + } |
| 80 | + const lro: types.Operation = await api.cloneDatabase( |
| 81 | + sourceProject, |
| 82 | + { |
| 83 | + database: sourceDatabase, |
| 84 | + snapshotTime, |
| 85 | + }, |
| 86 | + targetDatabaseId, |
| 87 | + encryptionConfig, |
| 88 | + ); |
| 89 | + |
| 90 | + if (options.json) { |
| 91 | + logger.info(JSON.stringify(lro, undefined, 2)); |
| 92 | + } else if (lro.error) { |
| 93 | + logger.error( |
| 94 | + clc.bold( |
| 95 | + `Clone to ${printer.prettyDatabaseString(targetDatabase)} failed. See below for details.`, |
| 96 | + ), |
| 97 | + ); |
| 98 | + printer.prettyPrintOperation(lro); |
| 99 | + } else { |
| 100 | + logger.info( |
| 101 | + clc.bold(`Successfully initiated clone to ${printer.prettyDatabaseString(targetDatabase)}`), |
| 102 | + ); |
| 103 | + logger.info( |
| 104 | + "Please be sure to configure Firebase rules in your Firebase config file for\n" + |
| 105 | + "the new database. By default, created databases will have closed rules that\n" + |
| 106 | + "block any incoming third-party traffic.", |
| 107 | + ); |
| 108 | + logger.info(); |
| 109 | + logger.info(`You can monitor the progress of this clone by executing this command:`); |
| 110 | + logger.info(); |
| 111 | + logger.info( |
| 112 | + `firebase firestore:operations:describe --database="${targetDatabaseId}" ${lro.name}`, |
| 113 | + ); |
| 114 | + logger.info(); |
| 115 | + logger.info( |
| 116 | + `Once the clone is complete, your database may be viewed at ${printer.firebaseConsoleDatabaseUrl(options.project, targetDatabaseId)}`, |
| 117 | + ); |
| 118 | + } |
| 119 | + |
| 120 | + return lro; |
| 121 | + |
| 122 | + function throwIfKmsKeyNameIsSet(kmsKeyName: string | undefined): void { |
| 123 | + if (kmsKeyName) { |
| 124 | + throw new FirebaseError( |
| 125 | + "--kms-key-name can only be set when specifying an --encryption-type " + |
| 126 | + `of ${EncryptionType.CUSTOMER_MANAGED_ENCRYPTION}.`, |
| 127 | + ); |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + function getKmsKeyOrThrow(kmsKeyName: string | undefined): string { |
| 132 | + if (kmsKeyName) return kmsKeyName; |
| 133 | + |
| 134 | + throw new FirebaseError( |
| 135 | + "--kms-key-name must be provided when specifying an --encryption-type " + |
| 136 | + `of ${EncryptionType.CUSTOMER_MANAGED_ENCRYPTION}.`, |
| 137 | + ); |
| 138 | + } |
| 139 | + }); |
0 commit comments