functions/billing/index.js (151 lines of code) (raw):

// 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. // [START functions_billing_limit] // [START functions_billing_stop] const {CloudBillingClient} = require('@google-cloud/billing'); const {InstancesClient} = require('@google-cloud/compute'); const PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT; const PROJECT_NAME = `projects/${PROJECT_ID}`; // [END functions_billing_stop] // [END functions_billing_limit] // [START functions_billing_slack] const slack = require('slack'); // TODO(developer) replace these with your own values const BOT_ACCESS_TOKEN = process.env.BOT_ACCESS_TOKEN || 'xxxx-111111111111-abcdefghidklmnopq'; const CHANNEL = process.env.SLACK_CHANNEL || 'general'; exports.notifySlack = async pubsubEvent => { const pubsubAttrs = pubsubEvent.attributes; const pubsubData = Buffer.from(pubsubEvent.data, 'base64').toString(); const budgetNotificationText = `${JSON.stringify( pubsubAttrs )}, ${pubsubData}`; await slack.chat.postMessage({ token: BOT_ACCESS_TOKEN, channel: CHANNEL, text: budgetNotificationText, }); return 'Slack notification sent successfully'; }; // [END functions_billing_slack] // [START functions_billing_stop] const billing = new CloudBillingClient(); exports.stopBilling = async pubsubEvent => { const pubsubData = JSON.parse( Buffer.from(pubsubEvent.data, 'base64').toString() ); if (pubsubData.costAmount <= pubsubData.budgetAmount) { return `No action necessary. (Current cost: ${pubsubData.costAmount})`; } if (!PROJECT_ID) { return 'No project specified'; } const billingEnabled = await _isBillingEnabled(PROJECT_NAME); if (billingEnabled) { return _disableBillingForProject(PROJECT_NAME); } else { return 'Billing already disabled'; } }; /** * Determine whether billing is enabled for a project * @param {string} projectName Name of project to check if billing is enabled * @return {bool} Whether project has billing enabled or not */ const _isBillingEnabled = async projectName => { try { const [res] = await billing.getProjectBillingInfo({name: projectName}); return res.billingEnabled; } catch (e) { console.log( 'Unable to determine if billing is enabled on specified project, assuming billing is enabled' ); return true; } }; /** * Disable billing for a project by removing its billing account * @param {string} projectName Name of project disable billing on * @return {string} Text containing response from disabling billing */ const _disableBillingForProject = async projectName => { const [res] = await billing.updateProjectBillingInfo({ name: projectName, resource: {billingAccountName: ''}, // Disable billing }); return `Billing disabled: ${JSON.stringify(res)}`; }; // [END functions_billing_stop] // Helper function to restart billing (used in tests) exports.startBilling = async pubsubEvent => { const pubsubData = JSON.parse( Buffer.from(pubsubEvent.data, 'base64').toString() ); if (!(await _isBillingEnabled(PROJECT_NAME))) { // Enable billing const [res] = await billing.updateProjectBillingInfo({ name: pubsubData.projectName, projectBillingInfo: { billingAccountName: pubsubData.billingAccountName, billingEnabled: true, }, }); return `Billing enabled: ${JSON.stringify(res)}`; } else { return 'Billing already enabled'; } }; // [START functions_billing_limit] const instancesClient = new InstancesClient(); const ZONE = 'us-central1-a'; exports.limitUse = async pubsubEvent => { const pubsubData = JSON.parse( Buffer.from(pubsubEvent.data, 'base64').toString() ); if (pubsubData.costAmount <= pubsubData.budgetAmount) { return `No action necessary. (Current cost: ${pubsubData.costAmount})`; } const instanceNames = await _listRunningInstances(PROJECT_ID, ZONE); if (!instanceNames.length) { return 'No running instances were found.'; } await _stopInstances(PROJECT_ID, ZONE, instanceNames); return `${instanceNames.length} instance(s) stopped successfully.`; }; /** * @return {Promise} Array of names of running instances */ const _listRunningInstances = async (projectId, zone) => { const [instances] = await instancesClient.list({ project: projectId, zone: zone, }); return instances .filter(item => item.status === 'RUNNING') .map(item => item.name); }; /** * @param {Array} instanceNames Names of instance to stop * @return {Promise} Response from stopping instances */ const _stopInstances = async (projectId, zone, instanceNames) => { await Promise.all( instanceNames.map(instanceName => { return instancesClient .stop({ project: projectId, zone: zone, instance: instanceName, }) .then(() => { console.log(`Instance stopped successfully: ${instanceName}`); }); }) ); }; // [END functions_billing_limit] // Helper function to restart instances (used in tests) exports.startInstances = async () => { const instanceNames = await _listStoppedInstances(PROJECT_ID, ZONE); if (!instanceNames.length) { return 'No stopped instances were found.'; } await _startInstances(PROJECT_ID, ZONE, instanceNames); return `${instanceNames.length} instance(s) started successfully.`; }; /** * @return {Promise} Array of names of running instances */ const _listStoppedInstances = async (projectId, zone) => { const [instances] = await instancesClient.list({ project: projectId, zone: zone, }); const stoppedInstances = instances.filter(item => item.status !== 'RUNNING'); return stoppedInstances.map(item => item.name); }; /** * @param {Array} instanceNames Names of instance to stop * @return {Promise} Response from stopping instances */ const _startInstances = async (projectId, zone, instanceNames) => { if (!instanceNames.length) { return 'No stopped instances were found.'; } await Promise.all( instanceNames.map(instanceName => { return instancesClient.start({ project: projectId, zone: zone, instance: instanceName, }); }) ); }; // Helper function used in tests exports.listRunningInstances = async () => { console.log(PROJECT_ID, ZONE); return _listRunningInstances(PROJECT_ID, ZONE); }; // export for mocking purposes exports.getInstancesClient = () => { return instancesClient; };