Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .kokoro/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
# limitations under the License.

export GCLOUD_PROJECT=nodejs-docs-samples-tests
export GCP_PROJECT=$GCLOUD_PROJECT
export GOOGLE_CLOUD_PROJECT=$GCLOUD_PROJECT

export GCF_REGION=us-central1
export NODE_ENV=development
export BUCKET_NAME=$GCLOUD_PROJECT
Expand Down Expand Up @@ -47,6 +50,10 @@ export NODEJS_IOT_EC_PUBLIC_KEY=${KOKORO_GFILE_DIR}/ec_public.pem
export NODEJS_IOT_RSA_PRIVATE_KEY=${KOKORO_GFILE_DIR}/rsa_private.pem
export NODEJS_IOT_RSA_PUBLIC_CERT=${KOKORO_GFILE_DIR}/rsa_cert.pem

# Configure Slack variables (for functions/slack sample)
export BOT_ACCESS_TOKEN=${KOKORO_GFILE_DIR}/secrets-slack-bot-access-token.txt
export CHANNEL=${KOKORO_GFILE_DIR}/secrets-slack-channel-id.txt

cd github/nodejs-docs-samples/${PROJECT}

# Install dependencies
Expand Down
25 changes: 25 additions & 0 deletions .kokoro/functions/billing-periodic.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Format: //devtools/kokoro/config/proto/build.proto

# Set the folder in which the tests are run
env_vars: {
key: "PROJECT"
value: "functions/billing"
}

# Set the test command to run (only for functions/billing)
env_vars: {
key: "TEST_CMD"
value: "compute-test"
}

# Configure the docker image for kokoro-trampoline.
env_vars: {
key: "TRAMPOLINE_IMAGE"
value: "gcr.io/cloud-devrel-kokoro-resources/node:8-user"
}

# Tell the trampoline which build file to use.
env_vars: {
key: "TRAMPOLINE_BUILD_FILE"
value: "github/nodejs-docs-samples/.kokoro/functions/functions-billing.sh"
}
8 changes: 4 additions & 4 deletions .kokoro/functions/billing.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ env_vars: {
value: "functions/billing"
}

# Configure the docker image for kokoro-trampoline.
# Set the test command to run (only for functions/billing)
env_vars: {
key: "TRAMPOLINE_IMAGE"
value: "gcr.io/cloud-devrel-kokoro-resources/node:8-user"
key: "TEST_CMD"
value: "test"
}

# Tell the trampoline which build file to use.
env_vars: {
key: "TRAMPOLINE_BUILD_FILE"
value: "github/nodejs-docs-samples/.kokoro/build.sh"
value: "github/nodejs-docs-samples/.kokoro/functions/functions-billing.sh"
}
41 changes: 41 additions & 0 deletions .kokoro/functions/functions-billing.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/bin/bash

# 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.

export GCLOUD_PROJECT=cdpe-functions-billing-test
export GCP_PROJECT=$GCLOUD_PROJECT
export GOOGLE_CLOUD_PROJECT=$GCLOUD_PROJECT
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this env variable being used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's used elsewhere - I was mainly adding it to save us the trouble if future samples start relying on it.


export GCF_REGION=us-central1
export NODE_ENV=development

# Configure Slack variables
export BOT_ACCESS_TOKEN=$(cat ${KOKORO_GFILE_DIR}/secrets-slack-bot-access-token.txt)
export CHANNEL=$(cat ${KOKORO_GFILE_DIR}/secrets-slack-channel-id.txt)
export BILLING_ACCOUNT=$(cat ${KOKORO_GFILE_DIR}/secrets-billing-account-id.txt)

cd github/nodejs-docs-samples/${PROJECT}

# Install dependencies
npm install

# Configure gcloud
export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/secrets-key.json
gcloud auth activate-service-account --key-file "$GOOGLE_APPLICATION_CREDENTIALS"
gcloud config set project $GCLOUD_PROJECT

npm run ${TEST_CMD}

exit $?
121 changes: 104 additions & 17 deletions functions/billing/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2018, Google LLC.
* 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
Expand All @@ -13,6 +13,8 @@
* limitations under the License.
*/

/* eslint-disable no-warning-comments */

// [START functions_billing_limit]
// [START functions_billing_stop]
const {google} = require('googleapis');
Expand All @@ -26,29 +28,33 @@ const PROJECT_NAME = `projects/${PROJECT_ID}`;
// [START functions_billing_slack]
const slack = require('slack');

