diff --git a/.kokoro/build-with-run.sh b/.kokoro/build-with-run.sh
index b8a6716b6e..5b0a01426e 100755
--- a/.kokoro/build-with-run.sh
+++ b/.kokoro/build-with-run.sh
@@ -40,7 +40,7 @@ export CONTAINER_IMAGE="gcr.io/${GOOGLE_CLOUD_PROJECT}/run-${SAMPLE_NAME}:${SAMP
# Register post-test cleanup.
function cleanup {
- gcloud --quiet container images delete "${CONTAINER_IMAGE}"
+ gcloud --quiet container images delete "${CONTAINER_IMAGE}" || true
}
trap cleanup EXIT
@@ -53,6 +53,4 @@ set +x
export NODE_ENV=development
npm install
npm test
-npm run | grep e2e-test && npm run e2e-test
-
-exit $?
+npm run --if-present e2e-test
diff --git a/.kokoro/run/pubsub.cfg b/.kokoro/run/pubsub.cfg
new file mode 100644
index 0000000000..5ba4d7310e
--- /dev/null
+++ b/.kokoro/run/pubsub.cfg
@@ -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: "run/pubsub"
+}
diff --git a/run/README.md b/run/README.md
index 858b65b34d..393ed9c2b8 100644
--- a/run/README.md
+++ b/run/README.md
@@ -6,10 +6,11 @@
## Samples
-| Sample | Description | Deploy |
-| ------------------------------- | ------------------------ | ------------- |
-|[Hello World][helloworld] ➥ | Quickstart | [
][run_button_helloworld] |
-|[Manual Logging][manual_logging] | Structured logging without client library | [
][run_button_manual_logging] |
+| Sample | Description | Deploy |
+| --------------------------------------- | ------------------------ | ------------- |
+|[Hello World][helloworld] ➥ | Quickstart | [
][run_button_helloworld] |
+|[Manual Logging][manual_logging] | Structured logging without client library | [
][run_button_manual_logging] |
+|[Pub/Sub][pubsub] | Pub/Sub push Handler | [
][run_button_pubsub] |
For more Cloud Run samples beyond Node.js, see the main list in the [Cloud Run Samples repository](https://github.com/GoogleCloudPlatform/cloud-run-samples).
@@ -109,5 +110,7 @@ for more information.
[run_deploy]: https://cloud.google.com/run/docs/deploying
[helloworld]: https://github.com/knative/docs/tree/master/docs/serving/samples/hello-world/helloworld-nodejs
[manual_logging]: logging-manual/
+[pubsub]: pubsub/
[run_button_helloworld]: https://console.cloud.google.com/cloudshell/editor?shellonly=true&cloudshell_image=gcr.io/cloudrun/button&cloudshell_git_repo=https://github.com/knative/docs&cloudshell_working_dir=docs/serving/samples/hello-world/helloworld-nodejs
[run_button_manual_logging]: https://console.cloud.google.com/cloudshell/editor?shellonly=true&cloudshell_image=gcr.io/cloudrun/button&cloudshell_git_repo=https://github.com/GoogleCloudPlatform/nodejs-docs-samples&cloudshell_working_dir=run/logging-manual
+[run_button_pubsub]: https://console.cloud.google.com/cloudshell/editor?shellonly=true&cloudshell_image=gcr.io/cloudrun/button&cloudshell_git_repo=https://github.com/GoogleCloudPlatform/nodejs-docs-samples&cloudshell_working_dir=run/pubsub
\ No newline at end of file
diff --git a/run/logging-manual/Dockerfile b/run/logging-manual/Dockerfile
index 12f5f20597..ca7b347297 100644
--- a/run/logging-manual/Dockerfile
+++ b/run/logging-manual/Dockerfile
@@ -14,8 +14,10 @@ WORKDIR /usr/src/app
# Copying this separately prevents re-running npm install on every code change.
COPY package*.json ./
-# Install production dependencies.
-RUN npm install --only=production
+# Install dependencies.
+RUN npm install
+# For production deploys, add a package-lock.json and use 'npm ci'.
+# RUN npm ci --only=production
# Copy local code to the container image.
COPY . .
diff --git a/run/pubsub/.dockerignore b/run/pubsub/.dockerignore
new file mode 100644
index 0000000000..5747c4c87d
--- /dev/null
+++ b/run/pubsub/.dockerignore
@@ -0,0 +1,4 @@
+Dockerfile
+.dockerignore
+node_modules
+npm-debug.log
diff --git a/run/pubsub/.gcloudignore b/run/pubsub/.gcloudignore
new file mode 100644
index 0000000000..26600c93a8
--- /dev/null
+++ b/run/pubsub/.gcloudignore
@@ -0,0 +1,3 @@
+.gcloudignore
+node_modules
+npm-debug.log
diff --git a/run/pubsub/.gitignore b/run/pubsub/.gitignore
new file mode 100644
index 0000000000..3c3629e647
--- /dev/null
+++ b/run/pubsub/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/run/pubsub/Dockerfile b/run/pubsub/Dockerfile
new file mode 100644
index 0000000000..7bc513b5a6
--- /dev/null
+++ b/run/pubsub/Dockerfile
@@ -0,0 +1,30 @@
+# Copyright 2019 Google LLC. All rights reserved.
+# Use of this source code is governed by the Apache 2.0
+# license that can be found in the LICENSE file.
+
+# [START run_pubsub_dockerfile]
+
+# Use the official Node.js 10 image.
+# https://hub.docker.com/_/node
+FROM node:10
+
+# Create and change to the app directory.
+WORKDIR /usr/src/app
+
+# Copy application dependency manifests to the container image.
+# A wildcard is used to ensure both package.json AND package-lock.json are copied.
+# Copying this separately prevents re-running npm install on every code change.
+COPY package*.json ./
+
+# Install dependencies.
+RUN npm install --production
+# If you add a package-lock.json, speed your build by switching to 'npm ci'.
+# RUN npm ci --only=production
+
+# Copy local code to the container image.
+COPY . .
+
+# Run the web service on container startup.
+CMD [ "npm", "start" ]
+
+# [END run_pubsub_dockerfile]
diff --git a/run/pubsub/README.md b/run/pubsub/README.md
new file mode 100644
index 0000000000..ab385ca4cb
--- /dev/null
+++ b/run/pubsub/README.md
@@ -0,0 +1,13 @@
+# Cloud Run Pub/Sub Tutorial Sample
+
+This sample shows how to create a service that processes Pub/Sub messages.
+
+Use it with the [Cloud Pub/Sub with Cloud Run tutorial](http://cloud.google.com/run/docs/tutorials/pubsub).
+
+For more details on how to work with this sample read the [Google Cloud Run Node.js Samples README](https://github.com/GoogleCloudPlatform/nodejs-docs-samples/run).
+
+## Dependencies
+
+* **express**: Web server framework.
+* **body-parser**: express middleware for request payload processing.
+
diff --git a/run/pubsub/app.js b/run/pubsub/app.js
new file mode 100644
index 0000000000..42f7482566
--- /dev/null
+++ b/run/pubsub/app.js
@@ -0,0 +1,40 @@
+// Copyright 2019 Google LLC. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+// [START run_pubsub_server_setup]
+const express = require('express');
+const bodyParser = require('body-parser');
+const app = express();
+
+app.use(bodyParser.json());
+// [END run_pubsub_server_setup]
+
+// [START run_pubsub_handler]
+app.post('/', (req, res) => {
+ if (!req.body) {
+ const msg = 'no Pub/Sub message received';
+ console.error(`error: ${msg}`);
+ res.status(400).send(`Bad Request: ${msg}`);
+ return;
+ }
+ if (!req.body.message) {
+ const msg = 'invalid Pub/Sub message format';
+ console.error(`error: ${msg}`);
+ res.status(400).send(`Bad Request: ${msg}`);
+ return;
+ }
+
+ const pubSubMessage = req.body.message;
+ const name = pubSubMessage.data
+ ? Buffer.from(pubSubMessage.data, 'base64')
+ .toString()
+ .trim()
+ : 'World';
+
+ console.log(`Hello ${name}!`);
+ res.status(204).send();
+});
+// [END run_pubsub_handler]
+
+module.exports = app;
diff --git a/run/pubsub/index.js b/run/pubsub/index.js
new file mode 100644
index 0000000000..e0c24b2b8f
--- /dev/null
+++ b/run/pubsub/index.js
@@ -0,0 +1,12 @@
+// Copyright 2019 Google LLC. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+// [START run_pubsub_server]
+const app = require('./app.js');
+const PORT = process.env.PORT || 8080;
+
+app.listen(PORT, () =>
+ console.log(`nodejs-pubsub-tutorial listening on port ${PORT}`)
+);
+// [END run_pubsub_server]
diff --git a/run/pubsub/package.json b/run/pubsub/package.json
new file mode 100644
index 0000000000..21406a7cd9
--- /dev/null
+++ b/run/pubsub/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "nodejs-pubsub",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Simple Pub/Sub subscriber service sample",
+ "main": "index.js",
+ "author": "Google LLC",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ },
+ "scripts": {
+ "start": "node index.js",
+ "test": "mocha test/*.test.js --check-leaks"
+ },
+ "dependencies": {
+ "body-parser": "^1.19.0",
+ "express": "^4.16.4"
+ },
+ "devDependencies": {
+ "mocha": "^6.1.4",
+ "sinon": "^7.3.2",
+ "supertest": "^4.0.2",
+ "uuid": "^3.3.2"
+ }
+}
diff --git a/run/pubsub/test/app.test.js b/run/pubsub/test/app.test.js
new file mode 100644
index 0000000000..524dc17432
--- /dev/null
+++ b/run/pubsub/test/app.test.js
@@ -0,0 +1,92 @@
+// 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.
+
+// NOTE:
+// This app can only be fully tested when deployed, because
+// Pub/Sub requires a live endpoint URL to hit. Nevertheless,
+// these tests mock it and partially test it locally.
+
+'use strict';
+
+const assert = require('assert');
+const path = require('path');
+const supertest = require('supertest');
+const sinon = require('sinon');
+const uuid = require('uuid');
+
+let request;
+
+describe('Unit Tests', () => {
+ before(() => {
+ const app = require(path.join(__dirname, '..', 'app'));
+ request = supertest(app);
+ });
+
+ describe('should fail', () => {
+ it(`on a Bad Request with an empty payload`, async () => {
+ await request
+ .post('/')
+ .type('json')
+ .send('')
+ .expect(400);
+ });
+
+ it(`on a Bad Request with an invalid payload`, async () => {
+ await request
+ .post('/')
+ .type('json')
+ .send({nomessage: 'invalid'})
+ .expect(400);
+ });
+
+ it(`on a Bad Request with an invalid mimetype`, async () => {
+ await request
+ .post('/')
+ .type('text')
+ .send('{message: true}')
+ .expect(400);
+ });
+ });
+
+ describe('should succeed', () => {
+ beforeEach(() => {
+ sinon.spy(console, 'error');
+ sinon.spy(console, 'log');
+ });
+ afterEach(() => {
+ console.error.restore();
+ console.log.restore();
+ });
+
+ it(`with a minimally valid Pub/Sub Message`, async () => {
+ await request
+ .post('/')
+ .type('json')
+ .send({message: true})
+ .expect(204)
+ .expect(() => assert.ok(console.log.calledWith('Hello World!')));
+ });
+
+ it(`with a populated Pub/Sub Message`, async () => {
+ const name = uuid.v4();
+ const data = Buffer.from(name).toString(`base64`);
+
+ await request
+ .post('/')
+ .type('json')
+ .send({message: {data}})
+ .expect(204)
+ .expect(() => assert.ok(console.log.calledWith(`Hello ${name}!`)));
+ });
+ });
+});