Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloud Task + GAE + Functions Tutorial #1488

Merged
merged 28 commits into from
Oct 1, 2019
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
aad4ff6
add samples and tests
averikitsch Sep 18, 2019
5b0ef75
Fix kokoro typo
averikitsch Sep 18, 2019
5ce9b69
linting
averikitsch Sep 18, 2019
9e7d19e
Merge branch 'master' into task-tutorial
averikitsch Sep 18, 2019
6c3fedc
remove comment
averikitsch Sep 18, 2019
74a300d
Merge branch 'task-tutorial' of https://github.com/GoogleCloudPlatfor…
averikitsch Sep 18, 2019
a90062e
lint
averikitsch Sep 18, 2019
1d22519
Update service account
averikitsch Sep 19, 2019
9bb9e1f
Update app
averikitsch Sep 20, 2019
2c2187b
Merge branch 'master' into task-tutorial
averikitsch Sep 20, 2019
6477d03
Merge branch 'master' into task-tutorial
averikitsch Sep 23, 2019
acf4a1e
Merge branch 'master' into task-tutorial
fhinkel Sep 24, 2019
73d7ab3
update tag and readme
averikitsch Sep 24, 2019
95e7ac4
Merge branch 'task-tutorial' of https://github.com/GoogleCloudPlatfor…
averikitsch Sep 24, 2019
f0eb573
remove version
averikitsch Sep 25, 2019
b5e103a
Simplify readme
averikitsch Sep 25, 2019
176961b
Update param comments
averikitsch Sep 25, 2019
cadeddd
clean up
averikitsch Sep 25, 2019
efee1ab
Merge branch 'master' into task-tutorial
averikitsch Sep 25, 2019
cfbc82b
remove kokoro common file duplicate
averikitsch Sep 30, 2019
fe94dbd
Update comments and tests
averikitsch Sep 30, 2019
4202798
Merge branch 'task-tutorial' of https://github.com/GoogleCloudPlatfor…
averikitsch Sep 30, 2019
be4588e
Add region tag
averikitsch Sep 30, 2019
3446705
g/postcard/email/
averikitsch Sep 30, 2019
a0b74b1
Merge branch 'master' into task-tutorial
averikitsch Oct 1, 2019
4e8f7ea
re-add common file
averikitsch Oct 1, 2019
5400912
Merge branch 'task-tutorial' of https://github.com/GoogleCloudPlatfor…
averikitsch Oct 1, 2019
3d0f6af
lint
averikitsch Oct 1, 2019
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/cloud-tasks/cloud-tasks-app.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Format: //devtools/kokoro/config/proto/build.proto

# Set the folder in which the tests are run
env_vars: {
key: "PROJECT"
value: "cloud-tasks/app"
}
7 changes: 7 additions & 0 deletions .kokoro/cloud-tasks/cloud-tasks-func.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Format: //devtools/kokoro/config/proto/build.proto

# Set the folder in which the tests are run
env_vars: {
key: "PROJECT"
value: "cloud-tasks/function"
}
22 changes: 22 additions & 0 deletions .kokoro/cloud-tasks/common.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Format: //devtools/kokoro/config/proto/build.proto

# Download trampoline resources. These will be in ${KOKORO_GFILE_DIR}
gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"

# Download secrets from Cloud Storage.
gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/nodejs-docs-samples"

# All builds use the trampoline script to run in docker.
build_file: "nodejs-docs-samples/.kokoro/trampoline.sh"

# Tell the trampoline which build file to use.
env_vars: {
key: "TRAMPOLINE_BUILD_FILE"
value: "github/nodejs-docs-samples/.kokoro/build.sh"
}

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

Choose a reason for hiding this comment

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

Why does cloud-tasks need a customized common.cfg? So far most products don't need one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Every folder in .kokoro has a common.cfg file, but you are right it is a duplication of the parents.

22 changes: 22 additions & 0 deletions cloud-tasks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# [Node.js Cloud Tasks sample for Google App Engine and Cloud Functions][tutorial-link]

