Skip to content
Open
14 changes: 7 additions & 7 deletions functions/scheduleinstance/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<img src="https://avatars2.githubusercontent.com/u/2810941?v=3&s=96" alt="Google Cloud Platform logo" title="Google Cloud Platform" align="right" height="96" width="96"/>

# 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

Expand All @@ -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
180 changes: 101 additions & 79 deletions functions/scheduleinstance/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,133 +14,155 @@

// [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) => {
functions.cloudEvent('startInstanceEvent', async cloudEvent => {
try {
const project = await instancesClient.getProjectId();
const payload = _validatePayload(event);
const options = {
filter: `labels.${payload.label}`,
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,
zone: payload.zone,
};
filter,
});

const [instances] = await instancesClient.list(options);
// 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 => {
const [response] = await instancesClient.start({
console.log(`Starting Cloud SQL instance: ${instance.name}`);
const request = {
project,
zone: payload.zone,
instance: instance.name,
});

return waitForOperation(project, response.latestResponse);
body: {
settings: {
activationPolicy: 'ALWAYS',
},
},
};
const [operation] = await sqlInstancesClient.patch(request);
console.log(
`Patch operation started for ${instance.name}: ${operation.name}`
);
})
);

// Operation complete. Instance successfully started.
const message = 'Successfully started instance(s)';
console.log(message);
callback(null, message);
} catch (err) {
console.log(err);
callback(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) => {
functions.cloudEvent('stopInstanceEvent', async cloudEvent => {
try {
const project = await instancesClient.getProjectId();
const payload = _validatePayload(event);
const options = {
filter: `labels.${payload.label}`,
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,
zone: payload.zone,
};
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.`
);

const [instances] = await instancesClient.list(options);
if (instances.length === 0) {
console.log(
`No SQL instances found in project '${project}' matching filter '${filter}'.`
);
return;
}

await Promise.all(
instances.map(async instance => {
const [response] = await instancesClient.stop({
console.log(`Stopping Cloud SQL instance: ${instance.name}`);
const request = {
project,
zone: payload.zone,
instance: instance.name,
});

return waitForOperation(project, response.latestResponse);
body: {
settings: {
activationPolicy: 'NEVER',
},
},
};
const [operation] = await sqlInstancesClient.patch(request);
console.log(
`Patch operation started for ${instance.name}: ${operation.name}`
);
})
);

// Operation complete. Instance successfully stopped.
const message = 'Successfully stopped instance(s)';
console.log(message);
callback(null, message);
} catch (err) {
console.log(err);
callback(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 => {
const _validatePayload = cloudEvent => {
let payload;
try {
payload = JSON.parse(Buffer.from(event.data, 'base64').toString());
const base64Data = cloudEvent.data.message.data;
payload = JSON.parse(Buffer.from(base64Data, 'base64').toString());
} catch (err) {
throw new Error('Invalid Pub/Sub message: ' + err);
throw new Error('Invalid CloudEvent / Pub/Sub message: ' + err);
}
if (!payload.zone) {
throw new Error("Attribute 'zone' missing from payload");
} else if (!payload.label) {
if (!payload.label) {
throw new Error("Attribute 'label' missing from payload");
}
Comment thread
camiekim marked this conversation as resolved.
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]
Expand Down
5 changes: 3 additions & 2 deletions functions/scheduleinstance/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "cloud-functions-schedule-instance",
"name": "cloud-functions-schedule-sql",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
Expand All @@ -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"
}
}
Loading
Loading