From ab303426e5923602f57f32cfb6557960c74dd65d Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 3 Sep 2025 07:40:50 +0000 Subject: [PATCH 01/16] feat(storagecontrol): Add samples for Storage Anywhere Cache --- storage-control/createAnywhereCache.js | 106 ++++++++++++++++++++++++ storage-control/disableAnywhereCache.js | 60 ++++++++++++++ storage-control/getAnywhereCache.js | 60 ++++++++++++++ storage-control/listAnywhereCaches.js | 56 +++++++++++++ storage-control/package.json | 4 +- storage-control/pauseAnywhereCache.js | 60 ++++++++++++++ storage-control/resumeAnywhereCache.js | 60 ++++++++++++++ storage-control/updateAnywhereCache.js | 71 ++++++++++++++++ 8 files changed, 475 insertions(+), 2 deletions(-) create mode 100644 storage-control/createAnywhereCache.js create mode 100644 storage-control/disableAnywhereCache.js create mode 100644 storage-control/getAnywhereCache.js create mode 100644 storage-control/listAnywhereCaches.js create mode 100644 storage-control/pauseAnywhereCache.js create mode 100644 storage-control/resumeAnywhereCache.js create mode 100644 storage-control/updateAnywhereCache.js diff --git a/storage-control/createAnywhereCache.js b/storage-control/createAnywhereCache.js new file mode 100644 index 00000000000..cd3c691f7ba --- /dev/null +++ b/storage-control/createAnywhereCache.js @@ -0,0 +1,106 @@ +// Copyright 2025 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 +// +// https://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'; + +function main(bucketName, cacheName, zoneName) { + // [START storage_control_create_anywhere_cache] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + + // The name of your GCS bucket + // const bucketName = 'bucketName'; + + // The name of the cache to be created + // const cacheName = 'cacheName'; + + // The zone that the cache instance will run in. + // const zoneName = 'zoneName'; + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callCreateAnywhereCache() { + const bucketPath = controlClient.bucketPath('_', bucketName); + + // Create the request + const request = { + parent: bucketPath, + anywhereCache: { + name: cacheName, + zone: zoneName, + }, + }; + + // Run the request, which returns an Operation object + const [operation] = await controlClient.createAnywhereCache(request); + console.log(`Waiting for operation ${operation.name} to complete...`); + + // Wait for the operation to complete and get the final resource + const anywhereCache = await checkCreateAnywhereCacheProgress( + operation.name + ); + console.log(`Created anywhere cache: ${anywhereCache.result.name}.`); + } + + // A custom function to check the operation's progress. + async function checkCreateAnywhereCacheProgress(operationName) { + let operation = {done: false}; + console.log('Starting manual polling for operation...'); + + // Poll the operation until it's done. + while (!operation.done) { + await new Promise(resolve => setTimeout(resolve, 180000)); // Wait for 3 minutes before the next check. + const request = { + name: operationName, + }; + try { + const [latestOperation] = await controlClient.getOperation(request); + operation = latestOperation; + } catch (err) { + // Handle potential errors during polling. + console.error('Error while polling:', err.message); + break; // Exit the loop on error. + } + } + + // Return the final result of the operation. + if (operation.response) { + // Decode the operation response into a usable Operation object + const decodeOperation = new controlClient._gaxModule.Operation( + operation, + controlClient.descriptors.longrunning.createAnywhereCache, + controlClient._gaxModule.createDefaultBackoffSettings() + ); + // Return the decoded operation + return decodeOperation; + } else { + // If there's no response, it indicates an issue, so throw an error + throw new Error('Operation completed without a response.'); + } + } + + callCreateAnywhereCache(); + // [END storage_control_create_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/disableAnywhereCache.js b/storage-control/disableAnywhereCache.js new file mode 100644 index 00000000000..3259812f338 --- /dev/null +++ b/storage-control/disableAnywhereCache.js @@ -0,0 +1,60 @@ +// Copyright 2025 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 +// +// https://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'; + +function main(bucketName, cacheName) { + // [START storage_control_disable_anywhere_cache] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + + // The name of your GCS bucket + // const bucketName = 'bucketName'; + + // The name of the cache to be disabled + // const cacheName = 'cacheName'; + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callDisableAnywhereCache() { + const anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + + // Create the request + const request = { + name: anywhereCachePath, + }; + + // Run request + const [response] = await controlClient.disableAnywhereCache(request); + console.log(`Disabled anywhere cache: ${response.name}.`); + } + + callDisableAnywhereCache(); + // [END storage_control_disable_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/getAnywhereCache.js b/storage-control/getAnywhereCache.js new file mode 100644 index 00000000000..fb30ae304e1 --- /dev/null +++ b/storage-control/getAnywhereCache.js @@ -0,0 +1,60 @@ +// Copyright 2025 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 +// +// https://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'; + +function main(bucketName, cacheName) { + // [START storage_control_get_anywhere_cache] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + + // The name of your GCS bucket + // const bucketName = 'bucketName'; + + // The name of the cache to get + // const cacheName = 'cacheName'; + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callGetAnywhereCache() { + const anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + + // Create the request + const request = { + name: anywhereCachePath, + }; + + // Run request + const [response] = await controlClient.getAnywhereCache(request); + console.log(`Got anywhere cache: ${response.name}.`); + } + + callGetAnywhereCache(); + // [END storage_control_get_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/listAnywhereCaches.js b/storage-control/listAnywhereCaches.js new file mode 100644 index 00000000000..40c44568858 --- /dev/null +++ b/storage-control/listAnywhereCaches.js @@ -0,0 +1,56 @@ +// Copyright 2025 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 +// +// https://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'; + +function main(bucketName) { + // [START storage_control_list_anywhere_caches] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + + // The name of your GCS bucket + // const bucketName = 'bucketName'; + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callListAnywhereCaches() { + const bucketPath = controlClient.bucketPath('_', bucketName); + + // Create the request + const request = { + parent: bucketPath, + }; + + // Run request + const [response] = await controlClient.listAnywhereCaches(request); + for (const anywhereCache of response) { + console.log(anywhereCache.name); + } + } + + callListAnywhereCaches(); + // [END storage_control_list_anywhere_caches] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/package.json b/storage-control/package.json index 0f6d9328455..98f61559c5d 100644 --- a/storage-control/package.json +++ b/storage-control/package.json @@ -12,8 +12,8 @@ "author": "Google Inc.", "license": "Apache-2.0", "devDependencies": { - "@google-cloud/storage": "^7.12.0", - "@google-cloud/storage-control": "^0.2.0", + "@google-cloud/storage": "^7.17.0", + "@google-cloud/storage-control": "^0.5.0", "c8": "^10.0.0", "chai": "^4.5.0", "mocha": "^10.7.0", diff --git a/storage-control/pauseAnywhereCache.js b/storage-control/pauseAnywhereCache.js new file mode 100644 index 00000000000..8663c9553d6 --- /dev/null +++ b/storage-control/pauseAnywhereCache.js @@ -0,0 +1,60 @@ +// Copyright 2025 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 +// +// https://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'; + +function main(bucketName, cacheName) { + // [START storage_control_pause_anywhere_cache] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + + // The name of your GCS bucket + // const bucketName = 'bucketName'; + + // The name of the cache to be paused + // const cacheName = 'cacheName'; + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callPauseAnywhereCache() { + const anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + + // Create the request + const request = { + name: anywhereCachePath, + }; + + // Run request + const [response] = await controlClient.pauseAnywhereCache(request); + console.log(`Paused anywhere cache: ${response.name}.`); + } + + callPauseAnywhereCache(); + // [END storage_control_pause_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/resumeAnywhereCache.js b/storage-control/resumeAnywhereCache.js new file mode 100644 index 00000000000..8418e3129fa --- /dev/null +++ b/storage-control/resumeAnywhereCache.js @@ -0,0 +1,60 @@ +// Copyright 2025 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 +// +// https://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'; + +function main(bucketName, cacheName) { + // [START storage_control_resume_anywhere_cache] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + + // The name of your GCS bucket + // const bucketName = 'bucketName'; + + // The name of the cache to be resumed + // const cacheName = 'cacheName'; + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callResumeAnywhereCache() { + const anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + + // Create the request + const request = { + name: anywhereCachePath, + }; + + // Run request + const [response] = await controlClient.resumeAnywhereCache(request); + console.log(`Resumed anywhere cache: ${response.name}.`); + } + + callResumeAnywhereCache(); + // [END storage_control_resume_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/updateAnywhereCache.js b/storage-control/updateAnywhereCache.js new file mode 100644 index 00000000000..67fcd0c7b66 --- /dev/null +++ b/storage-control/updateAnywhereCache.js @@ -0,0 +1,71 @@ +// Copyright 2025 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 +// +// https://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'; + +function main(bucketName, cacheName, admissionPolicy) { + // [START storage_control_update_anywhere_cache] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + + // The name of your GCS bucket + // const bucketName = 'bucketName'; + + // The name of the cache to be update + // const cacheName = 'cacheName'; + + // The admission policy of the cache to be updated + // const admissionPolicy = 'admit-on-first-miss'; + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callUpdateAnywhereCache() { + const anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + + // Create the request + const request = { + anywhereCache: { + name: anywhereCachePath, + admissionPolicy: admissionPolicy, + }, + updateMask: { + paths: ['admission_policy'], + }, + }; + + // Run request + const [operation] = await controlClient.updateAnywhereCache(request); + const [response] = await operation.promise(); + + console.log(`Updated anywhere cache: ${response.name}.`); + } + + callUpdateAnywhereCache(); + // [END storage_control_update_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); From d5ec75f5adf1ab9b34954c0367f026f9cc1a5d16 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 3 Sep 2025 07:44:46 +0000 Subject: [PATCH 02/16] add testcase --- .../system-test/anywhereCache.test.js | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 storage-control/system-test/anywhereCache.test.js diff --git a/storage-control/system-test/anywhereCache.test.js b/storage-control/system-test/anywhereCache.test.js new file mode 100644 index 00000000000..04c9a5bbb7c --- /dev/null +++ b/storage-control/system-test/anywhereCache.test.js @@ -0,0 +1,134 @@ +// Copyright 2025 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 +// +// https://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. + +const {Storage, Bucket} = require('@google-cloud/storage'); +const {StorageControlClient} = require('@google-cloud/storage-control').v2; +const cp = require('child_process'); +const {assert} = require('chai'); +const {describe, it, before, after} = require('mocha'); +const uuid = require('uuid'); + +const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); +const bucketPrefix = `storage-control-samples-${uuid.v4()}`; +const bucketName = `${bucketPrefix}-a`; +// const bucketName = 'php-gcscontrol-sample-awc-17510023031559095173'; +const controlClient = new StorageControlClient(); +const storage = new Storage({projectId: 'storage-sdk-vendor'}); +const bucket = new Bucket(storage, bucketName); +const location = 'us-west1'; +const cacheName = 'us-west1-c'; +let anywhereCachePath; + +describe('Anywhere Cache', () => { + before(async () => { + await storage.createBucket(bucketName, { + iamConfiguration: { + uniformBucketLevelAccess: { + enabled: true, + }, + }, + hierarchicalNamespace: {enabled: true}, + location: location, + }); + + anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + }); + + after(async function () { + this.timeout(3600000); + let caches = false; + // The `while` loop will continue to run as long as the `caches` flag is `false`. + while (!caches) { + await new Promise(resolve => setTimeout(resolve, 30000)); + const bucketPath = controlClient.bucketPath('_', bucketName); + + try { + // Call the `listAnywhereCaches` method to check for any active caches. + // The response is an array of caches. + const [response] = await controlClient.listAnywhereCaches({ + parent: bucketPath, + }); + // Check if the response array is empty. If so, it means there are no more caches, and we can exit the loop. + if (response.length === 0) { + // Set `caches` to `true` to break out of the `while` loop. + caches = true; + } + } catch (err) { + console.error('Error while polling:', err.message); + break; + } + } + // After the loop has finished (i.e., no more caches are found), we proceed with deleting the bucket. + await bucket.delete(); + }); + + it('should create an anywhere cache', async function () { + this.timeout(3600000); + const output = execSync( + `node createAnywhereCache.js ${bucketName} ${location} ${cacheName}` + ); + assert.match(output, /Created anywhere cache:/); + assert.match(output, new RegExp(anywhereCachePath)); + }); + + it('should get an anywhere cache', async () => { + const output = execSync( + `node getAnywhereCache.js ${bucketName} ${cacheName}` + ); + assert.match(output, /Got anywhere cache:/); + assert.match(output, new RegExp(anywhereCachePath)); + }); + + it('should list anywhere caches', async () => { + const output = execSync(`node listAnywhereCaches.js ${bucketName}`); + assert.match(output, new RegExp(anywhereCachePath)); + }); + + it('should update an anywhere cache', async () => { + const admissionPolicy = 'admit-on-second-miss'; + const output = execSync( + `node updateAnywhereCache.js ${bucketName} ${cacheName} ${admissionPolicy}` + ); + assert.match(output, /Updated anywhere cache:/); + assert.match(output, new RegExp(anywhereCachePath)); + }); + + it('should pause an anywhere cache', async () => { + const output = execSync( + `node pauseAnywhereCache.js ${bucketName} ${cacheName}` + ); + assert.match(output, /Paused anywhere cache:/); + assert.match(output, new RegExp(anywhereCachePath)); + }); + + it('should resume an anywhere cache', async () => { + const output = execSync( + `node resumeAnywhereCache.js ${bucketName} ${cacheName}` + ); + assert.match(output, /Resumed anywhere cache:/); + assert.match(output, new RegExp(anywhereCachePath)); + }); + + it('should disable an anywhere cache', async () => { + const output = execSync( + `node disableAnywhereCache.js ${bucketName} ${cacheName}` + ); + assert.match(output, /Disabled anywhere cache:/); + assert.match(output, new RegExp(anywhereCachePath)); + }); +}); From 3b805ea61eb3433e6e3f10c3005ad844e3e4ffb3 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 3 Sep 2025 07:51:18 +0000 Subject: [PATCH 03/16] lint fix --- storage-control/system-test/anywhereCache.test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/storage-control/system-test/anywhereCache.test.js b/storage-control/system-test/anywhereCache.test.js index 04c9a5bbb7c..a99eeb5f0c1 100644 --- a/storage-control/system-test/anywhereCache.test.js +++ b/storage-control/system-test/anywhereCache.test.js @@ -22,7 +22,6 @@ const uuid = require('uuid'); const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); const bucketPrefix = `storage-control-samples-${uuid.v4()}`; const bucketName = `${bucketPrefix}-a`; -// const bucketName = 'php-gcscontrol-sample-awc-17510023031559095173'; const controlClient = new StorageControlClient(); const storage = new Storage({projectId: 'storage-sdk-vendor'}); const bucket = new Bucket(storage, bucketName); @@ -50,6 +49,9 @@ describe('Anywhere Cache', () => { }); after(async function () { + // Sets the timeout for the test to 3600000 milliseconds (1 hour). + // This is necessary for long-running operations, such as waiting for a + // cache to be disabled, to prevent the test from failing due to a timeout. this.timeout(3600000); let caches = false; // The `while` loop will continue to run as long as the `caches` flag is `false`. @@ -78,6 +80,9 @@ describe('Anywhere Cache', () => { }); it('should create an anywhere cache', async function () { + // Sets the timeout for the test to 3600000 milliseconds (1 hour). + // This is necessary for long-running operations, such as waiting for a + // cache to be created, to prevent the test from failing due to a timeout. this.timeout(3600000); const output = execSync( `node createAnywhereCache.js ${bucketName} ${location} ${cacheName}` From 46ee0b24d897f6d9636c67f138c193572e6c5e13 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 3 Sep 2025 13:57:14 +0000 Subject: [PATCH 04/16] fix: test case --- storage-control/system-test/anywhereCache.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/storage-control/system-test/anywhereCache.test.js b/storage-control/system-test/anywhereCache.test.js index a99eeb5f0c1..49070f101ea 100644 --- a/storage-control/system-test/anywhereCache.test.js +++ b/storage-control/system-test/anywhereCache.test.js @@ -25,7 +25,7 @@ const bucketName = `${bucketPrefix}-a`; const controlClient = new StorageControlClient(); const storage = new Storage({projectId: 'storage-sdk-vendor'}); const bucket = new Bucket(storage, bucketName); -const location = 'us-west1'; +const zoneName = 'us-west1'; const cacheName = 'us-west1-c'; let anywhereCachePath; @@ -38,7 +38,7 @@ describe('Anywhere Cache', () => { }, }, hierarchicalNamespace: {enabled: true}, - location: location, + location: zoneName, }); anywhereCachePath = controlClient.anywhereCachePath( @@ -85,7 +85,7 @@ describe('Anywhere Cache', () => { // cache to be created, to prevent the test from failing due to a timeout. this.timeout(3600000); const output = execSync( - `node createAnywhereCache.js ${bucketName} ${location} ${cacheName}` + `node createAnywhereCache.js ${bucketName} ${cacheName} ${zoneName}` ); assert.match(output, /Created anywhere cache:/); assert.match(output, new RegExp(anywhereCachePath)); From a39cd561a6b1dd5aa27f2fd8f563a680f9cb73fb Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Thu, 4 Sep 2025 06:31:10 +0000 Subject: [PATCH 05/16] fix: remove cacheName --- storage-control/createAnywhereCache.js | 6 +----- storage-control/system-test/anywhereCache.test.js | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/storage-control/createAnywhereCache.js b/storage-control/createAnywhereCache.js index cd3c691f7ba..54a0c3e633a 100644 --- a/storage-control/createAnywhereCache.js +++ b/storage-control/createAnywhereCache.js @@ -14,7 +14,7 @@ 'use strict'; -function main(bucketName, cacheName, zoneName) { +function main(bucketName, zoneName) { // [START storage_control_create_anywhere_cache] /** * TODO(developer): Uncomment these variables before running the sample. @@ -23,9 +23,6 @@ function main(bucketName, cacheName, zoneName) { // The name of your GCS bucket // const bucketName = 'bucketName'; - // The name of the cache to be created - // const cacheName = 'cacheName'; - // The zone that the cache instance will run in. // const zoneName = 'zoneName'; @@ -42,7 +39,6 @@ function main(bucketName, cacheName, zoneName) { const request = { parent: bucketPath, anywhereCache: { - name: cacheName, zone: zoneName, }, }; diff --git a/storage-control/system-test/anywhereCache.test.js b/storage-control/system-test/anywhereCache.test.js index 49070f101ea..fc9c2ab03df 100644 --- a/storage-control/system-test/anywhereCache.test.js +++ b/storage-control/system-test/anywhereCache.test.js @@ -25,7 +25,7 @@ const bucketName = `${bucketPrefix}-a`; const controlClient = new StorageControlClient(); const storage = new Storage({projectId: 'storage-sdk-vendor'}); const bucket = new Bucket(storage, bucketName); -const zoneName = 'us-west1'; +const zoneName = 'us-west1-c'; const cacheName = 'us-west1-c'; let anywhereCachePath; @@ -38,7 +38,7 @@ describe('Anywhere Cache', () => { }, }, hierarchicalNamespace: {enabled: true}, - location: zoneName, + location: 'us-west1', }); anywhereCachePath = controlClient.anywhereCachePath( @@ -85,7 +85,7 @@ describe('Anywhere Cache', () => { // cache to be created, to prevent the test from failing due to a timeout. this.timeout(3600000); const output = execSync( - `node createAnywhereCache.js ${bucketName} ${cacheName} ${zoneName}` + `node createAnywhereCache.js ${bucketName} ${zoneName}` ); assert.match(output, /Created anywhere cache:/); assert.match(output, new RegExp(anywhereCachePath)); From 6afd1d222f0f9ddb302166e1fdbfc26a10483f31 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Thu, 4 Sep 2025 08:17:49 +0000 Subject: [PATCH 06/16] skip test case --- storage-control/system-test/anywhereCache.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/storage-control/system-test/anywhereCache.test.js b/storage-control/system-test/anywhereCache.test.js index fc9c2ab03df..cf964ec7d24 100644 --- a/storage-control/system-test/anywhereCache.test.js +++ b/storage-control/system-test/anywhereCache.test.js @@ -29,7 +29,9 @@ const zoneName = 'us-west1-c'; const cacheName = 'us-west1-c'; let anywhereCachePath; -describe('Anywhere Cache', () => { +// Skipped to prevent CI timeouts caused by long-running operations. +// Un-skip for deliberate, manual runs. +describe.skip('Anywhere Cache', () => { before(async () => { await storage.createBucket(bucketName, { iamConfiguration: { From e3c1483a2e1ea20d63cd1cea6bcb14550408554a Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Fri, 5 Sep 2025 14:29:55 +0000 Subject: [PATCH 07/16] test case remove projectId --- storage-control/system-test/anywhereCache.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage-control/system-test/anywhereCache.test.js b/storage-control/system-test/anywhereCache.test.js index cf964ec7d24..ea38f5db221 100644 --- a/storage-control/system-test/anywhereCache.test.js +++ b/storage-control/system-test/anywhereCache.test.js @@ -23,7 +23,7 @@ const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); const bucketPrefix = `storage-control-samples-${uuid.v4()}`; const bucketName = `${bucketPrefix}-a`; const controlClient = new StorageControlClient(); -const storage = new Storage({projectId: 'storage-sdk-vendor'}); +const storage = new Storage(); const bucket = new Bucket(storage, bucketName); const zoneName = 'us-west1-c'; const cacheName = 'us-west1-c'; From a0abee88867886366fda2a03095c2838ce10a628 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Tue, 9 Sep 2025 08:20:56 +0000 Subject: [PATCH 08/16] addressing review comments --- storage-control/createAnywhereCache.js | 7 +++++++ storage-control/disableAnywhereCache.js | 7 +++++++ storage-control/getAnywhereCache.js | 7 +++++++ storage-control/listAnywhereCaches.js | 7 +++++++ storage-control/pauseAnywhereCache.js | 7 +++++++ storage-control/resumeAnywhereCache.js | 7 +++++++ storage-control/updateAnywhereCache.js | 9 ++++++++- 7 files changed, 50 insertions(+), 1 deletion(-) diff --git a/storage-control/createAnywhereCache.js b/storage-control/createAnywhereCache.js index 54a0c3e633a..aee8ab6235f 100644 --- a/storage-control/createAnywhereCache.js +++ b/storage-control/createAnywhereCache.js @@ -14,6 +14,13 @@ 'use strict'; +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + function main(bucketName, zoneName) { // [START storage_control_create_anywhere_cache] /** diff --git a/storage-control/disableAnywhereCache.js b/storage-control/disableAnywhereCache.js index 3259812f338..b298c887ad6 100644 --- a/storage-control/disableAnywhereCache.js +++ b/storage-control/disableAnywhereCache.js @@ -14,6 +14,13 @@ 'use strict'; +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + function main(bucketName, cacheName) { // [START storage_control_disable_anywhere_cache] /** diff --git a/storage-control/getAnywhereCache.js b/storage-control/getAnywhereCache.js index fb30ae304e1..814227b6652 100644 --- a/storage-control/getAnywhereCache.js +++ b/storage-control/getAnywhereCache.js @@ -14,6 +14,13 @@ 'use strict'; +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + function main(bucketName, cacheName) { // [START storage_control_get_anywhere_cache] /** diff --git a/storage-control/listAnywhereCaches.js b/storage-control/listAnywhereCaches.js index 40c44568858..6dcd3d29ed9 100644 --- a/storage-control/listAnywhereCaches.js +++ b/storage-control/listAnywhereCaches.js @@ -15,6 +15,13 @@ 'use strict'; +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + function main(bucketName) { // [START storage_control_list_anywhere_caches] /** diff --git a/storage-control/pauseAnywhereCache.js b/storage-control/pauseAnywhereCache.js index 8663c9553d6..492f6efab40 100644 --- a/storage-control/pauseAnywhereCache.js +++ b/storage-control/pauseAnywhereCache.js @@ -14,6 +14,13 @@ 'use strict'; +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + function main(bucketName, cacheName) { // [START storage_control_pause_anywhere_cache] /** diff --git a/storage-control/resumeAnywhereCache.js b/storage-control/resumeAnywhereCache.js index 8418e3129fa..0362e40bc80 100644 --- a/storage-control/resumeAnywhereCache.js +++ b/storage-control/resumeAnywhereCache.js @@ -14,6 +14,13 @@ 'use strict'; +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + function main(bucketName, cacheName) { // [START storage_control_resume_anywhere_cache] /** diff --git a/storage-control/updateAnywhereCache.js b/storage-control/updateAnywhereCache.js index 67fcd0c7b66..bb2e29c15ca 100644 --- a/storage-control/updateAnywhereCache.js +++ b/storage-control/updateAnywhereCache.js @@ -14,6 +14,13 @@ 'use strict'; +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + function main(bucketName, cacheName, admissionPolicy) { // [START storage_control_update_anywhere_cache] /** @@ -27,7 +34,7 @@ function main(bucketName, cacheName, admissionPolicy) { // const cacheName = 'cacheName'; // The admission policy of the cache to be updated - // const admissionPolicy = 'admit-on-first-miss'; + // const admissionPolicy = 'admit-on-second-miss'; // Imports the Control library const {StorageControlClient} = require('@google-cloud/storage-control').v2; From 4056e103773ace1e699354d347fbec3889b7d31a Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 8 Oct 2025 06:17:48 +0000 Subject: [PATCH 09/16] index on anywhere-cache-samples: e83ba77b addressing review comments --- storage-control/createAnywhereCache.js | 19 ++++++++++++------- storage-control/disableAnywhereCache.js | 18 +++++++++++------- storage-control/getAnywhereCache.js | 25 ++++++++++++++++++------- storage-control/listAnywhereCaches.js | 10 ++++++---- storage-control/updateAnywhereCache.js | 18 ++++++++---------- 5 files changed, 55 insertions(+), 35 deletions(-) diff --git a/storage-control/createAnywhereCache.js b/storage-control/createAnywhereCache.js index aee8ab6235f..836c5817693 100644 --- a/storage-control/createAnywhereCache.js +++ b/storage-control/createAnywhereCache.js @@ -23,16 +23,19 @@ function main(bucketName, zoneName) { // [START storage_control_create_anywhere_cache] + /** - * TODO(developer): Uncomment these variables before running the sample. + * Creates an Anywhere Cache instance for a Cloud Storage bucket. + * Anywhere Cache is a feature that provides an SSD-backed zonal read cache. + * This can significantly improve read performance for frequently accessed data + * by caching it in the same zone as your compute resources. + * + * @param {string} bucketName The name of the bucket to create the cache for. + * Example: 'your-gcp-bucket-name' + * @param {string} zoneName The zone where the cache will be created. + * Example: 'us-central1-a' */ - // The name of your GCS bucket - // const bucketName = 'bucketName'; - - // The zone that the cache instance will run in. - // const zoneName = 'zoneName'; - // Imports the Control library const {StorageControlClient} = require('@google-cloud/storage-control').v2; @@ -47,6 +50,8 @@ function main(bucketName, zoneName) { parent: bucketPath, anywhereCache: { zone: zoneName, + ttl: '70000s', + admissionPolicy: 'admit-on-first-miss', }, }; diff --git a/storage-control/disableAnywhereCache.js b/storage-control/disableAnywhereCache.js index b298c887ad6..3b1f7aa021a 100644 --- a/storage-control/disableAnywhereCache.js +++ b/storage-control/disableAnywhereCache.js @@ -24,15 +24,19 @@ function main(bucketName, cacheName) { // [START storage_control_disable_anywhere_cache] /** - * TODO(developer): Uncomment these variables before running the sample. + * Disables an Anywhere Cache instance. + * + * Disabling a cache is the first step to permanently removing it. Once disabled, + * the cache stops ingesting new data. After a grace period, the cache and its + * contents are deleted. This is useful for decommissioning caches that are no + * longer needed. + * + * @param {string} bucketName The name of the bucket where the cache resides. + * Example: 'your-gcp-bucket-name' + * @param {string} cacheName The unique identifier of the cache instance to disable. + * Example: 'cacheName' */ - // The name of your GCS bucket - // const bucketName = 'bucketName'; - - // The name of the cache to be disabled - // const cacheName = 'cacheName'; - // Imports the Control library const {StorageControlClient} = require('@google-cloud/storage-control').v2; diff --git a/storage-control/getAnywhereCache.js b/storage-control/getAnywhereCache.js index 814227b6652..61d058c567c 100644 --- a/storage-control/getAnywhereCache.js +++ b/storage-control/getAnywhereCache.js @@ -24,15 +24,17 @@ function main(bucketName, cacheName) { // [START storage_control_get_anywhere_cache] /** - * TODO(developer): Uncomment these variables before running the sample. + * Retrieves details of a specific Anywhere Cache instance. + * + * This function is useful for checking the current state, configuration (like TTL), + * and other metadata of an existing cache. + * + * @param {string} bucketName The name of the bucket where the cache resides. + * Example: 'your-gcp-bucket-name' + * @param {string} cacheName The unique identifier of the cache instance. + * Example: 'my-anywhere-cache-id' */ - // The name of your GCS bucket - // const bucketName = 'bucketName'; - - // The name of the cache to get - // const cacheName = 'cacheName'; - // Imports the Control library const {StorageControlClient} = require('@google-cloud/storage-control').v2; @@ -54,6 +56,15 @@ function main(bucketName, cacheName) { // Run request const [response] = await controlClient.getAnywhereCache(request); console.log(`Got anywhere cache: ${response.name}.`); + console.log(`Anywhere Cache details for '${cacheName}':`); + console.log(` ID: ${response.id}`); + console.log(` Zone: ${response.zone}`); + console.log(` State: ${response.state}`); + console.log(` TTL: ${response.ttl.seconds}s`); + console.log(` Admission Policy: ${response.admissionPolicy}`); + console.log( + ` Create Time: ${new Date(response.createTime.seconds * 1000).toISOString()}` + ); } callGetAnywhereCache(); diff --git a/storage-control/listAnywhereCaches.js b/storage-control/listAnywhereCaches.js index 6dcd3d29ed9..360b2ee5654 100644 --- a/storage-control/listAnywhereCaches.js +++ b/storage-control/listAnywhereCaches.js @@ -25,12 +25,14 @@ function main(bucketName) { // [START storage_control_list_anywhere_caches] /** - * TODO(developer): Uncomment these variables before running the sample. + * Lists all Anywhere Cache instances for a Cloud Storage bucket. + * This function helps you discover all active and pending caches associated with + * a specific bucket, which is useful for auditing and management. + * + * @param {string} bucketName The name of the bucket to list caches for. + * Example: 'your-gcp-bucket-name' */ - // The name of your GCS bucket - // const bucketName = 'bucketName'; - // Imports the Control library const {StorageControlClient} = require('@google-cloud/storage-control').v2; diff --git a/storage-control/updateAnywhereCache.js b/storage-control/updateAnywhereCache.js index bb2e29c15ca..02c55494a56 100644 --- a/storage-control/updateAnywhereCache.js +++ b/storage-control/updateAnywhereCache.js @@ -24,18 +24,16 @@ function main(bucketName, cacheName, admissionPolicy) { // [START storage_control_update_anywhere_cache] /** - * TODO(developer): Uncomment these variables before running the sample. + * Updates the Admission Policy of an Anywhere Cache instance. + * + * @param {string} bucketName The name of the bucket where the cache resides. + * Example: 'your-gcp-bucket-name' + * @param {string} cacheName The unique identifier of the cache instance to update. + * Example: 'my-anywhere-cache-id' + * @param {string} admissionPolicy Determines when data is ingested into the cache + * Example: 'admit-on-second-miss' */ - // The name of your GCS bucket - // const bucketName = 'bucketName'; - - // The name of the cache to be update - // const cacheName = 'cacheName'; - - // The admission policy of the cache to be updated - // const admissionPolicy = 'admit-on-second-miss'; - // Imports the Control library const {StorageControlClient} = require('@google-cloud/storage-control').v2; From 76a7024cde6504512937c550bb6329e2acd649d7 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 8 Oct 2025 09:22:17 +0000 Subject: [PATCH 10/16] feat(storage-control): Improve Anywhere Cache API samples Improve Anywhere Cache API samples (Documentation & Error Handling) * Wraps all asynchronous Anywhere Cache API samples (Create, Get, List, Disable, Pause, Resume) in `try...catch` blocks for production readiness. * Adds specific gRPC error code checks (NOT_FOUND, FAILED_PRECONDITION) to provide better diagnostic feedback to users. * Clarifies documentation regarding the optional nature and default values of `ttl` and `admissionPolicy` for cache creation. --- storage-control/createAnywhereCache.js | 28 +++++++++------ storage-control/disableAnywhereCache.js | 33 ++++++++++++++++++ storage-control/getAnywhereCache.js | 37 +++++++++++++------- storage-control/listAnywhereCaches.js | 25 +++++++++++--- storage-control/pauseAnywhereCache.js | 45 +++++++++++++++++++------ storage-control/resumeAnywhereCache.js | 44 ++++++++++++++++++------ storage-control/updateAnywhereCache.js | 32 +++++++++++++++--- 7 files changed, 194 insertions(+), 50 deletions(-) diff --git a/storage-control/createAnywhereCache.js b/storage-control/createAnywhereCache.js index 836c5817693..1b43d6e4d5b 100644 --- a/storage-control/createAnywhereCache.js +++ b/storage-control/createAnywhereCache.js @@ -50,20 +50,28 @@ function main(bucketName, zoneName) { parent: bucketPath, anywhereCache: { zone: zoneName, - ttl: '70000s', - admissionPolicy: 'admit-on-first-miss', + ttl: { + seconds: '10000s', + }, // Optional. Default: '86400s'(1 day) + admissionPolicy: 'admit-on-first-miss', // Optional. Default: 'admit-on-first-miss' }, }; - // Run the request, which returns an Operation object - const [operation] = await controlClient.createAnywhereCache(request); - console.log(`Waiting for operation ${operation.name} to complete...`); + try { + // Run the request, which returns an Operation object + const [operation] = await controlClient.createAnywhereCache(request); + console.log(`Waiting for operation ${operation.name} to complete...`); - // Wait for the operation to complete and get the final resource - const anywhereCache = await checkCreateAnywhereCacheProgress( - operation.name - ); - console.log(`Created anywhere cache: ${anywhereCache.result.name}.`); + // Wait for the operation to complete and get the final resource + const anywhereCache = await checkCreateAnywhereCacheProgress( + operation.name + ); + console.log(`Created anywhere cache: ${anywhereCache.result.name}.`); + } catch (error) { + // Handle any error that occurred during the creation or polling process. + console.error('Failed to create Anywhere Cache:', error.message); + throw error; + } } // A custom function to check the operation's progress. diff --git a/storage-control/disableAnywhereCache.js b/storage-control/disableAnywhereCache.js index 3b1f7aa021a..e9ce829a557 100644 --- a/storage-control/disableAnywhereCache.js +++ b/storage-control/disableAnywhereCache.js @@ -44,6 +44,10 @@ function main(bucketName, cacheName) { const controlClient = new StorageControlClient(); async function callDisableAnywhereCache() { + // You have a one-hour grace period after disabling a cache to resume it and prevent its deletion. + // If you don't resume the cache within that hour, it will be deleted, its data will be evicted, + // and the cache will be permanently removed from the bucket. + const anywhereCachePath = controlClient.anywhereCachePath( '_', bucketName, @@ -55,6 +59,35 @@ function main(bucketName, cacheName) { name: anywhereCachePath, }; + try { + // Run request. This initiates the disablement process. + const [response] = await controlClient.disableAnywhereCache(request); + + console.log( + `Successfully initiated disablement for Anywhere Cache '${cacheName}'.` + ); + console.log(` Current State: ${response.state}`); + console.log(` Resource Name: ${response.name}`); + } catch (error) { + // Catch and handle potential API errors. + console.error( + `Error disabling Anywhere Cache '${cacheName}': ${error.message}` + ); + + if (error.code === 5) { + // NOT_FOUND (gRPC code 5) error can occur if the bucket or cache does not exist. + console.error( + `Please ensure the cache '${cacheName}' exists in bucket '${bucketName}'.` + ); + } else if (error.code === 9) { + // FAILED_PRECONDITION (gRPC code 9) can occur if the cache is already being disabled + // or is not in a RUNNING state that allows the disable operation. + console.error( + `Cache '${cacheName}' may not be in a state that allows disabling (e.g., must be RUNNING).` + ); + } + throw error; + } // Run request const [response] = await controlClient.disableAnywhereCache(request); console.log(`Disabled anywhere cache: ${response.name}.`); diff --git a/storage-control/getAnywhereCache.js b/storage-control/getAnywhereCache.js index 61d058c567c..2633ede7e44 100644 --- a/storage-control/getAnywhereCache.js +++ b/storage-control/getAnywhereCache.js @@ -53,18 +53,31 @@ function main(bucketName, cacheName) { name: anywhereCachePath, }; - // Run request - const [response] = await controlClient.getAnywhereCache(request); - console.log(`Got anywhere cache: ${response.name}.`); - console.log(`Anywhere Cache details for '${cacheName}':`); - console.log(` ID: ${response.id}`); - console.log(` Zone: ${response.zone}`); - console.log(` State: ${response.state}`); - console.log(` TTL: ${response.ttl.seconds}s`); - console.log(` Admission Policy: ${response.admissionPolicy}`); - console.log( - ` Create Time: ${new Date(response.createTime.seconds * 1000).toISOString()}` - ); + try { + // Run request + const [response] = await controlClient.getAnywhereCache(request); + console.log(`Anywhere Cache details for '${cacheName}':`); + console.log(` Name: ${response.name}`); + console.log(` Zone: ${response.zone}`); + console.log(` State: ${response.state}`); + console.log(` TTL: ${response.ttl.seconds}s`); + console.log(` Admission Policy: ${response.admissionPolicy}`); + console.log( + ` Create Time: ${new Date(response.createTime.seconds * 1000).toISOString()}` + ); + } catch (error) { + // Handle errors (e.g., cache not found, permission denied). + console.error( + `Error retrieving Anywhere Cache '${cacheName}': ${error.message}` + ); + + if (error.code === 5) { + console.error( + `Ensure the cache '${cacheName}' exists in bucket '${bucketName}'.` + ); + } + throw error; + } } callGetAnywhereCache(); diff --git a/storage-control/listAnywhereCaches.js b/storage-control/listAnywhereCaches.js index 360b2ee5654..9bf51759812 100644 --- a/storage-control/listAnywhereCaches.js +++ b/storage-control/listAnywhereCaches.js @@ -47,10 +47,27 @@ function main(bucketName) { parent: bucketPath, }; - // Run request - const [response] = await controlClient.listAnywhereCaches(request); - for (const anywhereCache of response) { - console.log(anywhereCache.name); + try { + // Run request. The response is an array where the first element is the list of caches. + const [response] = await controlClient.listAnywhereCaches(request); + + if (response && response.length > 0) { + console.log( + `Found ${response.length} Anywhere Caches for bucket: ${bucketName}` + ); + for (const anywhereCache of response) { + console.log(anywhereCache.name); + } + } else { + // Case: Successful but empty list (No Anywhere Caches found) + console.log(`No Anywhere Caches found for bucket: ${bucketName}.`); + } + } catch (error) { + console.error( + `Error listing Anywhere Caches for bucket ${bucketName}:`, + error.message + ); + throw error; } } diff --git a/storage-control/pauseAnywhereCache.js b/storage-control/pauseAnywhereCache.js index 492f6efab40..dcb974b0fe5 100644 --- a/storage-control/pauseAnywhereCache.js +++ b/storage-control/pauseAnywhereCache.js @@ -24,15 +24,18 @@ function main(bucketName, cacheName) { // [START storage_control_pause_anywhere_cache] /** - * TODO(developer): Uncomment these variables before running the sample. + * Pauses an Anywhere Cache instance. + * + * This synchronous function stops the ingestion of new data for a cache that's in a RUNNING state. + * While PAUSED, you can still read existing data (which resets the TTL), but no new data is ingested. + * The cache can be returned to the RUNNING state by calling the resume function. + * + * @param {string} bucketName The name of the bucket where the cache resides. + * Example: 'your-gcp-bucket-name' + * @param {string} cacheName The unique identifier of the cache instance. + * Example: 'my-anywhere-cache-id' */ - // The name of your GCS bucket - // const bucketName = 'bucketName'; - - // The name of the cache to be paused - // const cacheName = 'cacheName'; - // Imports the Control library const {StorageControlClient} = require('@google-cloud/storage-control').v2; @@ -51,9 +54,31 @@ function main(bucketName, cacheName) { name: anywhereCachePath, }; - // Run request - const [response] = await controlClient.pauseAnywhereCache(request); - console.log(`Paused anywhere cache: ${response.name}.`); + try { + // Run request + const [response] = await controlClient.pauseAnywhereCache(request); + + console.log(`Successfully paused anywhere cache: ${response.name}.`); + console.log(` Current State: ${response.state}`); + } catch (error) { + // Catch and handle potential API errors. + console.error( + `Error pausing Anywhere Cache '${cacheName}': ${error.message}` + ); + + if (error.code === 5) { + // NOT_FOUND (gRPC code 5) + console.error( + `Please ensure the cache '${cacheName}' exists in bucket '${bucketName}'.` + ); + } else if (error.code === 9) { + // FAILED_PRECONDITION (gRPC code 9) + console.error( + `Cache '${cacheName}' may not be in a state that allows pausing (e.g., must be RUNNING).` + ); + } + throw error; + } } callPauseAnywhereCache(); diff --git a/storage-control/resumeAnywhereCache.js b/storage-control/resumeAnywhereCache.js index 0362e40bc80..1cea5035d33 100644 --- a/storage-control/resumeAnywhereCache.js +++ b/storage-control/resumeAnywhereCache.js @@ -24,15 +24,17 @@ function main(bucketName, cacheName) { // [START storage_control_resume_anywhere_cache] /** - * TODO(developer): Uncomment these variables before running the sample. + * Resumes a disabled Anywhere Cache instance. + * + * This action reverts a cache from a PAUSED state or a DISABLED state back to RUNNING, + * provided it is done within the 1-hour grace period before the cache is permanently deleted. + * + * @param {string} bucketName The name of the bucket where the cache resides. + * Example: 'your-gcp-bucket-name' + * @param {string} cacheName The unique identifier of the cache instance. + * Example: 'my-anywhere-cache-id' */ - // The name of your GCS bucket - // const bucketName = 'bucketName'; - - // The name of the cache to be resumed - // const cacheName = 'cacheName'; - // Imports the Control library const {StorageControlClient} = require('@google-cloud/storage-control').v2; @@ -51,9 +53,31 @@ function main(bucketName, cacheName) { name: anywhereCachePath, }; - // Run request - const [response] = await controlClient.resumeAnywhereCache(request); - console.log(`Resumed anywhere cache: ${response.name}.`); + try { + // Run request + const [response] = await controlClient.resumeAnywhereCache(request); + + console.log(`Successfully resumed anywhere cache: ${response.name}.`); + console.log(` Current State: ${response.state}`); + } catch (error) { + // Catch and handle potential API errors. + console.error( + `Error resuming Anywhere Cache '${cacheName}': ${error.message}` + ); + + if (error.code === 5) { + // NOT_FOUND (gRPC code 5) + console.error( + `Please ensure the cache '${cacheName}' exists in bucket '${bucketName}'.` + ); + } else if (error.code === 9) { + // FAILED_PRECONDITION (gRPC code 9) + console.error( + `Cache '${cacheName}' may not be in a state that allows resuming (e.g., already RUNNING or past the 1-hour deletion grace period).` + ); + } + throw error; + } } callResumeAnywhereCache(); diff --git a/storage-control/updateAnywhereCache.js b/storage-control/updateAnywhereCache.js index 02c55494a56..1d006e41198 100644 --- a/storage-control/updateAnywhereCache.js +++ b/storage-control/updateAnywhereCache.js @@ -58,11 +58,35 @@ function main(bucketName, cacheName, admissionPolicy) { }, }; - // Run request - const [operation] = await controlClient.updateAnywhereCache(request); - const [response] = await operation.promise(); + try { + // Run request + const [operation] = await controlClient.updateAnywhereCache(request); + console.log( + `Waiting for update operation ${operation.name} to complete...` + ); - console.log(`Updated anywhere cache: ${response.name}.`); + const [response] = await operation.promise(); + + console.log(`Updated anywhere cache: ${response.name}.`); + } catch (error) { + // Handle errors during the initial request or during the LRO polling. + console.error( + `Error updating Anywhere Cache '${cacheName}': ${error.message}` + ); + + if (error.code === 5) { + // NOT_FOUND (gRPC code 5) + console.error( + `Ensure the cache '${cacheName}' exists in bucket '${bucketName}'.` + ); + } else if (error.code === 3) { + // INVALID_ARGUMENT (gRPC code 3) + console.error( + `Ensure '${admissionPolicy}' is a valid Admission Policy.` + ); + } + throw error; + } } callUpdateAnywhereCache(); From 2c026b10c749fd80a468259a3418b9c545e5a6a0 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 8 Oct 2025 12:52:38 +0000 Subject: [PATCH 11/16] test(storagecontrol): Enhance Anywhere Cache test assertions and coverage Updates tests for all Anywhere Cache management methods (Create, Get, List, Resume, Disable, Pause) to align with enhanced sample script output. **(Tests were skipped due to reliance on Long-Running Operations (LROs) in the sample code.)** This includes: * **Enhanced Assertions:** Asserting against all newly added detail fields (e.g., Name, State, TTL) to verify full API response parsing in Get and List samples. * **Negative Scenario Coverage:** Adding explicit **negative scenario tests** for state-dependent operations (Disable, Pause, Resume) to assert graceful failure on expected errors like `FAILED_PRECONDITION`. --- storage-control/disableAnywhereCache.js | 2 +- .../system-test/anywhereCache.test.js | 39 +++++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/storage-control/disableAnywhereCache.js b/storage-control/disableAnywhereCache.js index e9ce829a557..f4487d725f0 100644 --- a/storage-control/disableAnywhereCache.js +++ b/storage-control/disableAnywhereCache.js @@ -64,7 +64,7 @@ function main(bucketName, cacheName) { const [response] = await controlClient.disableAnywhereCache(request); console.log( - `Successfully initiated disablement for Anywhere Cache '${cacheName}'.` + `Successfully initiated disablement for Anywhere Cache: '${cacheName}'.` ); console.log(` Current State: ${response.state}`); console.log(` Resource Name: ${response.name}`); diff --git a/storage-control/system-test/anywhereCache.test.js b/storage-control/system-test/anywhereCache.test.js index ea38f5db221..59a6bbadb6b 100644 --- a/storage-control/system-test/anywhereCache.test.js +++ b/storage-control/system-test/anywhereCache.test.js @@ -97,8 +97,15 @@ describe.skip('Anywhere Cache', () => { const output = execSync( `node getAnywhereCache.js ${bucketName} ${cacheName}` ); - assert.match(output, /Got anywhere cache:/); + const detailsHeader = `Anywhere Cache details for '${cacheName}':`; + assert.match(output, new RegExp(detailsHeader)); + assert.match(output, /Name:/); assert.match(output, new RegExp(anywhereCachePath)); + assert.match(output, /Zone:/); + assert.match(output, /State:/); + assert.match(output, /TTL:/); + assert.match(output, /Admission Policy:/); + assert.match(output, /Create Time:/); }); it('should list anywhere caches', async () => { @@ -119,23 +126,39 @@ describe.skip('Anywhere Cache', () => { const output = execSync( `node pauseAnywhereCache.js ${bucketName} ${cacheName}` ); - assert.match(output, /Paused anywhere cache:/); + assert.match(output, /Successfully paused anywhere cache:/); assert.match(output, new RegExp(anywhereCachePath)); + assert.match(output, /Current State:/); }); it('should resume an anywhere cache', async () => { const output = execSync( `node resumeAnywhereCache.js ${bucketName} ${cacheName}` ); - assert.match(output, /Resumed anywhere cache:/); + assert.match(output, /Successfully resumed anywhere cache:/); assert.match(output, new RegExp(anywhereCachePath)); + assert.match(output, /Current State:/); }); it('should disable an anywhere cache', async () => { - const output = execSync( - `node disableAnywhereCache.js ${bucketName} ${cacheName}` - ); - assert.match(output, /Disabled anywhere cache:/); - assert.match(output, new RegExp(anywhereCachePath)); + try { + const output = execSync( + `node disableAnywhereCache.js ${bucketName} ${cacheName}` + ); + assert.match( + output, + /Successfully initiated disablement for Anywhere Cache:/ + ); + assert.match(output, new RegExp(anywhereCachePath)); + assert.match(output, /Current State:/); + assert.match(output, /Resource Name:/); + } catch (error) { + const errorMessage = error.stderr.toString(); + + assert.match( + errorMessage, + /9 FAILED_PRECONDITION: The requested DISABLE operation can't be applied on cache in DISABLED state./ + ); + } }); }); From 41e2ade433a97ab413a5ac12b2b0c551092dde1b Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 22 Oct 2025 13:07:57 +0000 Subject: [PATCH 12/16] Fix: Remove duplicate call to disableAnywhereCache The function was being called twice due to an oversight during a recent try/catch restructure. --- storage-control/disableAnywhereCache.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/storage-control/disableAnywhereCache.js b/storage-control/disableAnywhereCache.js index f4487d725f0..882ff251513 100644 --- a/storage-control/disableAnywhereCache.js +++ b/storage-control/disableAnywhereCache.js @@ -88,9 +88,6 @@ function main(bucketName, cacheName) { } throw error; } - // Run request - const [response] = await controlClient.disableAnywhereCache(request); - console.log(`Disabled anywhere cache: ${response.name}.`); } callDisableAnywhereCache(); From fd1aed38d75129961405c67d98e9ca2736c2f33e Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 20 May 2026 16:05:12 +0000 Subject: [PATCH 13/16] feat(storage): add samples and system tests for bucket encryption enforcement (#4272) * feat(storage): add samples and system tests for bucket encryption enforcement Adds comprehensive code samples and system tests to verify Google-managed, Customer-managed, and Customer-supplied encryption enforcement logic. - Add setBucketEncryptionEnforcementConfig.js sample - Add getBucketEncryptionEnforcementConfig.js sample - Add updateBucketEncryptionEnforcementConfig.js sample - Add system tests to verify CLI output and backend metadata state - Ensure server-side effectiveTime is correctly captured and displayed * code refactor * test(storage): refactor encryption tests to assert state over strings * test: skip bucket encryption enforcement tests if defaultKmsKeyName is missing --------- Co-authored-by: Jennifer Davis --- .../getBucketEncryptionEnforcementConfig.js | 76 ++++++++++ .../setBucketEncryptionEnforcementConfig.js | 93 ++++++++++++ storage/system-test/buckets.test.js | 143 ++++++++++++++++++ ...updateBucketEncryptionEnforcementConfig.js | 74 +++++++++ 4 files changed, 386 insertions(+) create mode 100644 storage/getBucketEncryptionEnforcementConfig.js create mode 100644 storage/setBucketEncryptionEnforcementConfig.js create mode 100644 storage/system-test/buckets.test.js create mode 100644 storage/updateBucketEncryptionEnforcementConfig.js diff --git a/storage/getBucketEncryptionEnforcementConfig.js b/storage/getBucketEncryptionEnforcementConfig.js new file mode 100644 index 00000000000..aa791ab1bbb --- /dev/null +++ b/storage/getBucketEncryptionEnforcementConfig.js @@ -0,0 +1,76 @@ +// Copyright 2026 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'; + +// sample-metadata: +// title: Get Bucket Encryption Enforcement +// description: Retrieves the current encryption enforcement configurations for a bucket. +// usage: node getBucketEncryptionEnforcementConfig.js + +function main(bucketName = 'my-bucket') { + // [START storage_get_encryption_enforcement_config] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function getBucketEncryptionEnforcementConfig() { + const [metadata] = await storage.bucket(bucketName).getMetadata(); + + console.log( + `Encryption enforcement configuration for bucket ${bucketName}.` + ); + const enc = metadata.encryption; + if (!enc) { + console.log( + 'No encryption configuration found (Default GMEK is active).' + ); + return; + } + console.log(`Default KMS Key: ${enc.defaultKmsKeyName || 'None'}`); + + const printConfig = (label, config) => { + if (config) { + console.log(`${label}:`); + console.log(` Mode: ${config.restrictionMode}`); + console.log(` Effective: ${config.effectiveTime}`); + } + }; + + printConfig( + 'Google Managed (GMEK) Enforcement', + enc.googleManagedEncryptionEnforcementConfig + ); + printConfig( + 'Customer Managed (CMEK) Enforcement', + enc.customerManagedEncryptionEnforcementConfig + ); + printConfig( + 'Customer Supplied (CSEK) Enforcement', + enc.customerSuppliedEncryptionEnforcementConfig + ); + } + + getBucketEncryptionEnforcementConfig().catch(console.error); + // [END storage_get_encryption_enforcement_config] +} +main(...process.argv.slice(2)); diff --git a/storage/setBucketEncryptionEnforcementConfig.js b/storage/setBucketEncryptionEnforcementConfig.js new file mode 100644 index 00000000000..625ada06299 --- /dev/null +++ b/storage/setBucketEncryptionEnforcementConfig.js @@ -0,0 +1,93 @@ +// Copyright 2026 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'; + +// sample-metadata: +// title: Set Bucket Encryption Enforcement +// description: Configures a bucket to enforce specific encryption types (e.g., CMEK-only). +// usage: node setBucketEncryptionEnforcementConfig.js + +function main( + bucketName = 'my-bucket', + defaultKmsKeyName = process.env.GOOGLE_CLOUD_KMS_KEY_ASIA +) { + // [START storage_set_encryption_enforcement_config] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // The name of the KMS key to be used as the default + // const defaultKmsKeyName = 'my-key'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function setBucketEncryptionEnforcementConfig() { + const options = { + encryption: { + defaultKmsKeyName, + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + customerSuppliedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + customerManagedEncryptionEnforcementConfig: { + restrictionMode: 'NotRestricted', + }, + }, + }; + + const [metadata] = await storage.bucket(bucketName).setMetadata(options); + + console.log( + `Encryption enforcement configuration updated for bucket ${bucketName}.` + ); + const enc = metadata.encryption; + if (enc) { + console.log(`Default KMS Key: ${enc.defaultKmsKeyName}`); + + const logEnforcement = (label, config) => { + if (config) { + console.log(`${label}:`); + console.log(` Mode: ${config.restrictionMode}`); + console.log(` Effective: ${config.effectiveTime}`); + } + }; + + logEnforcement( + 'Google Managed (GMEK) Enforcement', + enc.googleManagedEncryptionEnforcementConfig + ); + logEnforcement( + 'Customer Managed (CMEK) Enforcement', + enc.customerManagedEncryptionEnforcementConfig + ); + logEnforcement( + 'Customer Supplied (CSEK) Enforcement', + enc.customerSuppliedEncryptionEnforcementConfig + ); + } + } + + setBucketEncryptionEnforcementConfig().catch(console.error); + // [END storage_set_encryption_enforcement_config] +} +main(...process.argv.slice(2)); diff --git a/storage/system-test/buckets.test.js b/storage/system-test/buckets.test.js new file mode 100644 index 00000000000..d5055007d88 --- /dev/null +++ b/storage/system-test/buckets.test.js @@ -0,0 +1,143 @@ +// Copyright 2019 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'; + +const {Storage} = require('@google-cloud/storage'); +const {assert} = require('chai'); +const {before, after, afterEach, it} = require('mocha'); +const cp = require('child_process'); +const uuid = require('uuid'); + +const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); + +const storage = new Storage(); +const samplesTestBucketPrefix = `nodejs-storage-samples-${uuid.v4()}`; +const bucketName = `${samplesTestBucketPrefix}-a`; +const defaultKmsKeyName = process.env.GOOGLE_CLOUD_KMS_KEY_ASIA; +const bucket = storage.bucket(bucketName); + +before(async () => { + await storage.createBucket(bucketName); +}); + +async function deleteAllBucketsAsync() { + const [buckets] = await storage.getBuckets({prefix: samplesTestBucketPrefix}); + + for (const bucket of buckets) { + await bucket.deleteFiles({force: true}); + await bucket.delete({ignoreNotFound: true}); + } +} + +after(deleteAllBucketsAsync); +afterEach(async () => { + await new Promise(res => setTimeout(res, 1000)); +}); + +it('should set bucket encryption enforcement configuration', async function () { + if (!defaultKmsKeyName) { + this.skip(); + } + const output = execSync( + `node setBucketEncryptionEnforcementConfig.js ${bucketName} ${defaultKmsKeyName}` + ); + + assert.include( + output, + `Encryption enforcement configuration updated for bucket ${bucketName}.` + ); + + assert.include(output, `Default KMS Key: ${defaultKmsKeyName}`); + + assert.include(output, 'Google Managed (GMEK) Enforcement:'); + assert.include(output, 'Mode: FullyRestricted'); + + assert.include(output, 'Customer Managed (CMEK) Enforcement:'); + assert.include(output, 'Mode: NotRestricted'); + + assert.include(output, 'Customer Supplied (CSEK) Enforcement:'); + assert.include(output, 'Mode: FullyRestricted'); + + assert.match(output, new RegExp('Effective:')); + + const [metadata] = await bucket.getMetadata(); + const encryption = metadata.encryption || {}; + assert.strictEqual( + encryption.googleManagedEncryptionEnforcementConfig?.restrictionMode, + 'FullyRestricted' + ); + assert.strictEqual( + encryption.customerManagedEncryptionEnforcementConfig?.restrictionMode, + 'NotRestricted' + ); + assert.strictEqual( + encryption.customerSuppliedEncryptionEnforcementConfig?.restrictionMode, + 'FullyRestricted' + ); +}); + +it('should get bucket encryption enforcement configuration', async function () { + if (!defaultKmsKeyName) { + this.skip(); + } + const output = execSync( + `node getBucketEncryptionEnforcementConfig.js ${bucketName}` + ); + + assert.include( + output, + `Encryption enforcement configuration for bucket ${bucketName}.` + ); + assert.include(output, `Default KMS Key: ${defaultKmsKeyName}`); + + assert.include(output, 'Google Managed (GMEK) Enforcement:'); + assert.include(output, 'Mode: FullyRestricted'); + assert.match(output, /Effective:/); + + const [metadata] = await bucket.getMetadata(); + const encryption = metadata.encryption || {}; + + assert.strictEqual(encryption.defaultKmsKeyName, defaultKmsKeyName); + assert.strictEqual( + encryption.googleManagedEncryptionEnforcementConfig?.restrictionMode, + 'FullyRestricted' + ); + assert.strictEqual( + encryption.customerManagedEncryptionEnforcementConfig?.restrictionMode, + 'NotRestricted' + ); + assert.strictEqual( + encryption.customerSuppliedEncryptionEnforcementConfig?.restrictionMode, + 'FullyRestricted' + ); +}); + +it('should update and then remove bucket encryption enforcement configuration', async () => { + const output = execSync( + `node updateBucketEncryptionEnforcementConfig.js ${bucketName}` + ); + + assert.include( + output, + `Google-managed encryption enforcement set to FullyRestricted for ${bucketName}.` + ); + assert.include( + output, + `All encryption enforcement configurations removed from bucket ${bucketName}.` + ); + + const [metadata] = await bucket.getMetadata(); + assert.ok(!metadata.encryption); +}); diff --git a/storage/updateBucketEncryptionEnforcementConfig.js b/storage/updateBucketEncryptionEnforcementConfig.js new file mode 100644 index 00000000000..1bf5feb839b --- /dev/null +++ b/storage/updateBucketEncryptionEnforcementConfig.js @@ -0,0 +1,74 @@ +// Copyright 2026 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'; + +// sample-metadata: +// title: Update Bucket Encryption Enforcement Config +// description: Updates and then removes encryption enforcement configurations from a bucket. +// usage: node updateBucketEncryptionEnforcementConfig.js + +function main(bucketName = 'my-bucket') { + // [START storage_update_bucket_encryption_enforcement_config] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function updateBucketEncryptionEnforcementConfig() { + const bucket = storage.bucket(bucketName); + + // Update a specific encryption type's restriction mode + // This partial update preserves other existing encryption settings. + const updateOptions = { + encryption: { + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + }, + }; + + await bucket.setMetadata(updateOptions); + console.log( + `Google-managed encryption enforcement set to FullyRestricted for ${bucketName}.` + ); + + // Remove all encryption enforcement configurations altogether + // Setting these values to null removes the policies from the bucket metadata. + const clearOptions = { + encryption: { + defaultKmsKeyName: null, + googleManagedEncryptionEnforcementConfig: null, + customerSuppliedEncryptionEnforcementConfig: null, + customerManagedEncryptionEnforcementConfig: null, + }, + }; + + await bucket.setMetadata(clearOptions); + console.log( + `All encryption enforcement configurations removed from bucket ${bucketName}.` + ); + } + + updateBucketEncryptionEnforcementConfig().catch(console.error); + // [END storage_update_bucket_encryption_enforcement_config] +} +main(...process.argv.slice(2)); From 6d11272796602977daba652fc346ff03917da131 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Thu, 21 May 2026 08:34:08 +0000 Subject: [PATCH 14/16] feat(storage): add Object Contexts samples and system tests (#4276) * Feat(storage): add Object Contexts samples and system tests - Add `setObjectContexts.js` to demonstrate CRUD and deletion of contexts. - Add `getObjectContexts.js` to show retrieval of structured metadata. - Add `listObjectsWithContextFilter.js` to demonstrate server-side filtering. - Implement comprehensive system tests in `files.test.js` covering presence, absence (-), and existence (:) filter operators. - Ensure samples use correct 'contexts' field with 'custom' map structure. * code refactor * fix: typo corrections * test(storage): refactor to state-based assertions --------- Co-authored-by: Jennifer Davis --- storage/getObjectContexts.js | 71 ++++++++++++++ storage/listObjectContexts.js | 104 ++++++++++++++++++++ storage/setObjectContexts.js | 66 +++++++++++++ storage/system-test/files.test.js | 153 ++++++++++++++++++++++++++++++ 4 files changed, 394 insertions(+) create mode 100644 storage/getObjectContexts.js create mode 100644 storage/listObjectContexts.js create mode 100644 storage/setObjectContexts.js create mode 100644 storage/system-test/files.test.js diff --git a/storage/getObjectContexts.js b/storage/getObjectContexts.js new file mode 100644 index 00000000000..613a22c2abc --- /dev/null +++ b/storage/getObjectContexts.js @@ -0,0 +1,71 @@ +// Copyright 2026 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'; + +// sample-metadata: +// title: Get Object Contexts +// description: Retrieves the structured Object Contexts from an object. +// usage: node getObjectContexts.js + +/** + * This application demonstrates how to retrieve the 'contexts' field from a file + * in Google Cloud Storage. + */ + +function main(bucketName = 'my-bucket', fileName = 'test.txt') { + // [START storage_get_object_contexts] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // The ID of your GCS file + // const fileName = 'your-file-name'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function getObjectContexts() { + // Gets the metadata for the file + const [metadata] = await storage + .bucket(bucketName) + .file(fileName) + .getMetadata(); + + // Contexts are stored in metadata.contexts.custom + if (metadata.contexts && metadata.contexts.custom) { + console.log(`Object Contexts for ${fileName}:`); + + // Iterate through the custom contexts to show values and timestamps + for (const [key, details] of Object.entries(metadata.contexts.custom)) { + console.log(`- Key: ${key}`); + console.log(` Value: ${details.value}`); + console.log(` Created: ${details.createTime}`); + console.log(` Updated: ${details.updateTime}`); + } + } else { + console.log(`No Object Contexts found for ${fileName}.`); + } + } + + getObjectContexts().catch(console.error); + // [END storage_get_object_contexts] +} + +main(...process.argv.slice(2)); diff --git a/storage/listObjectContexts.js b/storage/listObjectContexts.js new file mode 100644 index 00000000000..3e0c0b00bf8 --- /dev/null +++ b/storage/listObjectContexts.js @@ -0,0 +1,104 @@ +// Copyright 2026 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'; + +// sample-metadata: +// title: List Objects with Context Filter +// description: Lists objects in a bucket that match specific custom contexts. +// usage: node listObjectContexts.js + +/** + * This application demonstrates how to list objects in a bucket while filtering + * by their custom 'contexts' metadata. + */ + +function main(bucketName = 'my-bucket') { + // [START storage_list_object_contexts] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function listObjectContexts() { + // Define the filter for contexts. + const bucket = storage.bucket(bucketName); + + /** + * List any object that has a context with the specified key and value. + * Syntax: contexts.""="" + */ + const filterByValue = 'contexts."priority"="high"'; + const [filesByValue] = await bucket.getFiles({ + filter: filterByValue, + }); + + console.log(`\nFiles matching filter [${filterByValue}]:`); + filesByValue.forEach(file => console.log(` - ${file.name}`)); + + /** + * List any object that has a context with the specified key attached. + * Syntax: contexts."":* + */ + const filterByExistence = 'contexts."team-owner":*'; + const [filesWithKey] = await bucket.getFiles({ + filter: filterByExistence, + }); + + console.log( + `\nFiles with the "team-owner" context key [${filterByExistence}]:` + ); + filesWithKey.forEach(file => console.log(` - ${file.name}`)); + + /** + * List any object that does not have a context with the specified key and value attached. + * Syntax: -contexts.""="" + */ + const absenceOfValuePair = '-contexts."priority"="high"'; + const [filesNoHighPriority] = await bucket.getFiles({ + filter: absenceOfValuePair, + }); + + console.log( + `\nFiles matching absence of value pair [${absenceOfValuePair}]:` + ); + filesNoHighPriority.forEach(file => console.log(` - ${file.name}`)); + + /** + * List any object that does not have a context with the specified key attached. + * Syntax: -contexts."":* + */ + const absenceOfKey = '-contexts."team-owner":*'; + const [filesNoTeamOwner] = await bucket.getFiles({ + filter: absenceOfKey, + }); + + console.log( + `\nFiles matching absence of key regardless of value [${absenceOfKey}]:` + ); + filesNoTeamOwner.forEach(file => console.log(` - ${file.name}`)); + } + + listObjectContexts().catch(console.error); + // [END storage_list_object_contexts] +} + +main(...process.argv.slice(2)); diff --git a/storage/setObjectContexts.js b/storage/setObjectContexts.js new file mode 100644 index 00000000000..19e308fd43f --- /dev/null +++ b/storage/setObjectContexts.js @@ -0,0 +1,66 @@ +// Copyright 2026 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'; + +// sample-metadata: +// title: Set Object Contexts +// description: Sets custom metadata (contexts) on an object. +// usage: node setObjectContexts.js + +/** + * This application demonstrates how to set, update, and delete object contexts (metadata) on a file + * in Google Cloud Storage. + */ + +function main(bucketName = 'my-bucket', fileName = 'test.txt') { + // [START storage_set_object_contexts] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // The ID of your GCS file + // const fileName = 'your-file-name'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function setObjectContexts() { + const file = storage.bucket(bucketName).file(fileName); + + // Create/Update Object Contexts + // Object Contexts live in the 'contexts' field, not the 'metadata' field. + const [metadata] = await file.setMetadata({ + contexts: { + custom: { + 'team-owner': {value: 'storage-team'}, + priority: {value: 'high'}, + }, + }, + }); + + console.log(`Updated Object Contexts for ${fileName}:`); + console.log(JSON.stringify(metadata.contexts, null, 2)); + } + + setObjectContexts().catch(console.error); + // [END storage_set_object_contexts] +} + +main(...process.argv.slice(2)); diff --git a/storage/system-test/files.test.js b/storage/system-test/files.test.js new file mode 100644 index 00000000000..a0147fa8a0b --- /dev/null +++ b/storage/system-test/files.test.js @@ -0,0 +1,153 @@ +// Copyright 2019 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'; + +const fs = require('fs'); +const path = require('path'); +const {Storage} = require('@google-cloud/storage'); +const {assert} = require('chai'); +const {before, after, it, describe} = require('mocha'); +const cp = require('child_process'); +const uuid = require('uuid'); +const {promisify} = require('util'); + +const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); + +const storage = new Storage(); +const cwd = path.join(__dirname, '..'); +const bucketName = generateName(); +const bucket = storage.bucket(bucketName); +const fileName = 'test.txt'; +const filePath = path.join(cwd, 'resources', fileName); +const downloadFilePath = path.join(cwd, 'downloaded.txt'); + +describe('file', () => { + before(async () => { + await bucket.create(); + }); + + after(async () => { + await promisify(fs.unlink)(downloadFilePath).catch(console.error); + // Try deleting all files twice, just to make sure + await bucket.deleteFiles({force: true}).catch(console.error); + await bucket.deleteFiles({force: true}).catch(console.error); + await bucket.delete().catch(console.error); + }); + + describe('Object Contexts', () => { + const contextFile = bucket.file(fileName); + + beforeEach(async () => { + await bucket.upload(filePath, {destination: fileName}); + + await contextFile.setMetadata({ + contexts: { + custom: { + 'team-owner': {value: 'storage-team'}, + priority: {value: 'high'}, + }, + }, + }); + }); + + it('should set object contexts', async () => { + const output = execSync( + `node setObjectContexts.js ${bucketName} ${fileName}` + ); + // Verify Initial Creation + assert.include(output, `Updated Object Contexts for ${fileName}:`); + assert.include(output, '"team-owner":'); + assert.include(output, '"value": "storage-team"'); + assert.include(output, '"priority":'); + assert.include(output, '"value": "high"'); + assert.include(output, '"createTime":'); + assert.include(output, '"updateTime":'); + + const [metadata] = await contextFile.getMetadata(); + const customContexts = metadata.contexts?.custom || {}; + + assert.strictEqual(customContexts['priority']?.value, 'high'); + assert.strictEqual(customContexts['team-owner']?.value, 'storage-team'); + }); + + it('should get object contexts', async () => { + const output = execSync( + `node getObjectContexts.js ${bucketName} ${fileName}` + ); + assert.include(output, `Object Contexts for ${fileName}:`); + assert.include(output, 'Key: priority'); + assert.include(output, 'Value: high'); + assert.include(output, 'Key: team-owner'); + assert.include(output, 'Value: storage-team'); + + const [metadata] = await contextFile.getMetadata(); + const customContexts = metadata.contexts?.custom || {}; + + assert.strictEqual(customContexts['priority'].value, 'high'); + assert.strictEqual(customContexts['team-owner'].value, 'storage-team'); + }); + + it('should list objects with context filters', async () => { + const noContextFileName = `no-context-${fileName}`; + await bucket.upload(filePath, {destination: noContextFileName}); + // Ensure it has no contexts + await bucket + .file(noContextFileName) + .setMetadata({contexts: {custom: null}}); + + const output = execSync(`node listObjectContexts.js ${bucketName}`); + + // Testing Existence of Value Pair (contexts."key"="val") + assert.include( + output, + 'Files matching filter [contexts."priority"="high"]' + ); + assert.include(output, ` - ${fileName}`); + + // Testing Existence of Key (contexts."key":*) + assert.include(output, 'Files with the "team-owner" context key'); + assert.include(output, ` - ${fileName}`); + + // Testing Absence of Value Pair (-contexts."key"="val") + assert.include( + output, + 'Files matching absence of value pair [-contexts."priority"="high"]' + ); + assert.include(output, ` - ${noContextFileName}`); + + // Testing Absence of Key (-contexts."key":*) + assert.include( + output, + 'Files matching absence of key regardless of value [-contexts."team-owner":*]' + ); + assert.include(output, ` - ${noContextFileName}`); + + const [files] = await bucket.getFiles(); + const targetFile = files.find(f => f.name === fileName); + + assert.exists(targetFile); + const [metadata] = await targetFile.getMetadata(); + + // Verify the state that the list filter is supposed to find + assert.strictEqual(metadata.contexts?.custom?.priority?.value, 'high'); + + await bucket.file(noContextFileName).delete(); + }); + }); +}); + +function generateName() { + return `nodejs-storage-samples-${uuid.v4()}`; +} From 2d3cd728d574fb970dd22a3838c3fd53e02e0175 Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Thu, 21 May 2026 16:25:51 -0600 Subject: [PATCH 15/16] test(secret-manager): fix ALREADY_EXISTS in delayed destroy tests by using unique UUIDs (#4320) * test(secret-manager): fix ALREADY_EXISTS in delayed destroy tests by using unique UUIDs * test(secret-manager): ensure resource cleanup on assertion failure using try-finally --- secret-manager/test/secretmanager.test.js | 90 ++++++++++++++--------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/secret-manager/test/secretmanager.test.js b/secret-manager/test/secretmanager.test.js index f7a3ad684b1..1c6873363c9 100644 --- a/secret-manager/test/secretmanager.test.js +++ b/secret-manager/test/secretmanager.test.js @@ -687,9 +687,12 @@ describe('Secret Manager samples', () => { }); it('disables a secret delayed destroy', async () => { + const customSecretId = `${secretId}-${v4()}`; + const fullSecretName = `projects/${projectId}/secrets/${customSecretId}`; + await client.createSecret({ parent: `projects/${projectId}`, - secretId: `${secretId}-delayedDestroy`, + secretId: customSecretId, secret: { replication: { automatic: {}, @@ -700,21 +703,26 @@ describe('Secret Manager samples', () => { }, }); - const output = execSync( - `node disableSecretDelayedDestroy.js ${secret.name}-delayedDestroy` - ); - assert.match(output, new RegExp('Disabled delayed destroy')); - - await client.deleteSecret({ - name: `${secret.name}-delayedDestroy`, - }); + try { + const output = execSync( + `node disableSecretDelayedDestroy.js ${fullSecretName}` + ); + assert.match(output, new RegExp('Disabled delayed destroy')); + } finally { + await client.deleteSecret({ + name: fullSecretName, + }); + } }); it('updates a secret delayed destroy', async () => { + const customSecretId = `${secretId}-${v4()}`; + const fullSecretName = `projects/${projectId}/secrets/${customSecretId}`; const updatedTimeToLive = 24 * 60 * 60 * 2; + await client.createSecret({ parent: `projects/${projectId}`, - secretId: `${secretId}-delayedDestroy`, + secretId: customSecretId, secret: { replication: { automatic: {}, @@ -725,13 +733,16 @@ describe('Secret Manager samples', () => { }, }); - const output = execSync( - `node updateSecretWithDelayedDestroy.js ${secret.name}-delayedDestroy ${updatedTimeToLive}` - ); - assert.match(output, new RegExp('Updated secret')); - await client.deleteSecret({ - name: `${secret.name}-delayedDestroy`, - }); + try { + const output = execSync( + `node updateSecretWithDelayedDestroy.js ${fullSecretName} ${updatedTimeToLive}` + ); + assert.match(output, new RegExp('Updated secret')); + } finally { + await client.deleteSecret({ + name: fullSecretName, + }); + } }); it('creates a regional secret with delayed destroy', async () => { @@ -743,9 +754,12 @@ describe('Secret Manager samples', () => { }); it('disables a regional secret delayed destroy', async () => { + const customSecretId = `${secretId}-${v4()}`; + const fullSecretName = `projects/${projectId}/locations/${locationId}/secrets/${customSecretId}`; + await regionalClient.createSecret({ parent: `projects/${projectId}/locations/${locationId}`, - secretId: `${secretId}-delayedDestroy`, + secretId: customSecretId, secret: { version_destroy_ttl: { seconds: 24 * 60 * 60, @@ -753,21 +767,26 @@ describe('Secret Manager samples', () => { }, }); - const output = execSync( - `node regional_samples/disableRegionalSecretDelayedDestroy.js ${projectId} ${locationId} ${secretId}-delayedDestroy` - ); - assert.match(output, new RegExp('Disabled delayed destroy')); - - await regionalClient.deleteSecret({ - name: `projects/${projectId}/locations/${locationId}/secrets/${secretId}-delayedDestroy`, - }); + try { + const output = execSync( + `node regional_samples/disableRegionalSecretDelayedDestroy.js ${projectId} ${locationId} ${customSecretId}` + ); + assert.match(output, new RegExp('Disabled delayed destroy')); + } finally { + await regionalClient.deleteSecret({ + name: fullSecretName, + }); + } }); it('updates a regional secret delayed destroy', async () => { + const customSecretId = `${secretId}-${v4()}`; + const fullSecretName = `projects/${projectId}/locations/${locationId}/secrets/${customSecretId}`; + const updatedTimeToLive = 24 * 60 * 60 * 2; await regionalClient.createSecret({ parent: `projects/${projectId}/locations/${locationId}`, - secretId: `${secretId}-delayedDestroy`, + secretId: customSecretId, secret: { version_destroy_ttl: { seconds: 24 * 60 * 60, @@ -775,13 +794,16 @@ describe('Secret Manager samples', () => { }, }); - const output = execSync( - `node regional_samples/updateRegionalSecretWithDelayedDestroy.js ${projectId} ${locationId} ${secretId}-delayedDestroy ${updatedTimeToLive}` - ); - assert.match(output, new RegExp('Updated regional secret')); - await regionalClient.deleteSecret({ - name: `projects/${projectId}/locations/${locationId}/secrets/${secretId}-delayedDestroy`, - }); + try { + const output = execSync( + `node regional_samples/updateRegionalSecretWithDelayedDestroy.js ${projectId} ${locationId} ${customSecretId} ${updatedTimeToLive}` + ); + assert.match(output, new RegExp('Updated regional secret')); + } finally { + await regionalClient.deleteSecret({ + name: fullSecretName, + }); + } }); it('creates secret with tags', async () => { From 0f78d3cf6001cf7384fa058338d92b217c96c910 Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Thu, 21 May 2026 16:33:06 -0600 Subject: [PATCH 16/16] test(talent): add explicit delay to wait for job indexing (#4317) * test(talent): add explicit delay to wait for job indexing * test(talent): move indexing delay to before hook to avoid waiting on every retry --------- Co-authored-by: Jennifer Davis --- talent/test/talent.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/talent/test/talent.test.js b/talent/test/talent.test.js index bd1e769e96f..7ad396dfee0 100644 --- a/talent/test/talent.test.js +++ b/talent/test/talent.test.js @@ -47,6 +47,7 @@ describe('Talent Solution Jobs API v4 samples', () => { const tenantService = new talent.TenantServiceClient(); const companyService = new talent.CompanyServiceClient(); const jobService = new talent.JobServiceClient(); + const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); let tenant; let company; @@ -101,6 +102,7 @@ describe('Talent Solution Jobs API v4 samples', () => { }); console.log(`created job: ${job.name}`); jobId = job.name.split('/').slice(-1)[0]; + await delay(10000); }); after(async () => {