Skip to content

Commit

Permalink
docs: Samples and test for backups admin apis (#2003)
Browse files Browse the repository at this point in the history
This PR contains samples and its integration tests for admin client backups APIs.
  • Loading branch information
alkatrivedi committed Feb 29, 2024
1 parent f34a33a commit 2d99b5b
Show file tree
Hide file tree
Showing 13 changed files with 1,379 additions and 0 deletions.
174 changes: 174 additions & 0 deletions samples/system-test/v2/spanner.test.js
Expand Up @@ -24,6 +24,7 @@ const cp = require('child_process');
const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'});
const instanceCmd = 'node v2/instance.js';
const schemaCmd = 'node v2/schema.js';
const backupsCmd = 'node v2/backups.js';
const datatypesCmd = 'node v2/datatypes.js';
const createTableWithForeignKeyDeleteCascadeCommand =
'node v2/table-create-with-foreign-key-delete-cascade.js';
Expand Down Expand Up @@ -449,6 +450,179 @@ describe('Autogenerated Admin Clients', () => {
);
});

// create_backup
it('should create a backup of the database', async () => {
const instance = spanner.instance(INSTANCE_ID);
const database = instance.database(DATABASE_ID);
const query = {
sql: 'SELECT CURRENT_TIMESTAMP() as Timestamp',
};
const [rows] = await database.run(query);
const versionTime = rows[0].toJSON().Timestamp.toISOString();

const output = execSync(
`${backupsCmd} createBackup ${INSTANCE_ID} ${DATABASE_ID} ${BACKUP_ID} ${PROJECT_ID} ${versionTime}`
);
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}`);
});

// copy_backup
it('should create a copy of a backup', async () => {
const sourceBackupPath = `projects/${PROJECT_ID}/instances/${INSTANCE_ID}/backups/${BACKUP_ID}`;
const output = execSync(
`node v2/backups-copy.js ${INSTANCE_ID} ${COPY_BACKUP_ID} ${sourceBackupPath} ${PROJECT_ID}`
);
assert.match(
output,
new RegExp(`(.*)Backup copy(.*)${COPY_BACKUP_ID} of size(.*)`)
);
});

// cancel_backup
it('should cancel a backup of the database using', async () => {
const output = execSync(
`${backupsCmd} cancelBackup ${INSTANCE_ID} ${DATABASE_ID} ${CANCELLED_BACKUP_ID} ${PROJECT_ID}`
);
assert.match(output, /Backup cancelled./);
});
// get_backups
it('should list backups in the instance', async () => {
const output = execSync(
`${backupsCmd} getBackups ${INSTANCE_ID} ${DATABASE_ID} ${BACKUP_ID} ${PROJECT_ID}`
);
assert.include(output, 'All backups:');
assert.include(output, 'Backups matching backup name:');
assert.include(output, 'Backups expiring within 30 days:');
assert.include(output, 'Backups matching database name:');
assert.include(output, 'Backups filtered by size:');
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, 12);
});

// list_backup_operations
it('should list backup operations in the instance', async () => {
const output = execSync(
`${backupsCmd} getBackupOperations ${INSTANCE_ID} ${DATABASE_ID} ${BACKUP_ID} ${PROJECT_ID}`
);
assert.match(output, /Create Backup Operations:/);
assert.match(
output,
new RegExp(
`Backup (.+)${BACKUP_ID} on database (.+)${DATABASE_ID} is 100% complete.`
)
);
assert.match(output, /Copy Backup Operations:/);
assert.match(
output,
new RegExp(
`Backup (.+)${COPY_BACKUP_ID} copied from source backup (.+)${BACKUP_ID} is 100% complete`
)
);
});

// update_backup_expire_time
it('should update the expire time of a backup', async () => {
const output = execSync(
`${backupsCmd} updateBackup ${INSTANCE_ID} ${BACKUP_ID} ${PROJECT_ID}`
);
assert.match(output, /Expire time updated./);
});

// restore_backup
it('should restore database from a backup', 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 output = execSync(
`${backupsCmd} restoreBackup ${INSTANCE_ID} ${RESTORE_DATABASE_ID} ${BACKUP_ID} ${PROJECT_ID}`
);
assert.match(output, /Database restored from backup./);
assert.match(
output,
new RegExp(
`Database (.+) was restored to ${RESTORE_DATABASE_ID} from backup ` +
`(.+)${BACKUP_ID} with version time (.+)`
)
);
});

// delete_backup
it('should delete a backup', async () => {
function sleep(timeMillis) {
return new Promise(resolve => setTimeout(resolve, timeMillis));
}

// Wait for database to finish optimizing - cannot delete a backup if a database restored from it
const instance = spanner.instance(INSTANCE_ID);
const database = instance.database(RESTORE_DATABASE_ID);

while ((await database.getState()) === 'READY_OPTIMIZING') {
await sleep(1000);
}

const output = execSync(
`${backupsCmd} deleteBackup ${INSTANCE_ID} ${BACKUP_ID} ${PROJECT_ID}`
);
assert.match(output, /Backup deleted./);
});

// 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(
`${backupsCmd} getDatabaseOperations ${INSTANCE_ID} ${PROJECT_ID}`
);
assert.match(output, /Optimize Database Operations:/);
assert.match(
output,
new RegExp(
`Database (.+)${RESTORE_DATABASE_ID} restored from backup is (\\d+)% ` +
'optimized'
)
);
});

// create_database_with_version_retention_period
it('should create a database with a version retention period', async () => {
const output = execSync(
Expand Down
83 changes: 83 additions & 0 deletions samples/v2/backups-cancel.js
@@ -0,0 +1,83 @@
/**
* Copyright 2024 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 cancelBackup(instanceId, databaseId, backupId, projectId) {
// [START spanner_cancel_backup_create]

// Imports the Google Cloud client library and precise date library
const {Spanner, protos} = 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';

// Creates a client
const spanner = new Spanner({
projectId: projectId,
});

// Gets a reference to a Cloud Spanner Database Admin Client object
const databaseAdminClient = spanner.getDatabaseAdminClient();

// Creates a new backup of the database
try {
console.log(
`Creating backup of database ${databaseAdminClient.databasePath(
projectId,
instanceId,
databaseId
)}.`
);

// Expire backup one day in the future
const expireTime = Date.now() + 1000 * 60 * 60 * 24;
const [operation] = await databaseAdminClient.createBackup({
parent: databaseAdminClient.instancePath(projectId, instanceId),
backupId: backupId,
backup: (protos.google.spanner.admin.database.v1.Backup = {
database: databaseAdminClient.databasePath(
projectId,
instanceId,
databaseId
),
expireTime: Spanner.timestamp(expireTime).toStruct(),
name: databaseAdminClient.backupPath(projectId, instanceId, backupId),
}),
});

// Cancel the backup
await operation.cancel();

console.log('Backup cancelled.');
} catch (err) {
console.error('ERROR:', err);
} finally {
// Delete backup in case it got created before the cancel operation
await databaseAdminClient.deleteBackup({
name: databaseAdminClient.backupPath(projectId, instanceId, backupId),
});
// Close the spanner client when finished.
// The databaseAdminClient does not require explicit closure. The closure of the Spanner client will automatically close the databaseAdminClient.
spanner.close();
}
// [END spanner_cancel_backup_create]
}

module.exports.cancelBackup = cancelBackup;
100 changes: 100 additions & 0 deletions samples/v2/backups-copy.js
@@ -0,0 +1,100 @@
// Copyright 2024 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.

// sample-metadata:
// title: Copies a source backup
// usage: node spannerCopyBackup <INSTANCE_ID> <COPY_BACKUP_ID> <SOURCE_BACKUP_ID> <PROJECT_ID>

'use strict';

function main(
instanceId = 'my-instance',
backupId = 'my-backup',
sourceBackupPath = 'projects/my-project-id/instances/my-source-instance/backups/my-source-backup',
projectId = 'my-project-id'
) {
// [START spanner_copy_backup]
/**
* TODO(developer): Uncomment these variables before running the sample.
*/
// const instanceId = 'my-instance';
// const backupId = 'my-backup',
// const sourceBackupPath = 'projects/my-project-id/instances/my-source-instance/backups/my-source-backup',
// const projectId = 'my-project-id';

// Imports the Google Cloud Spanner client library
const {Spanner} = require('@google-cloud/spanner');
const {PreciseDate} = require('@google-cloud/precise-date');

// Creates a client
const spanner = new Spanner({
projectId: projectId,
});

// Gets a reference to a Cloud Spanner Database Admin Client object
const databaseAdminClient = spanner.getDatabaseAdminClient();

async function spannerCopyBackup() {
// Expire copy backup 14 days in the future
const expireTime = Spanner.timestamp(
Date.now() + 1000 * 60 * 60 * 24 * 14
).toStruct();

// Copy the source backup
try {
console.log(`Creating copy of the source backup ${sourceBackupPath}.`);
const [operation] = await databaseAdminClient.copyBackup({
parent: databaseAdminClient.instancePath(projectId, instanceId),
sourceBackup: sourceBackupPath,
backupId: backupId,
expireTime: expireTime,
});

console.log(
`Waiting for backup copy ${databaseAdminClient.backupPath(
projectId,
instanceId,
backupId
)} to complete...`
);
await operation.promise();

// Verify the copy backup is ready
const [copyBackup] = await databaseAdminClient.getBackup({
name: databaseAdminClient.backupPath(projectId, instanceId, backupId),
});

if (copyBackup.state === 'READY') {
console.log(
`Backup copy ${copyBackup.name} of size ` +
`${copyBackup.sizeBytes} bytes was created at ` +
`${new PreciseDate(copyBackup.createTime).toISOString()} ` +
'with version time ' +
`${new PreciseDate(copyBackup.versionTime).toISOString()}`
);
} else {
console.error('ERROR: Copy of backup is not ready.');
}
} catch (err) {
console.error('ERROR:', err);
}
}
spannerCopyBackup();
// [END spanner_copy_backup]
}
process.on('unhandledRejection', err => {
console.error(err.message);
process.exitCode = 1;
});
main(...process.argv.slice(2));

0 comments on commit 2d99b5b

Please sign in to comment.