const BOT_ACCESS_TOKEN = 'xxxx-111111111111-abcdefghidklmnopq';
const CHANNEL = 'general';
// 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 (data, context) => {
const pubsubMessage = data;
const pubsubAttrs = JSON.stringify(pubsubMessage.attributes);
const pubsubData = Buffer.from(pubsubMessage.data, 'base64').toString();
exports.notifySlack = async (pubsubEvent, context) => {
const pubsubAttrs = pubsubEvent.attributes;
const pubsubData = Buffer.from(pubsubEvent.data, 'base64').toString();
const budgetNotificationText = `${pubsubAttrs}, ${pubsubData}`;

const res = await slack.chat.postMessage({
await slack.chat.postMessage({
token: BOT_ACCESS_TOKEN,
channel: CHANNEL,
text: budgetNotificationText,
});
console.log(res);

return 'Slack notification sent successfully';
};
// [END functions_billing_slack]

// [START functions_billing_stop]
const billing = google.cloudbilling('v1').projects;

exports.stopBilling = async (data, context) => {
const pubsubData = JSON.parse(Buffer.from(data.data, 'base64').toString());
exports.stopBilling = async (pubsubEvent, context) => {
const pubsubData = JSON.parse(
Buffer.from(pubsubEvent.data, 'base64').toString()
);
if (pubsubData.costAmount <= pubsubData.budgetAmount) {
return `No action necessary. (Current cost: ${pubsubData.costAmount})`;
}
Expand Down Expand Up @@ -105,19 +111,50 @@ const _disableBillingForProject = async projectName => {
};
// [END functions_billing_stop]

// Helper function to restart billing (used in tests)
exports.startBilling = async (pubsubEvent, context) => {
const pubsubData = JSON.parse(
Buffer.from(pubsubEvent.data, 'base64').toString()
);

await _setAuthCredential();
if (!(await _isBillingEnabled(PROJECT_NAME))) {
// Enable billing

const res = await billing.updateBillingInfo({
name: pubsubData.projectName,
resource: {
billingAccountName: pubsubData.billingAccountName,
billingEnabled: true,
},
});
return `Billing enabled: ${JSON.stringify(res.data)}`;
} else {
return 'Billing already enabled';
}
};

// [START functions_billing_limit]
const compute = google.compute('v1');
const ZONE = 'us-west1-b';
const ZONE = 'us-west1-a';

exports.limitUse = async (data, context) => {
const pubsubData = JSON.parse(Buffer.from(data.data, 'base64').toString());
exports.limitUse = async (pubsubEvent, context) => {
const pubsubData = JSON.parse(
Buffer.from(pubsubEvent.data, 'base64').toString()
);
if (pubsubData.costAmount <= pubsubData.budgetAmount) {
return `No action necessary. (Current cost: ${pubsubData.costAmount})`;
}

await _setAuthCredential();

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.`;
};

/**
Expand All @@ -139,9 +176,6 @@ const _listRunningInstances = async (projectId, zone) => {
* @return {Promise} Response from stopping instances
*/
const _stopInstances = async (projectId, zone, instanceNames) => {
if (!instanceNames.length) {
return 'No running instances were found.';
}
await Promise.all(
instanceNames.map(instanceName => {
return compute.instances
Expand All @@ -158,3 +192,56 @@ const _stopInstances = async (projectId, zone, instanceNames) => {
);
};
// [END functions_billing_limit]

// Helper function to restart instances (used in tests)
exports.startInstances = async (pubsubEvent, context) => {
await _setAuthCredential();
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 res = await compute.instances.list({
project: projectId,
zone: zone,
});

const instances = res.data.items || [];
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 compute.instances.start({
project: projectId,
zone: zone,
instance: instanceName,
});
})
);
};

// Helper function used in tests
exports.listRunningInstances = async (pubsubEvent, context) => {
await _setAuthCredential();
console.log(PROJECT_ID, ZONE);
return _listRunningInstances(PROJECT_ID, ZONE);
};
10 changes: 8 additions & 2 deletions functions/billing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"node": ">=8.0.0"
},
"scripts": {
"test": "mocha test/*.test.js --timeout=20000"
"compute-test": "mocha test/periodic.test.js --timeout=600000",
"test": "mocha test/index.test.js --timeout=5000"
},
"author": "Ace Nassri <[email protected]>",
"license": "Apache-2.0",
Expand All @@ -17,9 +18,14 @@
"slack": "^11.0.1"
},
"devDependencies": {
"@google-cloud/functions-framework": "^1.1.1",
"@google-cloud/nodejs-repo-tools": "^3.3.0",
"child-process-promise": "^2.2.1",
"mocha": "^6.0.0",
"promise-retry": "^1.1.1",
"proxyquire": "^2.1.0",
"sinon": "^7.0.0"
"request": "^2.88.0",
"requestretry": "^4.0.0",
"sinon": "^7.3.2"
}
}
Loading