From 7bf829bfa9857f84189c3c6ef7396d1fd22b4bf9 Mon Sep 17 00:00:00 2001 From: Camie Kim Date: Fri, 10 Jul 2026 11:36:19 -0400 Subject: [PATCH 1/9] Update README for Cloud SQL instance scheduling --- functions/scheduleinstance/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/functions/scheduleinstance/README.md b/functions/scheduleinstance/README.md index 3605201d940..dff1c7109db 100644 --- a/functions/scheduleinstance/README.md +++ b/functions/scheduleinstance/README.md @@ -1,12 +1,12 @@ Google Cloud Platform logo -# Google Cloud Functions - Scheduling GCE Instances sample +# Cloud Run functions - Scheduling Cloud SQL instances sample ## Deploy and run the sample -See the [Scheduling Instances with Cloud Scheduler tutorial][tutorial]. +See the [Schedule a Cloud SQL instance to start or stop tutorial][tutorial]. -[tutorial]: https://cloud.google.com/scheduler/docs/scheduling-instances-with-cloud-scheduler +[tutorial]: https://docs.cloud.google.com/scheduler/docs/start-and-stop-sql-server-instances-on-a-schedule ## Run the tests @@ -20,8 +20,8 @@ See the [Scheduling Instances with Cloud Scheduler tutorial][tutorial]. ## Additional resources -* [GCE NodeJS Client Library documentation][compute_nodejs_docs] -* [Background Cloud Functions documentation][background_functions_docs] +* [Cloud Run functions client libraries][functions_nodejs_docs] +* [Write Cloud Run functions][background_functions_docs] -[compute_nodejs_docs]: https://cloud.google.com/compute/docs/tutorials/nodejs-guide -[background_functions_docs]: https://cloud.google.com/functions/docs/writing/background +[functions_nodejs_docs]: https://docs.cloud.google.com/functions/docs/apis/libraries +[background_functions_docs]: https://docs.cloud.google.com/run/docs/write-functions From db02a04bc4ef0f8eda7aaecfb70a3f09cc877d6b Mon Sep 17 00:00:00 2001 From: Camie Kim Date: Fri, 10 Jul 2026 13:30:10 -0400 Subject: [PATCH 2/9] Fix link formatting in README.md --- functions/scheduleinstance/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/scheduleinstance/README.md b/functions/scheduleinstance/README.md index dff1c7109db..efaaacc30e5 100644 --- a/functions/scheduleinstance/README.md +++ b/functions/scheduleinstance/README.md @@ -4,7 +4,7 @@ ## Deploy and run the sample -See the [Schedule a Cloud SQL instance to start or stop tutorial][tutorial]. +See the [Schedule a Cloud SQL instance to start or stop][tutorial] tutorial. [tutorial]: https://docs.cloud.google.com/scheduler/docs/start-and-stop-sql-server-instances-on-a-schedule From 185389ed67447bf9b689f52f7a10ed12d5e86624 Mon Sep 17 00:00:00 2001 From: Camie Kim Date: Fri, 10 Jul 2026 13:31:07 -0400 Subject: [PATCH 3/9] Rename package and update dependencies --- functions/scheduleinstance/package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/functions/scheduleinstance/package.json b/functions/scheduleinstance/package.json index d050b4e0167..be46d624a11 100644 --- a/functions/scheduleinstance/package.json +++ b/functions/scheduleinstance/package.json @@ -1,5 +1,5 @@ { - "name": "cloud-functions-schedule-instance", + "name": "cloud-functions-schedule-sql", "version": "0.1.0", "private": true, "license": "Apache-2.0", @@ -21,6 +21,7 @@ "sinon": "^18.0.0" }, "dependencies": { - "@google-cloud/compute": "^4.0.0" + "@google-cloud/functions-framework": "^3.0.0", + "@google-cloud/sql": "^0.20.1" } } From 5699b148e096c5a64382a49a860f8e557b649ce1 Mon Sep 17 00:00:00 2001 From: Camie Kim Date: Fri, 10 Jul 2026 16:07:42 -0400 Subject: [PATCH 4/9] Refactor to manage Cloud SQL instances with CloudEvents --- functions/scheduleinstance/index.js | 219 ++++++++++++++-------------- 1 file changed, 111 insertions(+), 108 deletions(-) diff --git a/functions/scheduleinstance/index.js b/functions/scheduleinstance/index.js index 66c5312b90e..8f1df6cc174 100644 --- a/functions/scheduleinstance/index.js +++ b/functions/scheduleinstance/index.js @@ -14,134 +14,137 @@ // [START functions_start_instance_pubsub] // [START functions_stop_instance_pubsub] -const compute = require('@google-cloud/compute'); -const instancesClient = new compute.InstancesClient(); -const operationsClient = new compute.ZoneOperationsClient(); - -async function waitForOperation(projectId, operation) { - while (operation.status !== 'DONE') { - [operation] = await operationsClient.wait({ - operation: operation.name, - project: projectId, - zone: operation.zone.split('/').pop(), - }); - } -} +const functions = require('@google-cloud/functions-framework'); +const {SqlInstancesServiceClient} = require('@google-cloud/sql'); +const sqlInstancesClient = new SqlInstancesServiceClient({ fallback: true }); // [END functions_stop_instance_pubsub] /** - * Starts Compute Engine instances. + * Starts Cloud SQL instances. * - * Expects a PubSub message with JSON-formatted event data containing the - * following attributes: - * zone - the GCP zone the instances are located in. - * label - the label of instances to start. + * Expects a PubSub message with JSON-formatted event data containing + * the label of instances to start. * - * @param {!object} event Cloud Function PubSub message event. - * @param {!object} callback Cloud Function PubSub callback indicating - * completion. */ -exports.startInstancePubSub = async (event, context, callback) => { - try { - const project = await instancesClient.getProjectId(); - const payload = _validatePayload(event); - const options = { - filter: `labels.${payload.label}`, - project, - zone: payload.zone, - }; - - const [instances] = await instancesClient.list(options); - - await Promise.all( - instances.map(async instance => { - const [response] = await instancesClient.start({ - project, - zone: payload.zone, - instance: instance.name, - }); - - return waitForOperation(project, response.latestResponse); - }) - ); - - // Operation complete. Instance successfully started. - const message = 'Successfully started instance(s)'; - console.log(message); - callback(null, message); - } catch (err) { - console.log(err); - callback(err); +functions.cloudEvent('startInstanceEvent', async cloudEvent => { +try { + const payload = _validatePayload(cloudEvent); + const project = await sqlInstancesClient.getProjectId(); + const [labelKey, labelValue] = payload.label.split('='); + const filter = `settings.userLabels.${labelKey}:${labelValue}`; + + console.log(`Attempting to start instances. Project ID resolved to: '${project}'. Filter applied: '${filter}'`); + + // Fetch the response object + const [response] = await sqlInstancesClient.list({ + project, + filter, + }); + + // Extract the array from the 'items' property (default to empty array if undefined) + const instances = response.items || []; + + console.log(`Raw instances array retrieved. Found ${instances.length} matching instances.`); + + if (instances.length === 0) { + console.log(`No SQL instances found in project '${project}' matching filter '${filter}'.`); + return; } -}; + + await Promise.all( + instances.map(async instance => { + console.log(`Starting Cloud SQL instance: ${instance.name}`); + const request = { + project, + instance: instance.name, + body: { + settings: { + activationPolicy: 'ALWAYS', + }, + }, + }; + const [operation] = await sqlInstancesClient.patch(request); + console.log(`Patch operation started for ${instance.name}: ${operation.name}`); + }) + ); +} catch (err) { + console.error(err); + throw err; +} +}); // [END functions_start_instance_pubsub] // [START functions_stop_instance_pubsub] /** - * Stops Compute Engine instances. + * Stops Cloud SQL instances. * - * Expects a PubSub message with JSON-formatted event data containing the - * following attributes: - * zone - the GCP zone the instances are located in. - * label - the label of instances to stop. + * Expects a PubSub message with JSON-formatted event data containing + * the label of instances to stop. * - * @param {!object} event Cloud Function PubSub message event. - * @param {!object} callback Cloud Function PubSub callback indicating completion. */ -exports.stopInstancePubSub = async (event, context, callback) => { - try { - const project = await instancesClient.getProjectId(); - const payload = _validatePayload(event); - const options = { - filter: `labels.${payload.label}`, - project, - zone: payload.zone, - }; - - const [instances] = await instancesClient.list(options); - - await Promise.all( - instances.map(async instance => { - const [response] = await instancesClient.stop({ - project, - zone: payload.zone, - instance: instance.name, - }); - - return waitForOperation(project, response.latestResponse); - }) - ); - - // Operation complete. Instance successfully stopped. - const message = 'Successfully stopped instance(s)'; - console.log(message); - callback(null, message); - } catch (err) { - console.log(err); - callback(err); +functions.cloudEvent('stopInstanceEvent', async cloudEvent => { +try { + const payload = _validatePayload(cloudEvent); + const project = await sqlInstancesClient.getProjectId(); + const [labelKey, labelValue] = payload.label.split('='); + const filter = `settings.userLabels.${labelKey}:${labelValue}`; + + console.log(`Attempting to stop instances. Project ID resolved to: '${project}'. Filter applied: '${filter}'`); + + // Fetch the response object + const [response] = await sqlInstancesClient.list({ + project, + filter, + }); + + // Extract the array from the 'items' property (default to empty array if undefined) + const instances = response.items || []; + + console.log(`Raw instances array retrieved. Found ${instances.length} matching instances.`); + + if (instances.length === 0) { + console.log(`No SQL instances found in project '${project}' matching filter '${filter}'.`); + return; } -}; + + await Promise.all( + instances.map(async instance => { + console.log(`Stopping Cloud SQL instance: ${instance.name}`); + const request = { + project, + instance: instance.name, + body: { + settings: { + activationPolicy: 'NEVER', + }, + }, + }; + const [operation] = await sqlInstancesClient.patch(request); + console.log(`Patch operation started for ${instance.name}: ${operation.name}`); + }) + ); +} catch (err) { + console.error(err); + throw err; +} +}); // [START functions_start_instance_pubsub] /** * Validates that a request payload contains the expected fields. - * - * @param {!object} payload the request payload to validate. - * @return {!object} the payload object. */ -const _validatePayload = event => { - let payload; - try { - payload = JSON.parse(Buffer.from(event.data, 'base64').toString()); - } catch (err) { - throw new Error('Invalid Pub/Sub message: ' + err); - } - if (!payload.zone) { - throw new Error("Attribute 'zone' missing from payload"); - } else if (!payload.label) { - throw new Error("Attribute 'label' missing from payload"); - } - return payload; +const _validatePayload = cloudEvent => { +let payload; +try { + const base64Data = cloudEvent.data.message.data; + payload = JSON.parse(Buffer.from(base64Data, 'base64').toString()); +} catch (err) { + throw new Error('Invalid CloudEvent / Pub/Sub message: ' + err); +} +if (!payload.label) { + throw new Error("Attribute 'label' missing from payload"); +} +return payload; }; // [END functions_start_instance_pubsub] // [END functions_stop_instance_pubsub] From 4826d2b97cbfd6cc789cdd9e28e77f69e2e9f4df Mon Sep 17 00:00:00 2001 From: Camie Kim Date: Fri, 10 Jul 2026 16:13:20 -0400 Subject: [PATCH 5/9] Refactor tests for start and stop instance events --- functions/scheduleinstance/test/index.test.js | 263 +++++++----------- 1 file changed, 107 insertions(+), 156 deletions(-) diff --git a/functions/scheduleinstance/test/index.test.js b/functions/scheduleinstance/test/index.test.js index 274c0581199..4311ec12d20 100644 --- a/functions/scheduleinstance/test/index.test.js +++ b/functions/scheduleinstance/test/index.test.js @@ -19,210 +19,161 @@ const proxyquire = require('proxyquire').noCallThru(); const assert = require('assert'); const getSample = () => { - const requestPromise = sinon - .stub() - .returns(new Promise(resolve => resolve('request sent'))); + const getProjectIdStub = sinon.stub().resolves('test-project'); + const listStub = sinon.stub().resolves([{ items: [{ name: 'test-instance' }] }]); + const patchStub = sinon.stub().resolves([{ name: 'test-operation' }]); + const registeredFunctions = {}; + + proxyquire('../index', { + '@google-cloud/functions-framework': { + cloudEvent: (name, handler) => { + registeredFunctions[name] = handler; + }, + }, + '@google-cloud/sql': { + SqlInstancesServiceClient: function () { + this.getProjectId = getProjectIdStub; + this.list = listStub; + this.patch = patchStub; + }, + }, + }); return { - program: proxyquire('../', { - '@google-cloud/compute': { - InstancesClient: function client() { - this.list = () => new Promise(resolve => resolve([[]])); - this.start = () => new Promise(resolve => resolve('request sent')); - this.getProjectId = () => 'project'; - }, - ZoneOperationsClient: function client() { - this.wait = () => new Promise(resolve => resolve('request sent')); - }, - }, - }), + registeredFunctions, mocks: { - requestPromise: requestPromise, + getProjectIdStub, + listStub, + patchStub, }, }; }; -const getMocks = () => { - const event = { - data: {}, - }; - - const callback = sinon.spy(); - - return { - event: event, - context: {}, - callback: callback, - }; -}; +const getCloudEvent = (data) => ({ + data: { + message: { + data: Buffer.from(JSON.stringify(data)).toString('base64'), + }, + }, +}); const stubConsole = function () { sinon.stub(console, 'error'); sinon.stub(console, 'log'); }; -//Restore console const restoreConsole = function () { console.log.restore(); console.error.restore(); }; + beforeEach(stubConsole); afterEach(restoreConsole); -/** Tests for startInstancePubSub */ -describe('functions_start_instance_pubsub', () => { - it('startInstancePubSub: should accept JSON-formatted event payload with label', async () => { - const mocks = getMocks(); +describe('functions_start_instance_event', () => { + it('startInstanceEvent: should accept CloudEvent payload with label', async () => { const sample = getSample(); - const pubsubData = {zone: 'test-zone', label: 'testkey=value'}; - mocks.event.data = Buffer.from(JSON.stringify(pubsubData)).toString( - 'base64' - ); - await sample.program.startInstancePubSub( - mocks.event, - mocks.context, - mocks.callback - ); - - assert.deepStrictEqual( - mocks.callback.firstCall.args[1], - 'Successfully started instance(s)' - ); + const cloudEvent = getCloudEvent({ label: 'env=dev' }); + + await sample.registeredFunctions.startInstanceEvent(cloudEvent); + + assert.strictEqual(sample.mocks.getProjectIdStub.calledOnce, true); + assert.strictEqual(sample.mocks.listStub.calledOnce, true); + assert.deepStrictEqual(sample.mocks.listStub.firstCall.args[0], { + project: 'test-project', + filter: 'settings.userLabels.env:dev', + }); + assert.strictEqual(sample.mocks.patchStub.calledOnce, true); + assert.deepStrictEqual(sample.mocks.patchStub.firstCall.args[0], { + project: 'test-project', + instance: 'test-instance', + body: { + settings: { + activationPolicy: 'ALWAYS', + }, + }, + }); }); - it("startInstancePubSub: should fail with missing 'zone' attribute", async () => { - const mocks = getMocks(); + it("startInstanceEvent: should fail with missing 'label' attribute", async () => { const sample = getSample(); - const pubsubData = {label: 'testkey=value'}; - mocks.event.data = Buffer.from(JSON.stringify(pubsubData)).toString( - 'base64' - ); - await sample.program.startInstancePubSub( - mocks.event, - mocks.context, - mocks.callback - ); + const cloudEvent = getCloudEvent({ other: 'value' }); - assert.deepStrictEqual( - mocks.callback.firstCall.args[0], - new Error("Attribute 'zone' missing from payload") + await assert.rejects( + sample.registeredFunctions.startInstanceEvent(cloudEvent), + /Attribute 'label' missing from payload/ ); }); - it("startInstancePubSub: should fail with missing 'label' attribute", async () => { - const mocks = getMocks(); + it('startInstanceEvent: should handle zero instances gracefully', async () => { const sample = getSample(); - const pubsubData = {zone: 'test-zone'}; - mocks.event.data = Buffer.from(JSON.stringify(pubsubData)).toString( - 'base64' - ); - await sample.program.startInstancePubSub( - mocks.event, - mocks.context, - mocks.callback - ); + sample.mocks.listStub.resolves([{}]); // Empty object missing 'items' property + const cloudEvent = getCloudEvent({ label: 'env=dev' }); - assert.deepStrictEqual( - mocks.callback.firstCall.args[0], - new Error("Attribute 'label' missing from payload") - ); + await sample.registeredFunctions.startInstanceEvent(cloudEvent); + + assert.strictEqual(sample.mocks.patchStub.called, false); }); - it('startInstancePubSub: should fail with empty event payload', async () => { - const mocks = getMocks(); + it('startInstanceEvent: should fail with invalid CloudEvent payload', async () => { const sample = getSample(); - const pubsubData = {}; - mocks.event.data = Buffer.from(JSON.stringify(pubsubData)).toString( - 'base64' - ); - await sample.program.startInstancePubSub( - mocks.event, - mocks.context, - mocks.callback - ); + const cloudEvent = { + data: { + message: { + data: 'invalid-base64-payload', + }, + }, + }; - assert.deepStrictEqual( - mocks.callback.firstCall.args[0], - new Error("Attribute 'zone' missing from payload") + await assert.rejects( + sample.registeredFunctions.startInstanceEvent(cloudEvent), + /Invalid CloudEvent \/ Pub\/Sub message/ ); }); }); -/** Tests for stopInstancePubSub */ -describe('functions_stop_instance_pubsub', () => { - it('stopInstancePubSub: should accept JSON-formatted event payload with label', async () => { - const mocks = getMocks(); +describe('functions_stop_instance_event', () => { + it('stopInstanceEvent: should accept CloudEvent payload with label', async () => { const sample = getSample(); - const pubsubData = {zone: 'test-zone', label: 'testkey=value'}; - mocks.event.data = Buffer.from(JSON.stringify(pubsubData)).toString( - 'base64' - ); - await sample.program.stopInstancePubSub( - mocks.event, - mocks.context, - mocks.callback - ); - - assert.deepStrictEqual( - mocks.callback.firstCall.args[1], - 'Successfully stopped instance(s)' - ); + const cloudEvent = getCloudEvent({ label: 'env=dev' }); + + await sample.registeredFunctions.stopInstanceEvent(cloudEvent); + + assert.strictEqual(sample.mocks.getProjectIdStub.calledOnce, true); + assert.strictEqual(sample.mocks.listStub.calledOnce, true); + assert.deepStrictEqual(sample.mocks.listStub.firstCall.args[0], { + project: 'test-project', + filter: 'settings.userLabels.env:dev', + }); + assert.strictEqual(sample.mocks.patchStub.calledOnce, true); + assert.deepStrictEqual(sample.mocks.patchStub.firstCall.args[0], { + project: 'test-project', + instance: 'test-instance', + body: { + settings: { + activationPolicy: 'NEVER', + }, + }, + }); }); - it("stopInstancePubSub: should fail with missing 'zone' attribute", async () => { - const mocks = getMocks(); + it("stopInstanceEvent: should fail with missing 'label' attribute", async () => { const sample = getSample(); - const pubsubData = {label: 'testkey=value'}; - mocks.event.data = Buffer.from(JSON.stringify(pubsubData)).toString( - 'base64' - ); - await sample.program.stopInstancePubSub( - mocks.event, - mocks.context, - mocks.callback - ); + const cloudEvent = getCloudEvent({ other: 'value' }); - assert.deepStrictEqual( - mocks.callback.firstCall.args[0], - new Error("Attribute 'zone' missing from payload") + await assert.rejects( + sample.registeredFunctions.stopInstanceEvent(cloudEvent), + /Attribute 'label' missing from payload/ ); }); - it("stopInstancePubSub: should fail with missing 'label' attribute", async () => { - const mocks = getMocks(); + it('stopInstanceEvent: should handle zero instances gracefully', async () => { const sample = getSample(); - const pubsubData = {zone: 'test-zone'}; - mocks.event.data = Buffer.from(JSON.stringify(pubsubData)).toString( - 'base64' - ); - await sample.program.stopInstancePubSub( - mocks.event, - mocks.context, - mocks.callback - ); - - assert.deepStrictEqual( - mocks.callback.firstCall.args[0], - new Error("Attribute 'label' missing from payload") - ); - }); + sample.mocks.listStub.resolves([{ items: [] }]); // Empty items array + const cloudEvent = getCloudEvent({ label: 'env=dev' }); - it('stopInstancePubSub: should fail with empty event payload', async () => { - const mocks = getMocks(); - const sample = getSample(); - const pubsubData = {}; - mocks.event.data = Buffer.from(JSON.stringify(pubsubData)).toString( - 'base64' - ); - await sample.program.stopInstancePubSub( - mocks.event, - mocks.context, - mocks.callback - ); + await sample.registeredFunctions.stopInstanceEvent(cloudEvent); - assert.deepStrictEqual( - mocks.callback.firstCall.args[0], - new Error("Attribute 'zone' missing from payload") - ); + assert.strictEqual(sample.mocks.patchStub.called, false); }); }); From 4bd9f4c9a1dde31a89253a882d1b9ece96fbfe89 Mon Sep 17 00:00:00 2001 From: Camie Kim Date: Fri, 10 Jul 2026 17:14:57 -0400 Subject: [PATCH 6/9] Fix lint errors --- functions/scheduleinstance/index.js | 211 +++++++++++++++------------- 1 file changed, 113 insertions(+), 98 deletions(-) diff --git a/functions/scheduleinstance/index.js b/functions/scheduleinstance/index.js index 8f1df6cc174..5e02db8c775 100644 --- a/functions/scheduleinstance/index.js +++ b/functions/scheduleinstance/index.js @@ -16,7 +16,7 @@ // [START functions_stop_instance_pubsub] const functions = require('@google-cloud/functions-framework'); const {SqlInstancesServiceClient} = require('@google-cloud/sql'); -const sqlInstancesClient = new SqlInstancesServiceClient({ fallback: true }); +const sqlInstancesClient = new SqlInstancesServiceClient({fallback: true}); // [END functions_stop_instance_pubsub] /** @@ -27,50 +27,58 @@ const sqlInstancesClient = new SqlInstancesServiceClient({ fallback: true }); * */ functions.cloudEvent('startInstanceEvent', async cloudEvent => { -try { - const payload = _validatePayload(cloudEvent); - const project = await sqlInstancesClient.getProjectId(); - const [labelKey, labelValue] = payload.label.split('='); - const filter = `settings.userLabels.${labelKey}:${labelValue}`; - - console.log(`Attempting to start instances. Project ID resolved to: '${project}'. Filter applied: '${filter}'`); - - // Fetch the response object - const [response] = await sqlInstancesClient.list({ - project, - filter, - }); - - // Extract the array from the 'items' property (default to empty array if undefined) - const instances = response.items || []; - - console.log(`Raw instances array retrieved. Found ${instances.length} matching instances.`); - - if (instances.length === 0) { - console.log(`No SQL instances found in project '${project}' matching filter '${filter}'.`); - return; - } - - await Promise.all( - instances.map(async instance => { - console.log(`Starting Cloud SQL instance: ${instance.name}`); - const request = { - project, - instance: instance.name, - body: { - settings: { - activationPolicy: 'ALWAYS', + try { + const payload = _validatePayload(cloudEvent); + const project = await sqlInstancesClient.getProjectId(); + const [labelKey, labelValue] = payload.label.split('='); + const filter = `settings.userLabels.${labelKey}:${labelValue}`; + + console.log( + `Attempting to start instances. Project ID resolved to: '${project}'. Filter applied: '${filter}'` + ); + + // Fetch the response object + const [response] = await sqlInstancesClient.list({ + project, + filter, + }); + + // Extract the array from the 'items' property (default to empty array if undefined) + const instances = response.items || []; + + console.log( + `Raw instances array retrieved. Found ${instances.length} matching instances.` + ); + + if (instances.length === 0) { + console.log( + `No SQL instances found in project '${project}' matching filter '${filter}'.` + ); + return; + } + + await Promise.all( + instances.map(async instance => { + console.log(`Starting Cloud SQL instance: ${instance.name}`); + const request = { + project, + instance: instance.name, + body: { + settings: { + activationPolicy: 'ALWAYS', + }, }, - }, - }; - const [operation] = await sqlInstancesClient.patch(request); - console.log(`Patch operation started for ${instance.name}: ${operation.name}`); - }) - ); -} catch (err) { - console.error(err); - throw err; -} + }; + const [operation] = await sqlInstancesClient.patch(request); + console.log( + `Patch operation started for ${instance.name}: ${operation.name}` + ); + }) + ); + } catch (err) { + console.error(err); + throw err; + } }); // [END functions_start_instance_pubsub] // [START functions_stop_instance_pubsub] @@ -83,50 +91,57 @@ try { * */ functions.cloudEvent('stopInstanceEvent', async cloudEvent => { -try { - const payload = _validatePayload(cloudEvent); - const project = await sqlInstancesClient.getProjectId(); - const [labelKey, labelValue] = payload.label.split('='); - const filter = `settings.userLabels.${labelKey}:${labelValue}`; - - console.log(`Attempting to stop instances. Project ID resolved to: '${project}'. Filter applied: '${filter}'`); - - // Fetch the response object - const [response] = await sqlInstancesClient.list({ - project, - filter, - }); - - // Extract the array from the 'items' property (default to empty array if undefined) - const instances = response.items || []; - - console.log(`Raw instances array retrieved. Found ${instances.length} matching instances.`); - - if (instances.length === 0) { - console.log(`No SQL instances found in project '${project}' matching filter '${filter}'.`); - return; - } - - await Promise.all( - instances.map(async instance => { - console.log(`Stopping Cloud SQL instance: ${instance.name}`); - const request = { - project, - instance: instance.name, - body: { - settings: { - activationPolicy: 'NEVER', + try { + const payload = _validatePayload(cloudEvent); + const project = await sqlInstancesClient.getProjectId(); + const [labelKey, labelValue] = payload.label.split('='); + const filter = `settings.userLabels.${labelKey}:${labelValue}`; + + console.log( + `Attempting to stop instances. Project ID resolved to: '${project}'. Filter applied: '${filter}'` + ); + + // Fetch the response object + const [response] = await sqlInstancesClient.list({ + project, + filter, + }); + + // Extract the array from the 'items' property (default to empty array if undefined) + const instances = response.items || []; + + console.log( + `Raw instances array retrieved. Found ${instances.length} matching instances.` + ); + + if (instances.length === 0) { + console.log(` + No SQL instances found in project '${project}' matching filter '${filter}'.`); + return; + } + + await Promise.all( + instances.map(async instance => { + console.log(`Stopping Cloud SQL instance: ${instance.name}`); + const request = { + project, + instance: instance.name, + body: { + settings: { + activationPolicy: 'NEVER', + }, }, - }, - }; - const [operation] = await sqlInstancesClient.patch(request); - console.log(`Patch operation started for ${instance.name}: ${operation.name}`); - }) - ); -} catch (err) { - console.error(err); - throw err; -} + }; + const [operation] = await sqlInstancesClient.patch(request); + console.log( + `Patch operation started for ${instance.name}: ${operation.name}` + ); + }) + ); + } catch (err) { + console.error(err); + throw err; + } }); // [START functions_start_instance_pubsub] @@ -134,17 +149,17 @@ try { * Validates that a request payload contains the expected fields. */ const _validatePayload = cloudEvent => { -let payload; -try { - const base64Data = cloudEvent.data.message.data; - payload = JSON.parse(Buffer.from(base64Data, 'base64').toString()); -} catch (err) { - throw new Error('Invalid CloudEvent / Pub/Sub message: ' + err); -} -if (!payload.label) { - throw new Error("Attribute 'label' missing from payload"); -} -return payload; + let payload; + try { + const base64Data = cloudEvent.data.message.data; + payload = JSON.parse(Buffer.from(base64Data, 'base64').toString()); + } catch (err) { + throw new Error('Invalid CloudEvent / Pub/Sub message: ' + err); + } + if (!payload.label) { + throw new Error("Attribute 'label' missing from payload"); + } + return payload; }; // [END functions_start_instance_pubsub] // [END functions_stop_instance_pubsub] From 68c916b84f522d0834c20154b1a225283439b2f2 Mon Sep 17 00:00:00 2001 From: Camie Kim Date: Fri, 10 Jul 2026 17:22:03 -0400 Subject: [PATCH 7/9] Fix lint errors --- functions/scheduleinstance/test/index.test.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/functions/scheduleinstance/test/index.test.js b/functions/scheduleinstance/test/index.test.js index 4311ec12d20..d96e0e82727 100644 --- a/functions/scheduleinstance/test/index.test.js +++ b/functions/scheduleinstance/test/index.test.js @@ -20,8 +20,8 @@ const assert = require('assert'); const getSample = () => { const getProjectIdStub = sinon.stub().resolves('test-project'); - const listStub = sinon.stub().resolves([{ items: [{ name: 'test-instance' }] }]); - const patchStub = sinon.stub().resolves([{ name: 'test-operation' }]); + const listStub = sinon.stub().resolves([{items: [{name: 'test-instance'}]}]); + const patchStub = sinon.stub().resolves([{name: 'test-operation'}]); const registeredFunctions = {}; proxyquire('../index', { @@ -38,7 +38,7 @@ const getSample = () => { }, }, }); - +cd return { registeredFunctions, mocks: { @@ -49,7 +49,7 @@ const getSample = () => { }; }; -const getCloudEvent = (data) => ({ +const getCloudEvent = data => ({ data: { message: { data: Buffer.from(JSON.stringify(data)).toString('base64'), @@ -73,7 +73,7 @@ afterEach(restoreConsole); describe('functions_start_instance_event', () => { it('startInstanceEvent: should accept CloudEvent payload with label', async () => { const sample = getSample(); - const cloudEvent = getCloudEvent({ label: 'env=dev' }); + const cloudEvent = getCloudEvent({label: 'env=dev'}); await sample.registeredFunctions.startInstanceEvent(cloudEvent); @@ -97,7 +97,7 @@ describe('functions_start_instance_event', () => { it("startInstanceEvent: should fail with missing 'label' attribute", async () => { const sample = getSample(); - const cloudEvent = getCloudEvent({ other: 'value' }); + const cloudEvent = getCloudEvent({other: 'value'}); await assert.rejects( sample.registeredFunctions.startInstanceEvent(cloudEvent), @@ -108,7 +108,7 @@ describe('functions_start_instance_event', () => { it('startInstanceEvent: should handle zero instances gracefully', async () => { const sample = getSample(); sample.mocks.listStub.resolves([{}]); // Empty object missing 'items' property - const cloudEvent = getCloudEvent({ label: 'env=dev' }); + const cloudEvent = getCloudEvent({label: 'env=dev'}); await sample.registeredFunctions.startInstanceEvent(cloudEvent); @@ -135,7 +135,7 @@ describe('functions_start_instance_event', () => { describe('functions_stop_instance_event', () => { it('stopInstanceEvent: should accept CloudEvent payload with label', async () => { const sample = getSample(); - const cloudEvent = getCloudEvent({ label: 'env=dev' }); + const cloudEvent = getCloudEvent({label: 'env=dev'}); await sample.registeredFunctions.stopInstanceEvent(cloudEvent); @@ -159,7 +159,7 @@ describe('functions_stop_instance_event', () => { it("stopInstanceEvent: should fail with missing 'label' attribute", async () => { const sample = getSample(); - const cloudEvent = getCloudEvent({ other: 'value' }); + const cloudEvent = getCloudEvent({other: 'value'}); await assert.rejects( sample.registeredFunctions.stopInstanceEvent(cloudEvent), @@ -169,8 +169,8 @@ describe('functions_stop_instance_event', () => { it('stopInstanceEvent: should handle zero instances gracefully', async () => { const sample = getSample(); - sample.mocks.listStub.resolves([{ items: [] }]); // Empty items array - const cloudEvent = getCloudEvent({ label: 'env=dev' }); + sample.mocks.listStub.resolves([{items: []}]); // Empty items array + const cloudEvent = getCloudEvent({label: 'env=dev'}); await sample.registeredFunctions.stopInstanceEvent(cloudEvent); From 494c48b552f55a40da9bcdde4fec42229717ee26 Mon Sep 17 00:00:00 2001 From: Camie Kim Date: Fri, 10 Jul 2026 17:39:35 -0400 Subject: [PATCH 8/9] Fix formatting issue in index.test.js --- functions/scheduleinstance/test/index.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/scheduleinstance/test/index.test.js b/functions/scheduleinstance/test/index.test.js index d96e0e82727..ddc57fc6579 100644 --- a/functions/scheduleinstance/test/index.test.js +++ b/functions/scheduleinstance/test/index.test.js @@ -38,7 +38,7 @@ const getSample = () => { }, }, }); -cd + return { registeredFunctions, mocks: { From e45d432a5e620c3bd3012f0181465c82173c9784 Mon Sep 17 00:00:00 2001 From: Camie Kim Date: Fri, 10 Jul 2026 17:41:49 -0400 Subject: [PATCH 9/9] Refactor console log and add label validation --- functions/scheduleinstance/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/functions/scheduleinstance/index.js b/functions/scheduleinstance/index.js index 5e02db8c775..8532d2f82b8 100644 --- a/functions/scheduleinstance/index.js +++ b/functions/scheduleinstance/index.js @@ -115,8 +115,9 @@ functions.cloudEvent('stopInstanceEvent', async cloudEvent => { ); if (instances.length === 0) { - console.log(` - No SQL instances found in project '${project}' matching filter '${filter}'.`); + console.log( + `No SQL instances found in project '${project}' matching filter '${filter}'.` + ); return; } @@ -159,6 +160,9 @@ const _validatePayload = cloudEvent => { if (!payload.label) { throw new Error("Attribute 'label' missing from payload"); } + if (typeof payload.label !== 'string' || !payload.label.includes('=')) { + throw new Error("Attribute 'label' must be in the format 'key=value'"); + } return payload; }; // [END functions_start_instance_pubsub]