This is the sample application for the
[Using Cloud Tasks to trigger Cloud Functions][tutorial-link] tutorial.

This tutorial shows how to create [Cloud Tasks][cloud-tasks] on
[Google App Engine Standard][gae-std] to trigger a [Cloud Function][cloud-func]
in order to send a postcard email.

## Application Architecture

* The App Engine application calls the Cloud Tasks API to add a scheduled task
to the queue.

* The queue processes tasks and sends requests to a Cloud Function.

* The Cloud Function calls the SendGrid API to send a postcard email.

[tutorial-link]: https://cloud.google.com/tasks/docs/tutorial-gcf
[cloud-tasks]: https://cloud.google.com/tasks/docs/
[gae-std]: https://cloud.google.com/appengine/docs/standard/nodejs/
[cloud-func]: https://cloud.google.com/functions/
30 changes: 30 additions & 0 deletions cloud-tasks/app/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 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
#
# https://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.

runtime: nodejs10

# [START cloud_tasks_app_env_vars]
env_variables:
QUEUE_NAME: "my-queue"
QUEUE_LOCATION: "us-central1"
FUNCTION_URL: "https://<region>-<project_id>.cloudfunctions.net/sendPostcard"
SERVICE_ACCOUNT_EMAIL: "<member>@<project_id>.iam.gserviceaccount.com"
# [END cloud_tasks_app_env_vars]

handlers:
- url: /
static_files: static/index.html
upload: static/index.html
- url: /static
static_dir: static
averikitsch marked this conversation as resolved.
Show resolved Hide resolved
90 changes: 90 additions & 0 deletions cloud-tasks/app/createTask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* 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.
*/

'use strict';

// [START cloud_tasks_app_create_task]
const MAX_SCHEDULE_LIMIT = 30 * 60 * 60 * 24; // Represents 30 days in seconds.

async function createHttpTaskWithToken(
project = 'my-project-id', // Your GCP Project id
queue = 'my-queue', // Name of your Queue
location = 'us-central1', // The GCP region of your queue
url = 'https://example.com/taskhandler', // The full url path that the request will be sent to
email = '<member>@<project-id>.iam.gserviceaccount.com', // Cloud IAM service account
payload = 'Hello, World!', // The task HTTP request body
date = new Date() // Intended date to schedule task
) {
// Imports the Google Cloud Tasks library.
const {v2beta3} = require('@google-cloud/tasks');

// Instantiates a client.
const client = new v2beta3.CloudTasksClient();

// Construct the fully qualified queue name.
const parent = client.queuePath(project, location, queue);

// Convert message to buffer
const convertedPayload = JSON.stringify(payload);
const body = Buffer.from(convertedPayload).toString('base64');

const task = {
httpRequest: {
httpMethod: 'POST',
url,
oidcToken: {
serviceAccountEmail: email,
},
headers: {
'Content-Type': 'application/json',
},
body,
},
};

const convertedDate = new Date(date);
const currentDate = new Date();

// Schedule time can not be in the past
if (convertedDate < currentDate) {
console.error('Scheduled date in the past.');
} else if (convertedDate > currentDate) {
// Restrict schedule time to the 30 day maximum
const date_diff_in_seconds = (convertedDate - currentDate) / 1000;
if (date_diff_in_seconds > MAX_SCHEDULE_LIMIT) {
console.error('Schedule time is over 30 day maximum.');
}
const date_in_seconds =
Math.min(date_diff_in_seconds, MAX_SCHEDULE_LIMIT) + Date.now() / 1000;
// Add schedule time to request in Timestamp format
// https://googleapis.dev/nodejs/tasks/latest/google.protobuf.html#.Timestamp
task.scheduleTime = {
seconds: date_in_seconds,
averikitsch marked this conversation as resolved.
Show resolved Hide resolved
};
}

try {
// Send create task request.
const [response] = await client.createTask({parent, task});
console.log(`Created task ${response.name}`);
return response.name;
} catch (error) {
// Construct error for Stackdriver Error Reporting
console.error(Error(error.message));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Non-Blocking.

Did you end up confirming that console.error(error) does not work?

It would be worth comparing the output from the two commands and seeing what the difference is, it might be fixable.

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 did run some more isolated tests and console.error(error) did not show up in Error Reporting.

}
}

