Skip to content

Commit 9040e72

Browse files
Ace Nassrifhinkel
authored andcommitted
functions/billing: Node 8 + system tests (#1361)
1 parent ad43645 commit 9040e72

File tree

8 files changed

+410
-150
lines changed

8 files changed

+410
-150
lines changed

.kokoro/build.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
# limitations under the License.
1616

1717
export GCLOUD_PROJECT=nodejs-docs-samples-tests
18+
export GCP_PROJECT=$GCLOUD_PROJECT
19+
export GOOGLE_CLOUD_PROJECT=$GCLOUD_PROJECT
20+
1821
export GCF_REGION=us-central1
1922
export NODE_ENV=development
2023
export BUCKET_NAME=$GCLOUD_PROJECT
@@ -47,6 +50,10 @@ export NODEJS_IOT_EC_PUBLIC_KEY=${KOKORO_GFILE_DIR}/ec_public.pem
4750
export NODEJS_IOT_RSA_PRIVATE_KEY=${KOKORO_GFILE_DIR}/rsa_private.pem
4851
export NODEJS_IOT_RSA_PUBLIC_CERT=${KOKORO_GFILE_DIR}/rsa_cert.pem
4952

53+
# Configure Slack variables (for functions/slack sample)
54+
export BOT_ACCESS_TOKEN=${KOKORO_GFILE_DIR}/secrets-slack-bot-access-token.txt
55+
export CHANNEL=${KOKORO_GFILE_DIR}/secrets-slack-channel-id.txt
56+
5057
cd github/nodejs-docs-samples/${PROJECT}
5158

5259
# Install dependencies
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Format: //devtools/kokoro/config/proto/build.proto
2+
3+
# Set the folder in which the tests are run
4+
env_vars: {
5+
key: "PROJECT"
6+
value: "functions/billing"
7+
}
8+
9+
# Set the test command to run (only for functions/billing)
10+
env_vars: {
11+
key: "TEST_CMD"
12+
value: "compute-test"
13+
}
14+
15+
# Configure the docker image for kokoro-trampoline.
16+
env_vars: {
17+
key: "TRAMPOLINE_IMAGE"
18+
value: "gcr.io/cloud-devrel-kokoro-resources/node:8-user"
19+
}
20+
21+
# Tell the trampoline which build file to use.
22+
env_vars: {
23+
key: "TRAMPOLINE_BUILD_FILE"
24+
value: "github/nodejs-docs-samples/.kokoro/functions/functions-billing.sh"
25+
}

.kokoro/functions/billing.cfg

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ env_vars: {
66
value: "functions/billing"
77
}
88

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

1515
# Tell the trampoline which build file to use.
1616
env_vars: {
1717
key: "TRAMPOLINE_BUILD_FILE"
18-
value: "github/nodejs-docs-samples/.kokoro/build.sh"
18+
value: "github/nodejs-docs-samples/.kokoro/functions/functions-billing.sh"
1919
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/bash
2+
3+
# Copyright 2019, Google LLC.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
export GCLOUD_PROJECT=cdpe-functions-billing-test
18+
export GCP_PROJECT=$GCLOUD_PROJECT
19+
export GOOGLE_CLOUD_PROJECT=$GCLOUD_PROJECT
20+
21+
export GCF_REGION=us-central1
22+
export NODE_ENV=development
23+
24+
# Configure Slack variables
25+
export BOT_ACCESS_TOKEN=$(cat ${KOKORO_GFILE_DIR}/secrets-slack-bot-access-token.txt)
26+
export CHANNEL=$(cat ${KOKORO_GFILE_DIR}/secrets-slack-channel-id.txt)
27+
export BILLING_ACCOUNT=$(cat ${KOKORO_GFILE_DIR}/secrets-billing-account-id.txt)
28+
29+
cd github/nodejs-docs-samples/${PROJECT}
30+
31+
# Install dependencies
32+
npm install
33+
34+
# Configure gcloud
35+
export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/secrets-key.json
36+
gcloud auth activate-service-account --key-file "$GOOGLE_APPLICATION_CREDENTIALS"
37+
gcloud config set project $GCLOUD_PROJECT
38+
39+
npm run ${TEST_CMD}
40+
41+
exit $?

functions/billing/index.js

Lines changed: 104 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2018, Google LLC.
2+
* Copyright 2019, Google LLC.
33
* Licensed under the Apache License, Version 2.0 (the "License");
44
* you may not use this file except in compliance with the License.
55
* You may obtain a copy of the License at
@@ -13,6 +13,8 @@
1313
* limitations under the License.
1414
*/
1515

16+
/* eslint-disable no-warning-comments */
17+
1618
// [START functions_billing_limit]
1719
// [START functions_billing_stop]
1820
const {google} = require('googleapis');
@@ -26,29 +28,33 @@ const PROJECT_NAME = `projects/${PROJECT_ID}`;
2628
// [START functions_billing_slack]
2729
const slack = require('slack');
2830

29-
const BOT_ACCESS_TOKEN = 'xxxx-111111111111-abcdefghidklmnopq';
30-
const CHANNEL = 'general';
31+
// TODO(developer) replace these with your own values
32+
const BOT_ACCESS_TOKEN =
33+
process.env.BOT_ACCESS_TOKEN || 'xxxx-111111111111-abcdefghidklmnopq';
34+
const CHANNEL = process.env.SLACK_CHANNEL || 'general';
3135

32-
exports.notifySlack = async (data, context) => {
33-
const pubsubMessage = data;
34-
const pubsubAttrs = JSON.stringify(pubsubMessage.attributes);
35-
const pubsubData = Buffer.from(pubsubMessage.data, 'base64').toString();
36+
exports.notifySlack = async (pubsubEvent, context) => {
37+
const pubsubAttrs = pubsubEvent.attributes;
38+
const pubsubData = Buffer.from(pubsubEvent.data, 'base64').toString();
3639
const budgetNotificationText = `${pubsubAttrs}, ${pubsubData}`;
3740

38-
const res = await slack.chat.postMessage({
41+
await slack.chat.postMessage({
3942
token: BOT_ACCESS_TOKEN,
4043
channel: CHANNEL,
4144
text: budgetNotificationText,
4245
});
43-
console.log(res);
46+
47+
return 'Slack notification sent successfully';
4448
};
4549
// [END functions_billing_slack]
4650

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

50-
exports.stopBilling = async (data, context) => {
51-
const pubsubData = JSON.parse(Buffer.from(data.data, 'base64').toString());
54+
exports.stopBilling = async (pubsubEvent, context) => {
55+
const pubsubData = JSON.parse(
56+
Buffer.from(pubsubEvent.data, 'base64').toString()
57+
);
5258
if (pubsubData.costAmount <= pubsubData.budgetAmount) {
5359
return `No action necessary. (Current cost: ${pubsubData.costAmount})`;
5460
}
@@ -105,19 +111,50 @@ const _disableBillingForProject = async projectName => {
105111
};
106112
// [END functions_billing_stop]
107113

114+
// Helper function to restart billing (used in tests)
115+
exports.startBilling = async (pubsubEvent, context) => {
116+
const pubsubData = JSON.parse(
117+
Buffer.from(pubsubEvent.data, 'base64').toString()
118+
);
119+
120+
await _setAuthCredential();
121+
if (!(await _isBillingEnabled(PROJECT_NAME))) {
122+
// Enable billing
123+
124+
const res = await billing.updateBillingInfo({
125+
name: pubsubData.projectName,
126+
resource: {
127+
billingAccountName: pubsubData.billingAccountName,
128+
billingEnabled: true,
129+
},
130+
});
131+
return `Billing enabled: ${JSON.stringify(res.data)}`;
132+
} else {
133+
return 'Billing already enabled';
134+
}
135+
};
136+
108137
// [START functions_billing_limit]
109138
const compute = google.compute('v1');
110-
const ZONE = 'us-west1-b';
139+
const ZONE = 'us-west1-a';
111140

112-
exports.limitUse = async (data, context) => {
113-
const pubsubData = JSON.parse(Buffer.from(data.data, 'base64').toString());
141+
exports.limitUse = async (pubsubEvent, context) => {
142+
const pubsubData = JSON.parse(
143+
Buffer.from(pubsubEvent.data, 'base64').toString()
144+
);
114145
if (pubsubData.costAmount <= pubsubData.budgetAmount) {
115146
return `No action necessary. (Current cost: ${pubsubData.costAmount})`;
116147
}
117148

118149
await _setAuthCredential();
150+
119151
const instanceNames = await _listRunningInstances(PROJECT_ID, ZONE);
152+
if (!instanceNames.length) {
153+
return 'No running instances were found.';
154+
}
155+
120156
await _stopInstances(PROJECT_ID, ZONE, instanceNames);
157+
return `${instanceNames.length} instance(s) stopped successfully.`;
121158
};
122159

123160
/**
@@ -139,9 +176,6 @@ const _listRunningInstances = async (projectId, zone) => {
139176
* @return {Promise} Response from stopping instances
140177
*/
141178
const _stopInstances = async (projectId, zone, instanceNames) => {
142-
if (!instanceNames.length) {
143-
return 'No running instances were found.';
144-
}
145179
await Promise.all(
146180
instanceNames.map(instanceName => {
147181
return compute.instances
@@ -158,3 +192,56 @@ const _stopInstances = async (projectId, zone, instanceNames) => {
158192
);
159193
};
160194
// [END functions_billing_limit]
195+
196+
// Helper function to restart instances (used in tests)
197+
exports.startInstances = async (pubsubEvent, context) => {
198+
await _setAuthCredential();
199+
const instanceNames = await _listStoppedInstances(PROJECT_ID, ZONE);
200+
201+
if (!instanceNames.length) {
202+
return 'No stopped instances were found.';
203+
}
204+
205+
await _startInstances(PROJECT_ID, ZONE, instanceNames);
206+
return `${instanceNames.length} instance(s) started successfully.`;
207+
};
208+
209+
/**
210+
* @return {Promise} Array of names of running instances
211+
*/
212+
const _listStoppedInstances = async (projectId, zone) => {
213+
const res = await compute.instances.list({
214+
project: projectId,
215+
zone: zone,
216+
});
217+
218+
const instances = res.data.items || [];
219+
const stoppedInstances = instances.filter(item => item.status !== 'RUNNING');
220+
return stoppedInstances.map(item => item.name);
221+
};
222+
223+
/**
224+
* @param {Array} instanceNames Names of instance to stop
225+
* @return {Promise} Response from stopping instances
226+
*/
227+
const _startInstances = async (projectId, zone, instanceNames) => {
228+
if (!instanceNames.length) {
229+
return 'No stopped instances were found.';
230+
}
231+
await Promise.all(
232+
instanceNames.map(instanceName => {
233+
return compute.instances.start({
234+
project: projectId,
235+
zone: zone,
236+
instance: instanceName,
237+
});
238+
})
239+
);
240+
};
241+
242+
// Helper function used in tests
243+
exports.listRunningInstances = async (pubsubEvent, context) => {
244+
await _setAuthCredential();
245+
console.log(PROJECT_ID, ZONE);
246+
return _listRunningInstances(PROJECT_ID, ZONE);
247+
};

functions/billing/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"node": ">=8.0.0"
88
},
99
"scripts": {
10-
"test": "mocha test/*.test.js --timeout=20000"
10+
"compute-test": "mocha test/periodic.test.js --timeout=600000",
11+
"test": "mocha test/index.test.js --timeout=5000"
1112
},
1213
"author": "Ace Nassri <anassri@google.com>",
1314
"license": "Apache-2.0",
@@ -17,9 +18,14 @@
1718
"slack": "^11.0.1"
1819
},
1920
"devDependencies": {
21+
"@google-cloud/functions-framework": "^1.1.1",
2022
"@google-cloud/nodejs-repo-tools": "^3.3.0",
23+
"child-process-promise": "^2.2.1",
2124
"mocha": "^6.0.0",
25+
"promise-retry": "^1.1.1",
2226
"proxyquire": "^2.1.0",
23-
"sinon": "^7.0.0"
27+
"request": "^2.88.0",
28+
"requestretry": "^4.0.0",
29+
"sinon": "^7.3.2"
2430
}
2531
}

0 commit comments

Comments
 (0)