Skip to content

Commit 494a996

Browse files
authored
docs: Samples and tests for Instance Admin Client APIs (#1987)
This PR contains sample files and their integration tests for the instance admin client APIs.
1 parent e2fe5b7 commit 494a996

9 files changed

+960
-0
lines changed
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const {Spanner} = require('@google-cloud/spanner');
18+
const pLimit = require('p-limit');
19+
const {describe, it, before, after, afterEach} = require('mocha');
20+
const {assert} = require('chai');
21+
const cp = require('child_process');
22+
23+
const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'});
24+
const instanceCmd = 'node v2/instance.js';
25+
26+
const CURRENT_TIME = Math.round(Date.now() / 1000).toString();
27+
const PROJECT_ID = process.env.GCLOUD_PROJECT;
28+
const PREFIX = 'test-instance';
29+
const SAMPLE_INSTANCE_ID = `${PREFIX}-my-sample-instance-${CURRENT_TIME}`;
30+
const SAMPLE_INSTANCE_CONFIG_ID = `custom-my-sample-instance-config-${CURRENT_TIME}`;
31+
const BASE_INSTANCE_CONFIG_ID = 'regional-us-central1';
32+
const INSTANCE_ID =
33+
process.env.SPANNERTEST_INSTANCE || `${PREFIX}-${CURRENT_TIME}`;
34+
const DATABASE_ID = `test-database-${CURRENT_TIME}`;
35+
const INSTANCE_ALREADY_EXISTS = !!process.env.SPANNERTEST_INSTANCE;
36+
const PG_DATABASE_ID = `test-pg-database-${CURRENT_TIME}`;
37+
const RESTORE_DATABASE_ID = `test-database-${CURRENT_TIME}-r`;
38+
const ENCRYPTED_RESTORE_DATABASE_ID = `test-database-${CURRENT_TIME}-r-enc`;
39+
const BACKUP_ID = `test-backup-${CURRENT_TIME}`;
40+
const COPY_BACKUP_ID = `test-copy-backup-${CURRENT_TIME}`;
41+
const ENCRYPTED_BACKUP_ID = `test-backup-${CURRENT_TIME}-enc`;
42+
const CANCELLED_BACKUP_ID = `test-backup-${CURRENT_TIME}-c`;
43+
const LOCATION_ID = 'regional-us-central1';
44+
45+
const spanner = new Spanner({
46+
projectId: PROJECT_ID,
47+
});
48+
49+
const LABEL = 'node-sample-tests';
50+
const GAX_OPTIONS = {
51+
retry: {
52+
retryCodes: [4, 8, 14],
53+
backoffSettings: {
54+
initialRetryDelayMillis: 1000,
55+
retryDelayMultiplier: 1.3,
56+
maxRetryDelayMillis: 32000,
57+
initialRpcTimeoutMillis: 60000,
58+
rpcTimeoutMultiplier: 1,
59+
maxRpcTimeoutMillis: 60000,
60+
totalTimeoutMillis: 600000,
61+
},
62+
},
63+
};
64+
65+
const delay = async test => {
66+
const retries = test.currentRetry();
67+
// No retry on the first failure.
68+
if (retries === 0) return;
69+
// See: https://cloud.google.com/storage/docs/exponential-backoff
70+
const ms = Math.pow(2, retries) + Math.random() * 1000;
71+
return new Promise(done => {
72+
console.info(`retrying "${test.title}" in ${ms}ms`);
73+
setTimeout(done, ms);
74+
});
75+
};
76+
77+
async function deleteStaleInstances() {
78+
let [instances] = await spanner.getInstances({
79+
filter: `(labels.${LABEL}:true) OR (labels.cloud_spanner_samples:true)`,
80+
});
81+
const old = new Date();
82+
old.setHours(old.getHours() - 4);
83+
84+
instances = instances.filter(instance => {
85+
return (
86+
instance.metadata.labels['created'] &&
87+
new Date(parseInt(instance.metadata.labels['created']) * 1000) < old
88+
);
89+
});
90+
const limit = pLimit(5);
91+
await Promise.all(
92+
instances.map(instance =>
93+
limit(() => setTimeout(deleteInstance, delay, instance))
94+
)
95+
);
96+
}
97+
98+
async function deleteInstance(instance) {
99+
const [backups] = await instance.getBackups();
100+
await Promise.all(backups.map(backup => backup.delete(GAX_OPTIONS)));
101+
return instance.delete(GAX_OPTIONS);
102+
}
103+
104+
describe('Autogenerated Admin Clients', () => {
105+
const instance = spanner.instance(INSTANCE_ID);
106+
107+
before(async () => {
108+
await deleteStaleInstances();
109+
110+
if (!INSTANCE_ALREADY_EXISTS) {
111+
const [, operation] = await instance.create({
112+
config: LOCATION_ID,
113+
nodes: 1,
114+
labels: {
115+
[LABEL]: 'true',
116+
created: CURRENT_TIME,
117+
},
118+
gaxOptions: GAX_OPTIONS,
119+
});
120+
return operation.promise();
121+
} else {
122+
console.log(
123+
`Not creating temp instance, using + ${instance.formattedName_}...`
124+
);
125+
}
126+
});
127+
128+
after(async () => {
129+
const instance = spanner.instance(INSTANCE_ID);
130+
131+
if (!INSTANCE_ALREADY_EXISTS) {
132+
// Make sure all backups are deleted before an instance can be deleted.
133+
await Promise.all([
134+
instance.backup(BACKUP_ID).delete(GAX_OPTIONS),
135+
instance.backup(ENCRYPTED_BACKUP_ID).delete(GAX_OPTIONS),
136+
instance.backup(COPY_BACKUP_ID).delete(GAX_OPTIONS),
137+
instance.backup(CANCELLED_BACKUP_ID).delete(GAX_OPTIONS),
138+
]);
139+
await instance.delete(GAX_OPTIONS);
140+
} else {
141+
await Promise.all([
142+
instance.database(DATABASE_ID).delete(),
143+
instance.database(PG_DATABASE_ID).delete(),
144+
instance.database(RESTORE_DATABASE_ID).delete(),
145+
instance.database(ENCRYPTED_RESTORE_DATABASE_ID).delete(),
146+
instance.backup(BACKUP_ID).delete(GAX_OPTIONS),
147+
instance.backup(COPY_BACKUP_ID).delete(GAX_OPTIONS),
148+
instance.backup(ENCRYPTED_BACKUP_ID).delete(GAX_OPTIONS),
149+
instance.backup(CANCELLED_BACKUP_ID).delete(GAX_OPTIONS),
150+
]);
151+
}
152+
await spanner.instance(SAMPLE_INSTANCE_ID).delete(GAX_OPTIONS);
153+
});
154+
describe('instance', () => {
155+
afterEach(async () => {
156+
const sample_instance = spanner.instance(SAMPLE_INSTANCE_ID);
157+
await sample_instance.delete();
158+
});
159+
160+
// create_instance_using_instance_admin_client
161+
it('should create an example instance', async () => {
162+
const output = execSync(
163+
`${instanceCmd} createInstance "${SAMPLE_INSTANCE_ID}" ${PROJECT_ID}`
164+
);
165+
assert.match(
166+
output,
167+
new RegExp(
168+
`Waiting for operation on ${SAMPLE_INSTANCE_ID} to complete...`
169+
)
170+
);
171+
assert.match(
172+
output,
173+
new RegExp(`Created instance ${SAMPLE_INSTANCE_ID}.`)
174+
);
175+
});
176+
177+
// create_instance_with_processing_units
178+
it('should create an example instance with processing units', async () => {
179+
const output = execSync(
180+
`${instanceCmd} createInstanceWithProcessingUnits "${SAMPLE_INSTANCE_ID}" ${PROJECT_ID}`
181+
);
182+
assert.match(
183+
output,
184+
new RegExp(
185+
`Waiting for operation on ${SAMPLE_INSTANCE_ID} to complete...`
186+
)
187+
);
188+
assert.match(
189+
output,
190+
new RegExp(`Created instance ${SAMPLE_INSTANCE_ID}.`)
191+
);
192+
assert.match(
193+
output,
194+
new RegExp(`Instance ${SAMPLE_INSTANCE_ID} has 500 processing units.`)
195+
);
196+
});
197+
});
198+
199+
describe('leader options', () => {
200+
before(async () => {
201+
const instance = spanner.instance(SAMPLE_INSTANCE_ID);
202+
const [, operation] = await instance.create({
203+
config: 'nam6',
204+
nodes: 1,
205+
displayName: 'Multi-region options test',
206+
labels: {
207+
['cloud_spanner_samples']: 'true',
208+
created: Math.round(Date.now() / 1000).toString(), // current time
209+
},
210+
});
211+
await operation.promise();
212+
});
213+
214+
after(async () => {
215+
const instance = spanner.instance(SAMPLE_INSTANCE_ID);
216+
await instance.delete();
217+
});
218+
219+
// create_instance_config
220+
it('should create an example custom instance config', async () => {
221+
const output = execSync(
222+
`node v2/instance-config-create.js ${SAMPLE_INSTANCE_CONFIG_ID} ${BASE_INSTANCE_CONFIG_ID} ${PROJECT_ID}`
223+
);
224+
assert.match(
225+
output,
226+
new RegExp(
227+
`Waiting for create operation for ${SAMPLE_INSTANCE_CONFIG_ID} to complete...`
228+
)
229+
);
230+
assert.match(
231+
output,
232+
new RegExp(`Created instance config ${SAMPLE_INSTANCE_CONFIG_ID}.`)
233+
);
234+
});
235+
236+
// update_instance_config
237+
it('should update an example custom instance config', async () => {
238+
const output = execSync(
239+
`node v2/instance-config-update.js ${SAMPLE_INSTANCE_CONFIG_ID} ${PROJECT_ID}`
240+
);
241+
assert.match(
242+
output,
243+
new RegExp(
244+
`Waiting for update operation for ${SAMPLE_INSTANCE_CONFIG_ID} to complete...`
245+
)
246+
);
247+
assert.match(
248+
output,
249+
new RegExp(`Updated instance config ${SAMPLE_INSTANCE_CONFIG_ID}.`)
250+
);
251+
});
252+
253+
// delete_instance_config
254+
it('should delete an example custom instance config', async () => {
255+
const output = execSync(
256+
`node instance-config-delete.js ${SAMPLE_INSTANCE_CONFIG_ID} ${PROJECT_ID}`
257+
);
258+
assert.match(
259+
output,
260+
new RegExp(`Deleting ${SAMPLE_INSTANCE_CONFIG_ID}...`)
261+
);
262+
assert.match(
263+
output,
264+
new RegExp(`Deleted instance config ${SAMPLE_INSTANCE_CONFIG_ID}.`)
265+
);
266+
});
267+
268+
// list_instance_config_operations
269+
it('should list all instance config operations', async () => {
270+
const output = execSync(
271+
`node v2/instance-config-get-operations.js ${PROJECT_ID}`
272+
);
273+
assert.match(
274+
output,
275+
new RegExp(
276+
`Getting list of instance config operations on project ${PROJECT_ID}...\n`
277+
)
278+
);
279+
assert.match(
280+
output,
281+
new RegExp(
282+
`Available instance config operations for project ${PROJECT_ID}:`
283+
)
284+
);
285+
assert.include(output, 'Instance config operation for');
286+
assert.include(
287+
output,
288+
'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceConfigMetadata'
289+
);
290+
});
291+
292+
// list_instance_configs
293+
it('should list available instance configs', async () => {
294+
const output = execSync(`node v2/list-instance-configs.js ${PROJECT_ID}`);
295+
assert.match(
296+
output,
297+
new RegExp(`Available instance configs for project ${PROJECT_ID}:`)
298+
);
299+
assert.include(output, 'Available leader options for instance config');
300+
});
301+
302+
// get_instance_config
303+
// TODO: Enable when the feature has been released.
304+
it.skip('should get a specific instance config', async () => {
305+
const output = execSync(`node v2/get-instance-config.js ${PROJECT_ID}`);
306+
assert.include(output, 'Available leader options for instance config');
307+
});
308+
});
309+
});

samples/v2/get-instance-config.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Copyright 2024 Google LLC
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
// sample-metadata:
17+
// title: Gets the instance config metadata for the configuration nam6
18+
// usage: node get-instance-config.js <PROJECT_ID> <INSTANCE_CONFIG_ID>
19+
20+
'use strict';
21+
22+
function main(projectId) {
23+
// [START spanner_get_instance_config]
24+
25+
/**
26+
* TODO(developer): Uncomment the following line before running the sample.
27+
*/
28+
// const projectId = 'my-project-id';
29+
30+
// Imports the Google Cloud client library
31+
const {Spanner} = require('@google-cloud/spanner');
32+
33+
// Creates a client
34+
const spanner = new Spanner({
35+
projectId: projectId,
36+
});
37+
38+
const instanceAdminClient = spanner.getInstanceAdminClient();
39+
40+
async function getInstanceConfig() {
41+
// Get the instance config for the multi-region North America 6 (NAM6).
42+
// See https://cloud.google.com/spanner/docs/instance-configurations#configuration for a list of all available
43+
// configurations.
44+
const [instanceConfig] = await instanceAdminClient.getInstanceConfig({
45+
name: instanceAdminClient.instanceConfigPath(projectId, 'nam6'),
46+
});
47+
console.log(
48+
`Available leader options for instance config ${instanceConfig.name} ('${
49+
instanceConfig.displayName
50+
}'):
51+
${instanceConfig.leaderOptions.join()}`
52+
);
53+
}
54+
getInstanceConfig();
55+
// [END spanner_get_instance_config]
56+
}
57+
process.on('unhandledRejection', err => {
58+
console.error(err.message);
59+
process.exitCode = 1;
60+
});
61+
main(...process.argv.slice(2));

0 commit comments

Comments
 (0)