module.exports = createHttpTaskWithToken;
// [END cloud_tasks_app_create_task]
58 changes: 58 additions & 0 deletions cloud-tasks/app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* 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.
*/

'use strict';

const createHttpTaskWithToken = require('./createTask');
const express = require('express');

const app = express();

const {QUEUE_NAME} = process.env;
const {QUEUE_LOCATION} = process.env;
const {FUNCTION_URL} = process.env;
const {SERVICE_ACCOUNT_EMAIL} = process.env;

app.use(express.urlencoded({extended: true}));
averikitsch marked this conversation as resolved.
Show resolved Hide resolved

// [START cloud_tasks_app]
app.post('/send-email', (req, res) => {
// Set the task payload to the form submission.
const {to_name, from_name, to_email, date} = req.body;
const payload = {to_name, from_name, to_email};

createHttpTaskWithToken(
process.env.GOOGLE_CLOUD_PROJECT,
QUEUE_NAME,
QUEUE_LOCATION,
FUNCTION_URL,
SERVICE_ACCOUNT_EMAIL,
payload,
date
);

res.status(202).send('📫 Your postcard is in the mail! 💌');
});
// [END cloud_tasks_app]

app.get('*', (req, res) => {
res.send('OK').end();
});
averikitsch marked this conversation as resolved.
Show resolved Hide resolved

const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
console.log('Press Ctrl+C to quit.');
});
18 changes: 18 additions & 0 deletions cloud-tasks/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "cloud-tasks-app",
"description": "Creates Cloud Tasks from form submission",
"main": "index.js",
"private": true,
"scripts": {
"start": "node index.js",
"test": "mocha"
},
"dependencies": {
"@google-cloud/tasks": "^1.2.1",
"express": "^4.17.1"
},
"devDependencies": {
"chai": "^4.2.0",
"mocha": "^6.1.3"
}
}
84 changes: 84 additions & 0 deletions cloud-tasks/app/static/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
.postcard {
margin: auto;
text-align: center;
position: relative;
width: 600px;
height: 400px;
background: #4285F4;
box-shadow: 0px 7px 20px 0px rgba(0, 0, 0, 0.5);
}

.postcard::after {
content: '';
position: absolute;
left: 30px;
top: 30px;
bottom: 30px;
right: 30px;
border: 5px solid #FFF;
}

.postcard-text {
font-family: Arial, sans-serif;
font-size: 60px;
font-weight: bold;
text-transform: uppercase;
color: #FFF;
background: #4285F4;
position: absolute;
left: 0;
padding-left: 27px;
bottom: 0;
padding-bottom: 15px;
z-index: 2;
}

.postcard-names {
font-family: Monaco, monospace;
font-size: 40px;
text-align: right;
color: #FFF;
background: #4285F4;
position: absolute;
right: 0;
top: 0;
padding: 15px 27px;
z-index: 2;
max-width: 546px;
max-height: 300px;
overflow: hidden;
}

.info {
margin: auto;
text-align: center;
position: relative;
margin-top: 3em;
font-family: Arial, sans-serif;
}

.info-line {
margin: 1em;
}

.postcard-names input {
font-family: Monaco, monospace;
font-size: 36px;
color: #FFF;
background: #4285F4;
border-bottom: 3px solid #FFF;
width: 200px;
}

.info-line input, label {
font-family: Monaco, monospace;
font-size: 24px;
}

button {
font-family: Monaco, monospace;
font-size: 24px;
color: #FFF;
text-transform: uppercase;
background: #4285F4;
}
Loading