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 on Google Cloud][run_button_helloworld] | -|[Manual Logging][manual_logging] | Structured logging without client library | [Run on Google Cloud][run_button_manual_logging] | +| Sample | Description | Deploy | +| --------------------------------------- | ------------------------ | ------------- | +|[Hello World][helloworld] ➥ | Quickstart | [Run on Google Cloud][run_button_helloworld] | +|[Manual Logging][manual_logging] | Structured logging without client library | [Run on Google Cloud][run_button_manual_logging] | +|[Pub/Sub][pubsub] | Pub/Sub push Handler | [Run on Google Cloud][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}!`))); + }); + }); +});