diff --git a/samples/backups-create-with-encryption-key.js b/samples/backups-create-with-encryption-key.js new file mode 100644 index 000000000..96433971e --- /dev/null +++ b/samples/backups-create-with-encryption-key.js @@ -0,0 +1,92 @@ +/** + * Copyright 2021 Google LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +async function createBackupWithEncryptionKey( + instanceId, + databaseId, + backupId, + projectId, + keyName +) { + // [START spanner_create_backup_with_encryption_key] + // Imports the Google Cloud client library and precise date library + const {Spanner} = require('@google-cloud/spanner'); + const {PreciseDate} = require('@google-cloud/precise-date'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + // const backupId = 'my-backup'; + // const versionTime = Date.now() - 1000 * 60 * 60 * 24; // One day ago + // const keyName = + // 'projects/my-project-id/my-region/keyRings/my-key-ring/cryptoKeys/my-key'; + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner instance and database + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + const backup = instance.backup(backupId); + + // Creates a new backup of the database + try { + console.log(`Creating backup of database ${database.formattedName_}.`); + const databasePath = database.formattedName_; + // Expire backup 14 days in the future + const expireTime = Date.now() + 1000 * 60 * 60 * 24 * 14; + // Create a backup of the state of the database at the current time. + const [, operation] = await backup.create({ + databasePath: databasePath, + expireTime: expireTime, + encryptionConfig: { + encryptionType: 'CUSTOMER_MANAGED_ENCRYPTION', + kmsKeyName: keyName, + }, + }); + + console.log(`Waiting for backup ${backup.formattedName_} to complete...`); + await operation.promise(); + + // Verify backup is ready + const [backupInfo] = await backup.getMetadata(); + if (backupInfo.state === 'READY') { + console.log( + `Backup ${backupInfo.name} of size ` + + `${backupInfo.sizeBytes} bytes was created at ` + + `${new PreciseDate(backupInfo.createTime).toISOString()} ` + + `using encryption key ${backupInfo.encryptionInfo.kmsKeyVersion}` + ); + } else { + console.error('ERROR: Backup is not ready.'); + } + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + await database.close(); + } + // [END spanner_create_backup_with_encryption_key] +} + +module.exports.createBackupWithEncryptionKey = createBackupWithEncryptionKey; diff --git a/samples/backups-restore-with-encryption-key.js b/samples/backups-restore-with-encryption-key.js new file mode 100644 index 000000000..c9ebb4003 --- /dev/null +++ b/samples/backups-restore-with-encryption-key.js @@ -0,0 +1,77 @@ +/** + * Copyright 2021 Google LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +async function restoreBackupWithEncryptionKey( + instanceId, + databaseId, + backupId, + projectId, + keyName +) { + // [START spanner_restore_backup_with_encryption_key] + // Imports the Google Cloud client library and precise date library + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + // const backupId = 'my-backup'; + // const keyName = + // 'projects/my-project-id/my-region/keyRings/my-key-ring/cryptoKeys/my-key'; + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner instance and database + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + // Restore the database + console.log( + `Restoring database ${database.formattedName_} from backup ${backupId}.` + ); + const [, restoreOperation] = await database.restore( + `projects/${projectId}/instances/${instanceId}/backups/${backupId}`, + { + encryptionConfig: { + encryptionType: 'CUSTOMER_MANAGED_ENCRYPTION', + kmsKeyName: keyName, + }, + } + ); + + // Wait for restore to complete + console.log('Waiting for database restore to complete...'); + await restoreOperation.promise(); + + console.log('Database restored from backup.'); + const restoreInfo = await database.getRestoreInfo(); + const [data] = await database.get(); + console.log( + `Database ${restoreInfo.backupInfo.sourceDatabase} was restored ` + + `to ${databaseId} from backup ${restoreInfo.backupInfo.backup} ` + + `using encryption key ${data.metadata.encryptionConfig.kmsKeyName}.` + ); + // [END spanner_restore_backup_with_encryption_key] +} + +module.exports.restoreBackupWithEncryptionKey = restoreBackupWithEncryptionKey; diff --git a/samples/backups.js b/samples/backups.js index 08de07e42..1ef05454e 100644 --- a/samples/backups.js +++ b/samples/backups.js @@ -16,12 +16,18 @@ 'use strict'; const {createBackup} = require('./backups-create'); +const { + createBackupWithEncryptionKey, +} = require('./backups-create-with-encryption-key'); const {cancelBackup} = require('./backups-cancel'); const {getBackups} = require('./backups-get'); const {getBackupOperations} = require('./backups-get-operations'); const {getDatabaseOperations} = require('./backups-get-database-operations'); const {updateBackup} = require('./backups-update'); const {restoreBackup} = require('./backups-restore'); +const { + restoreBackupWithEncryptionKey, +} = require('./backups-restore-with-encryption-key'); const {deleteBackup} = require('./backups-delete'); require('yargs') @@ -39,6 +45,19 @@ require('yargs') Date.parse(opts.versionTime) ) ) + .command( + 'createBackupWithEncryptionKey ', + 'Creates a backup of a Cloud Spanner database using an encryption key.', + {}, + opts => + createBackupWithEncryptionKey( + opts.instanceName, + opts.databaseName, + opts.backupName, + opts.projectId, + opts.keyName + ) + ) .command( 'cancelBackup ', 'Creates and cancels a backup of a Cloud Spanner database.', @@ -94,6 +113,19 @@ require('yargs') opts.projectId ) ) + .command( + 'restoreBackupWithEncryptionKey ', + 'Restores a Cloud Spanner database from a backup with an encryption key.', + {}, + opts => + restoreBackupWithEncryptionKey( + opts.instanceName, + opts.databaseName, + opts.backupName, + opts.projectId, + opts.keyName + ) + ) .command( 'deleteBackup ', 'Deletes a backup.', diff --git a/samples/database-create-with-encryption-key.js b/samples/database-create-with-encryption-key.js new file mode 100644 index 000000000..e19da7ebf --- /dev/null +++ b/samples/database-create-with-encryption-key.js @@ -0,0 +1,70 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +async function createDatabaseWithEncryptionKey( + instanceId, + databaseId, + projectId, + keyName +) { + // [START spanner_create_database_with_encryption_key] + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + // const keyName = + // 'projects/my-project-id/my-region/keyRings/my-key-ring/cryptoKeys/my-key'; + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner instance + const instance = spanner.instance(instanceId); + + const request = { + encryptionConfig: { + kmsKeyName: keyName, + }, + }; + + // Creates a database + const [database, operation] = await instance.createDatabase( + databaseId, + request + ); + + console.log(`Waiting for operation on ${database.id} to complete...`); + await operation.promise(); + + console.log(`Created database ${databaseId} on instance ${instanceId}.`); + + // Get encryption key + const [data] = await database.get(); + + console.log( + `Database encrypted with key ${data.metadata.encryptionConfig.kmsKeyName}.` + ); + // [END spanner_create_database_with_encryption_key] +} + +module.exports.createDatabaseWithEncryptionKey = createDatabaseWithEncryptionKey; diff --git a/samples/package.json b/samples/package.json index 24fcf4733..c790baeb6 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,6 +14,7 @@ "test": "mocha system-test --timeout 1600000" }, "dependencies": { + "@google-cloud/kms": "^2.1.3", "@google-cloud/precise-date": "^2.0.0", "@google-cloud/spanner": "^5.5.0", "yargs": "^16.0.0" diff --git a/samples/schema.js b/samples/schema.js index 7b582f702..462755660 100644 --- a/samples/schema.js +++ b/samples/schema.js @@ -163,6 +163,9 @@ async function queryDataWithNewColumn(instanceId, databaseId, projectId) { const { createDatabaseWithVersionRetentionPeriod, } = require('./database-create-with-version-retention-period'); +const { + createDatabaseWithEncryptionKey, +} = require('./database-create-with-encryption-key'); require('yargs') .demand(1) @@ -172,6 +175,18 @@ require('yargs') {}, opts => createDatabase(opts.instanceName, opts.databaseName, opts.projectId) ) + .command( + 'createDatabaseWithEncryptionKey ', + 'Creates an example database using given encryption key in a Cloud Spanner instance.', + {}, + opts => + createDatabaseWithEncryptionKey( + opts.instanceName, + opts.databaseName, + opts.projectId, + opts.keyName + ) + ) .command( 'addColumn ', 'Adds an example MarketingBudget column to an example Cloud Spanner table.', @@ -201,6 +216,9 @@ require('yargs') ) ) .example('node $0 createDatabase "my-instance" "my-database" "my-project-id"') + .example( + 'node $0 createDatabaseWithEncryptionKey "my-instance" "my-database" "my-project-id" "key-name"' + ) .example('node $0 addColumn "my-instance" "my-database" "my-project-id"') .example('node $0 queryNewColumn "my-instance" "my-database" "my-project-id"') .example( diff --git a/samples/system-test/spanner.test.js b/samples/system-test/spanner.test.js index f95fdc212..125332073 100644 --- a/samples/system-test/spanner.test.js +++ b/samples/system-test/spanner.test.js @@ -15,6 +15,7 @@ 'use strict'; const {Spanner} = require('@google-cloud/spanner'); +const {KeyManagementServiceClient} = require('@google-cloud/kms'); const {assert} = require('chai'); const {describe, it, before, after} = require('mocha'); const cp = require('child_process'); @@ -44,9 +45,16 @@ const SAMPLE_INSTANCE_ID = `${PREFIX}-my-sample-instance-${CURRENT_TIME}`; const INSTANCE_ALREADY_EXISTS = !!process.env.SPANNERTEST_INSTANCE; const DATABASE_ID = `test-database-${CURRENT_TIME}`; const RESTORE_DATABASE_ID = `test-database-${CURRENT_TIME}-r`; +const ENCRYPTED_RESTORE_DATABASE_ID = `test-database-${CURRENT_TIME}-r-enc`; const VERSION_RETENTION_DATABASE_ID = `test-database-${CURRENT_TIME}-v`; +const ENCRYPTED_DATABASE_ID = `test-database-${CURRENT_TIME}-enc`; const BACKUP_ID = `test-backup-${CURRENT_TIME}`; +const ENCRYPTED_BACKUP_ID = `test-backup-${CURRENT_TIME}-enc`; const CANCELLED_BACKUP_ID = `test-backup-${CURRENT_TIME}-c`; +const LOCATION_ID = 'regional-us-central1'; +const KEY_LOCATION_ID = 'us-central1'; +const KEY_RING_ID = 'test-key-ring-node'; +const KEY_ID = 'test-key'; const spanner = new Spanner({ projectId: PROJECT_ID, @@ -119,6 +127,69 @@ async function deleteInstance(instance) { return instance.delete(GAX_OPTIONS); } +async function getCryptoKey() { + const NOT_FOUND = 5; + + // Instantiates a client. + const client = new KeyManagementServiceClient(); + + // Build the parent key ring name. + const keyRingName = client.keyRingPath( + PROJECT_ID, + KEY_LOCATION_ID, + KEY_RING_ID + ); + + // Get key ring. + try { + await client.getKeyRing({name: keyRingName}); + } catch (err) { + // Create key ring if it doesn't exist. + if (err.code === NOT_FOUND) { + // Build the parent location name. + const locationName = client.locationPath(PROJECT_ID, KEY_LOCATION_ID); + await client.createKeyRing({ + parent: locationName, + keyRingId: KEY_RING_ID, + }); + } else { + throw err; + } + } + + // Get key. + try { + // Build the key name + const keyName = client.cryptoKeyPath( + PROJECT_ID, + KEY_LOCATION_ID, + KEY_RING_ID, + KEY_ID + ); + const [key] = await client.getCryptoKey({ + name: keyName, + }); + return key; + } catch (err) { + // Create key if it doesn't exist. + if (err.code === NOT_FOUND) { + const [key] = await client.createCryptoKey({ + parent: keyRingName, + cryptoKeyId: KEY_ID, + cryptoKey: { + purpose: 'ENCRYPT_DECRYPT', + versionTemplate: { + algorithm: 'GOOGLE_SYMMETRIC_ENCRYPTION', + }, + }, + }); + return key; + } else { + throw err; + } + } +} + describe('Spanner', () => { const instance = spanner.instance(INSTANCE_ID); @@ -127,7 +198,7 @@ describe('Spanner', () => { if (!INSTANCE_ALREADY_EXISTS) { const [, operation] = await instance.create({ - config: 'regional-us-central1', + config: LOCATION_ID, nodes: 1, labels: { [LABEL]: 'true', @@ -150,6 +221,7 @@ describe('Spanner', () => { // Make sure all backups are deleted before an instance can be deleted. await Promise.all([ instance.backup(BACKUP_ID).delete(GAX_OPTIONS), + instance.backup(ENCRYPTED_BACKUP_ID).delete(GAX_OPTIONS), instance.backup(CANCELLED_BACKUP_ID).delete(GAX_OPTIONS), ]); await instance.delete(GAX_OPTIONS); @@ -157,7 +229,9 @@ describe('Spanner', () => { await Promise.all([ instance.database(DATABASE_ID).delete(), instance.database(RESTORE_DATABASE_ID).delete(), + instance.database(ENCRYPTED_RESTORE_DATABASE_ID).delete(), instance.backup(BACKUP_ID).delete(GAX_OPTIONS), + instance.backup(ENCRYPTED_BACKUP_ID).delete(GAX_OPTIONS), instance.backup(CANCELLED_BACKUP_ID).delete(GAX_OPTIONS), ]); } @@ -213,6 +287,39 @@ describe('Spanner', () => { ); }); + describe('encrypted database', () => { + after(async () => { + const instance = spanner.instance(INSTANCE_ID); + const encrypted_database = instance.database(ENCRYPTED_DATABASE_ID); + await encrypted_database.delete(); + }); + + // create_database_with_encryption_key + it('should create a database with an encryption key', async () => { + const key = await getCryptoKey(); + + const output = execSync( + `${schemaCmd} createDatabaseWithEncryptionKey "${INSTANCE_ID}" "${ENCRYPTED_DATABASE_ID}" ${PROJECT_ID} "${key.name}"` + ); + assert.match( + output, + new RegExp( + `Waiting for operation on ${ENCRYPTED_DATABASE_ID} to complete...` + ) + ); + assert.match( + output, + new RegExp( + `Created database ${ENCRYPTED_DATABASE_ID} on instance ${INSTANCE_ID}.` + ) + ); + assert.match( + output, + new RegExp(`Database encrypted with key ${key.name}.`) + ); + }); + }); + describe('quickstart', () => { // Running the quickstart test in here since there's already a spanner // instance and database set up at this point. @@ -828,6 +935,20 @@ describe('Spanner', () => { assert.match(output, new RegExp(`Backup (.+)${BACKUP_ID} of size`)); }); + // create_backup_with_encryption_key + it('should create an encrypted backup of the database', async () => { + const key = await getCryptoKey(); + + const output = execSync( + `${backupsCmd} createBackupWithEncryptionKey ${INSTANCE_ID} ${DATABASE_ID} ${ENCRYPTED_BACKUP_ID} ${PROJECT_ID} ${key.name}` + ); + assert.match( + output, + new RegExp(`Backup (.+)${ENCRYPTED_BACKUP_ID} of size`) + ); + assert.include(output, `using encryption key ${key.name}`); + }); + // cancel_backup it('should cancel a backup of the database', async () => { const output = execSync( @@ -849,7 +970,7 @@ describe('Spanner', () => { assert.include(output, 'Ready backups filtered by create time:'); assert.include(output, 'Get backups paginated:'); const count = (output.match(new RegExp(`${BACKUP_ID}`, 'g')) || []).length; - assert.equal(count, 7); + assert.equal(count, 14); }); // list_backup_operations @@ -893,6 +1014,29 @@ describe('Spanner', () => { ); }); + // restore_backup_with_encryption_key + it('should restore database from a backup using an encryption key', async function () { + // Restoring a backup can be a slow operation so the test may timeout and + // we'll have to retry. + this.retries(5); + // Delay the start of the test, if this is a retry. + await delay(this.test); + + const key = await getCryptoKey(); + + const output = execSync( + `${backupsCmd} restoreBackupWithEncryptionKey ${INSTANCE_ID} ${ENCRYPTED_RESTORE_DATABASE_ID} ${ENCRYPTED_BACKUP_ID} ${PROJECT_ID} ${key.name}` + ); + assert.match(output, /Database restored from backup./); + assert.match( + output, + new RegExp( + `Database (.+) was restored to ${ENCRYPTED_RESTORE_DATABASE_ID} from backup ` + + `(.+)${ENCRYPTED_BACKUP_ID} using encryption key ${key.name}` + ) + ); + }); + // list_database_operations it('should list database operations in the instance', async () => { const output = execSync( diff --git a/src/backup.ts b/src/backup.ts index 9ee5052e7..a74b95a7e 100644 --- a/src/backup.ts +++ b/src/backup.ts @@ -52,6 +52,7 @@ export interface CreateBackupOptions { databasePath: string; expireTime: string | number | p.ITimestamp | PreciseDate; versionTime?: string | number | p.ITimestamp | PreciseDate; + encryptionConfig?: databaseAdmin.spanner.admin.database.v1.ICreateBackupEncryptionConfig; gaxOptions?: CallOptions; } @@ -122,6 +123,10 @@ class Backup { * expireTime The expire time of the backup. * @property {string|number|google.protobuf.Timestamp|external:PreciseDate} * versionTime Take a backup of the state of the database at this time. + * @property {databaseAdmin.spanner.admin.database.v1.ICreateBackupEncryptionConfig} + * encryptionConfig An encryption configuration describing the + * encryption type and key resources in Cloud KMS to be used to encrypt + * the backup. * @property {CallOptions} [gaxOptions] The request configuration options * outlined here: * https://googleapis.github.io/gax-nodejs/classes/CallSettings.html. @@ -165,6 +170,10 @@ class Backup { * databasePath: 'projects/my-project/instances/my-instance/databases/my-database', * expireTime: expireTime, * versionTime: versionTime, + * encryptionConfig: { + * encryptionType: 'CUSTOMER_MANAGED_ENCRYPTION', + * kmsKeyName: 'projects/my-project-id/my-region/keyRings/my-key-ring/cryptoKeys/my-key', + * }, * }); * // Await completion of the backup operation. * await backupOperation.promise(); @@ -188,6 +197,12 @@ class Backup { options.versionTime ).toStruct(); } + if ( + 'encryptionConfig' in options && + (options as CreateBackupOptions).encryptionConfig + ) { + reqOpts.encryptionConfig = (options as CreateBackupOptions).encryptionConfig; + } this.request( { client: 'DatabaseAdminClient', diff --git a/src/database.ts b/src/database.ts index f07ba2919..b936e5d98 100644 --- a/src/database.ts +++ b/src/database.ts @@ -232,6 +232,12 @@ interface DatabaseRequest { (config: RequestConfig, callback: RequestCallback): void; (config: RequestConfig, callback: RequestCallback): void; } + +export interface RestoreOptions { + encryptionConfig?: databaseAdmin.spanner.admin.database.v1.IRestoreDatabaseEncryptionConfig; + gaxOptions?: CallOptions; +} + /** * Create a Database object to interact with a Cloud Spanner database. * @@ -1834,14 +1840,24 @@ class Database extends common.GrpcServiceObject { restore(backupPath: string): Promise; restore( backupPath: string, - options?: CallOptions + options?: RestoreOptions | CallOptions ): Promise; restore(backupPath: string, callback: RestoreDatabaseCallback): void; restore( backupPath: string, - options: CallOptions, + options: RestoreOptions | CallOptions, callback: RestoreDatabaseCallback ): void; + /** + * @typedef {object} RestoreOptions + * @property {databaseAdmin.spanner.admin.database.v1.IRestoreDatabaseEncryptionConfig} + * encryptionConfig An encryption configuration describing + * the encryption type and key resources in Cloud KMS used to + * encrypt/decrypt the database to restore to. + * @property {CallOptions} [gaxOptions] The request configuration options + * outlined here: + * https://googleapis.github.io/gax-nodejs/classes/CallSettings.html. + */ /** * @typedef {array} RestoreDatabaseResponse * @property {Database} 0 The new {@link Database}. @@ -1877,25 +1893,52 @@ class Database extends common.GrpcServiceObject { * const [, restoreOperation] = await database.restore(backupName); * // Wait for restore to complete * await restoreOperation.promise(); + * + * //- + * // Restore database with a different encryption key to the one used by the + * // backup. + * //- + * const [, restoreWithKeyOperation] = await database.restore( + * backupName, + * { + * encryptionConfig: { + * encryptionType: 'CUSTOMER_MANAGED_ENCRYPTION', + * kmsKeyName: 'projects/my-project-id/my-region/keyRings/my-key-ring/cryptoKeys/my-key', + * } + * }, + * ); + * // Wait for restore to complete + * await restoreWithKeyOperation.promise(); */ restore( backupName: string, - optionsOrCallback?: CallOptions | RestoreDatabaseCallback, + optionsOrCallback?: RestoreOptions | CallOptions | RestoreDatabaseCallback, cb?: RestoreDatabaseCallback ): Promise | void { + const options = + typeof optionsOrCallback === 'object' ? optionsOrCallback : {}; const callback = typeof optionsOrCallback === 'function' ? (optionsOrCallback as RestoreDatabaseCallback) : cb; const gaxOpts = - typeof optionsOrCallback === 'object' - ? (optionsOrCallback as CallOptions) - : {}; + 'gaxOptions' in options + ? (options as RestoreOptions).gaxOptions + : (options as CallOptions); + const reqOpts: databaseAdmin.spanner.admin.database.v1.IRestoreDatabaseRequest = { parent: this.instance.formattedName_, databaseId: this.id, backup: Backup.formatName_(this.instance.formattedName_, backupName), }; + + if ( + 'encryptionConfig' in options && + (options as RestoreOptions).encryptionConfig + ) { + reqOpts.encryptionConfig = (options as RestoreOptions).encryptionConfig; + } + return this.request( { client: 'DatabaseAdminClient', diff --git a/test/backup.ts b/test/backup.ts index 2665a2581..da7c5b2ee 100644 --- a/test/backup.ts +++ b/test/backup.ts @@ -28,6 +28,8 @@ import * as bu from '../src/backup'; import {GetMetadataResponse} from '../src/backup'; import {grpc} from 'google-gax'; import {CLOUD_RESOURCE_HEADER} from '../src/common'; +import {google} from '../protos/protos'; +import EncryptionType = google.spanner.admin.database.v1.CreateBackupEncryptionConfig.EncryptionType; let promisified = false; // let callbackified = false; @@ -206,6 +208,27 @@ describe('Backup', () => { ); }); + it('should accept an encryption config', done => { + const encryptionConfig = { + encryptionType: EncryptionType.CUSTOMER_MANAGED_ENCRYPTION, + kmsKeyName: 'some/key/path', + }; + + backup.request = config => { + assert.strictEqual(config.reqOpts.encryptionConfig, encryptionConfig); + done(); + }; + + backup.create( + { + databasePath: DATABASE_FORMATTED_NAME, + expireTime: BACKUP_EXPIRE_TIME, + encryptionConfig, + }, + assert.ifError + ); + }); + it('should call Spanner.timestamp() with expireTime', done => { const spanner_timestamp_ = Spanner.timestamp; diff --git a/test/database.ts b/test/database.ts index 9148b3d06..580db7148 100644 --- a/test/database.ts +++ b/test/database.ts @@ -32,6 +32,8 @@ import {Instance} from '../src'; import {MockError} from './mockserver/mockspanner'; import {IOperation} from '../src/instance'; import {CLOUD_RESOURCE_HEADER} from '../src/common'; +import {google} from '../protos/protos'; +import EncryptionType = google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.EncryptionType; let promisified = false; const fakePfy = extend({}, pfy, { @@ -2727,13 +2729,49 @@ describe('Database', () => { database.restore(BACKUP_NAME, assert.ifError); }); - it('should accept gaxOpts', done => { - const options = { - timeout: 1000, + it('should accept restore options', done => { + const encryptionConfig = { + encryptionType: EncryptionType.CUSTOMER_MANAGED_ENCRYPTION, + kmsKeyName: 'some/key/path', + }; + const options = {encryptionConfig}; + + database.request = config => { + assert.deepStrictEqual( + config.reqOpts.encryptionConfig, + encryptionConfig + ); + done(); + }; + + database.restore(BACKUP_NAME, options, assert.ifError); + }); + + it('should accept gaxOpts as CallOptions', done => { + const gaxOptions = {timeout: 1000}; + + database.request = config => { + assert.deepStrictEqual(config.gaxOpts, gaxOptions); + done(); + }; + + database.restore(BACKUP_NAME, gaxOptions, assert.ifError); + }); + + it('should accept restore and gax options', done => { + const encryptionConfig = { + encryptionType: EncryptionType.CUSTOMER_MANAGED_ENCRYPTION, + kmsKeyName: 'some/key/path', }; + const gaxOptions = {timeout: 1000}; + const options = {gaxOptions, encryptionConfig}; database.request = config => { - assert.deepStrictEqual(config.gaxOpts, options); + assert.deepStrictEqual( + config.reqOpts.encryptionConfig, + encryptionConfig + ); + assert.deepStrictEqual(config.gaxOpts, options.gaxOptions); done(); };