From fb380c9be17925a08cc1409429f395bf29804d90 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Fri, 13 Sep 2019 14:11:38 -0700 Subject: [PATCH 1/9] run: add hello-broken sample --- run/README.md | 17 ++++--- run/hello-broken/Dockerfile | 30 ++++++++++++ run/hello-broken/README.md | 69 ++++++++++++++++++++++++++++ run/hello-broken/index.js | 46 +++++++++++++++++++ run/hello-broken/package.json | 23 ++++++++++ run/hello-broken/test/deploy.sh | 36 +++++++++++++++ run/hello-broken/test/runner.sh | 54 ++++++++++++++++++++++ run/hello-broken/test/system.test.js | 69 ++++++++++++++++++++++++++++ run/hello-broken/test/url.sh | 30 ++++++++++++ 9 files changed, 367 insertions(+), 7 deletions(-) create mode 100644 run/hello-broken/Dockerfile create mode 100644 run/hello-broken/README.md create mode 100644 run/hello-broken/index.js create mode 100644 run/hello-broken/package.json create mode 100755 run/hello-broken/test/deploy.sh create mode 100755 run/hello-broken/test/runner.sh create mode 100644 run/hello-broken/test/system.test.js create mode 100755 run/hello-broken/test/url.sh diff --git a/run/README.md b/run/README.md index 4e24c4c9e4..222a95fe70 100644 --- a/run/README.md +++ b/run/README.md @@ -8,10 +8,11 @@ | Sample | Description | Deploy | | --------------------------------------- | ------------------------ | ------------- | -|[Hello World][helloworld] ➥ | Quickstart | [Run on Google Cloud][run_button_helloworld] | -| [Image Processing][image_processing] | Event-driven image analysis & transformation | [Run on Google Cloud][run_button_image_processing] | -|[Manual Logging][manual_logging] | Structured logging without client library | [Run on Google Cloud][run_button_manual_logging] | -|[Pub/Sub][pubsub] | Event-driven service with a Pub/Sub push subscription | [Run on Google Cloud][run_button_pubsub] | +|[Hello World][helloworld] ➥ | Quickstart | [Run on Google Cloud][run_button_helloworld] | +|[Hello Broken][hello_broken] | Something is wrong, how do you fix it? | [Run on Google Cloud][run_button_hello_broken] | +|[Pub/Sub][pubsub] | Event-driven service with a Pub/Sub push subscription | [Run on Google Cloud][run_button_pubsub] | +|[Image Processing][image_processing] | Event-driven image analysis & transformation | [Run on Google Cloud][run_button_image_processing] | +|[Manual Logging][manual_logging] | Structured logging without client library | [Run on Google Cloud][run_button_manual_logging] | 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). @@ -99,7 +100,7 @@ For more Cloud Run samples beyond Node.js, see the main list in the [Cloud Run S gcloud builds submit --tag gcr.io/${GOOGLE_CLOUD_PROJECT}/${SAMPLE} gcloud beta run deploy $SAMPLE \ # Needed for Manual Logging sample. - --set-env-var GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT} + --set-env-var GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT} \ --image gcr.io/${GOOGLE_CLOUD_PROJECT}/${SAMPLE} ``` @@ -110,10 +111,12 @@ for more information. [run_build]: https://cloud.google.com/run/docs/building/containers [run_deploy]: https://cloud.google.com/run/docs/deploying [helloworld]: https://github.com/knative/docs/tree/master/docs/serving/samples/hello-world/helloworld-nodejs +[hello_broken]: hello-broken/ +[pubsub]: pubsub/ [image_processing]: image-processing/ [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_hello_broken]: 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/hello-broken +[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 [run_button_image_processing]: 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/image-processing [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/hello-broken/Dockerfile b/run/hello-broken/Dockerfile new file mode 100644 index 0000000000..dedd8deb5a --- /dev/null +++ b/run/hello-broken/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_broken_dockerfile] + +# Use the official lightweight Node.js 10 image. +# https://hub.docker.com/_/node +FROM node:10-slim + +# 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 copying both package.json AND package-lock.json (when available). +# Copying this first prevents re-running npm install on every code change. +COPY package*.json ./ + +# Install production dependencies. +# If you add a package-lock.json, speed your build by switching to 'npm ci'. +# RUN npm ci --only=production +RUN npm install --only=production + +# Copy local code to the container image. +COPY . ./ + +# Run the web service on container startup. +CMD [ "npm", "start" ] + +# [END run_broken_dockerfile] diff --git a/run/hello-broken/README.md b/run/hello-broken/README.md new file mode 100644 index 0000000000..598e75ae3c --- /dev/null +++ b/run/hello-broken/README.md @@ -0,0 +1,69 @@ +# Cloud Run Broken Sample + +This sample presents broken code in need of troubleshooting. An alternate +resource at `/improved` shows a more stable implementation with more informative +errors and default values. + +Use it with the [Local Container Troubleshooting tutorial](http://cloud.google.com/run/docs/tutorials/local-troubleshooting). + +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). + +## Local Development + +### `npm run e2e-test` + +``` +export SERVICE_NAME=broken +export CONTAINER_IMAGE=gcr.io/${GOOGLE_CLOUD_PROJECT}/broken +npm run e2e-test +``` + +## Using Testing Scripts + +### url.sh + +The `url.sh` script derives the automatically provisioned URL of a deployed +Cloud Run service. + +```sh +export SERVICE_NAME=broken +export REGION=us-central1 +test/url.sh +``` + +### deploy.sh + +The `deploy.sh` script deploys a Cloud Run service. + +```sh +export SERVICE_NAME=broken +export CONTAINER_IMAGE=gcr.io/${GOOGLE_CLOUD_PROJECT}/broken +export REGION=us-central1 +test/deploy.sh +``` + +### runner.sh + +The `runner.sh` script is a helper that facilitates: + +* Deploy the service to Cloud Run based on the `deploy.sh` script. +* Set the `BASE_URL` and `ID_TOKEN` environment variables. +* Run any arguments passed to the `runner.sh` script. +* Tear down the Cloud Run service on completion. + +```sh +test/runner.sh sleep 20 +``` + +## Environment Variables (Testing) + +* `BASE_URL`: Used to designate the URL of the Cloud Run service under system test. The URL is not deterministic, so the options are to record it at deploy time or use the Cloud Run API to retrieve the value. +* `ID_TOKEN`: Used for authenticated HTTP requests to the Cloud Run service. +* `REGION`: [`us-central1`] Optional override region for the location of the Cloud Run service. +* `SERVICE_NAME`: Used in testing to specify the name of the service. The name may be included in API calls and test conditions. +* `GOOGLE_CLOUD_PROJECT`: Used in production as an override to determination of the GCP Project from the GCP metadata server. Used in testing to configure the Logging client library. + +## Dependencies + +* **express**: Web server framework. +* **got**: [Testing] Used to make HTTP requests of the running service in end-to-end testing. diff --git a/run/hello-broken/index.js b/run/hello-broken/index.js new file mode 100644 index 0000000000..09bf1baaf5 --- /dev/null +++ b/run/hello-broken/index.js @@ -0,0 +1,46 @@ +// 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_broken_service] +const express = require('express'); +const app = express(); + +app.get('/', (req, res) => { + console.log('hello-broken: received request.'); + + // [START run_broken_service_problem] + const {TARGET} = process.env; + if (!TARGET) { + // Plain error logs do not appear in Stackdriver Error Reporting. + console.error('Environment validation failed.'); + console.error(new Error('Missing required server parameter')); + res.status(500).send('Internal Server Error'); + return; + } + // [END run_broken_service_problem] + res.send(`Hello ${TARGET}!`); +}); +// [END run_broken_service] + +app.get('/improved', (req, res) => { + console.log('hello-broken: received request.'); + + // [START run_broken_service_upgrade] + const TARGET = process.env.TARGET || 'World'; + if (!process.env.TARGET) { + console.log(JSON.stringify({ + severity: 'WARNING', + message: `TARGET not set, default to '${TARGET}'` + })); + } + // [END run_broken_service_upgrade] + res.send(`Hello ${TARGET}!`); +}); + +// [START run_broken_service] +const port = process.env.PORT || 8080; +app.listen(port, () => { + console.log(`hello-broken: listening on port ${port}`); +}); +// [END run_broken_service] diff --git a/run/hello-broken/package.json b/run/hello-broken/package.json new file mode 100644 index 0000000000..72ab404d96 --- /dev/null +++ b/run/hello-broken/package.json @@ -0,0 +1,23 @@ +{ + "name": "hello-broken", + "private": true, + "description": "Broken Cloud Run service for troubleshooting practice", + "main": "index.js", + "scripts": { + "start": "node index.js", + "test": "echo \"Error: no test specified\" && exit 0", + "e2e-test": "TARGET=Cloud test/runner.sh mocha test/system.test.js --timeout=20000" + }, + "author": "Google LLC", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "dependencies": { + "express": "^4.17.1" + }, + "devDependencies": { + "got": "^9.6.0" + } +} diff --git a/run/hello-broken/test/deploy.sh b/run/hello-broken/test/deploy.sh new file mode 100755 index 0000000000..630ac5d386 --- /dev/null +++ b/run/hello-broken/test/deploy.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env 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. + +set -eo pipefail; + +requireEnv() { + test "${!1}" || (echo "Environment Variable '$1' not found" && exit 1) +} + +requireEnv SERVICE_NAME +requireEnv CONTAINER_IMAGE + +# Deploy the service +set -x +gcloud beta --quiet run deploy "${SERVICE_NAME}" \ + --image="${CONTAINER_IMAGE}" \ + --region="${REGION:-us-central1}" \ + ${FLAGS} \ + --platform=managed + +echo 'Cloud Run Links:' +echo "- Logs: https://console.cloud.google.com/logs/viewer?project=${GOOGLE_CLOUD_PROJECT}&resource=cloud_run_revision%2Fservice_name%2F${SERVICE_NAME}" +echo "- Console: https://console.cloud.google.com/run/detail/${REGION:-us-central1}/${SERVICE_NAME}/metrics?project=${GOOGLE_CLOUD_PROJECT}" diff --git a/run/hello-broken/test/runner.sh b/run/hello-broken/test/runner.sh new file mode 100755 index 0000000000..7059233ecd --- /dev/null +++ b/run/hello-broken/test/runner.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env 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. + +set -eo pipefail; + +requireEnv() { + test "${!1}" || (echo "Environment Variable '$1' not found" && exit 1) +} +requireEnv SERVICE_NAME + +# The hello-broken sample needs to be tested with the TARGET environment variable +# both set and unset. +SERVICE_OVERRIDE="${SERVICE_NAME}-override" + +echo '---' +test/deploy.sh +FLAGS="--set-env-vars TARGET=$TARGET" SERVICE_NAME=${SERVICE_OVERRIDE} test/deploy.sh + +echo +echo '---' +echo + +# Register post-test cleanup. +# Only needed if deploy completed. +function cleanup { + set -x + gcloud --quiet beta run services delete ${SERVICE_NAME} \ + --platform=managed \ + --region="${REGION:-us-central1}" + gcloud --quiet beta run services delete ${SERVICE_OVERRIDE} \ + --platform=managed \ + --region="${REGION:-us-central1}" +} +trap cleanup EXIT + +# TODO: Perform authentication inside the test. +export ID_TOKEN=$(gcloud auth print-identity-token) +export BASE_URL=$(test/url.sh) +export BASE_URL_OVERRIDE=$(SERVICE_NAME=${SERVICE_OVERRIDE} test/url.sh) +# Do not use exec to preserve trap behavior. +"$@" diff --git a/run/hello-broken/test/system.test.js b/run/hello-broken/test/system.test.js new file mode 100644 index 0000000000..b7326a047d --- /dev/null +++ b/run/hello-broken/test/system.test.js @@ -0,0 +1,69 @@ +// 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. + +const assert = require('assert'); +const request = require('got'); + +const get = (route, base_url) => { + const {ID_TOKEN} = process.env; + if (!ID_TOKEN) { + throw Error('"ID_TOKEN" environment variable is required.'); + } + + return request(route, { + baseUrl: base_url.trim(), + headers: { + Authorization: `Bearer ${ID_TOKEN.trim()}`, + }, + throwHttpErrors: false + }); +} + +describe('Default Service', () => { + const {BASE_URL} = process.env; + if (!BASE_URL) { + throw Error( + '"BASE_URL" environment variable is required. For example: https://service-x8xabcdefg-uc.a.run.app' + ); + } + + it('Broken resource fails on any request', async () => { + const response = await get('/', BASE_URL); + assert.strictEqual(response.statusCode, 500, 'Internal service error not found'); + }); + + it('Fixed resource successfully falls back to a default value', async () => { + const response = await get('/improved', BASE_URL); + assert.strictEqual(response.statusCode, 200, 'Did not fallback to default as expected'); + assert.strictEqual(response.body, `Hello ${TARGET}!`, `Expected fallback "World" not found`); + }); +}); + +describe('Overridden Service', () => { + const {BASE_URL_OVERRIDE} = process.env; + if (!BASE_URL_OVERRIDE) { + throw Error( + '"BASE_URL_OVERRIDE" environment variable is required. For example: https://service-x8xabcdefg-uc.a.run.app' + ); + } + + const {TARGET} = process.env; + if (!TARGET) { + throw Error( + '"TARGET" environment variable is required. For example: Cosmos' + ); + } + + it('Broken resource uses the TARGET override', async () => { + const response = await get('/', BASE_URL); + assert.strictEqual(response.statusCode, 200, 'Did not use the TARGET override'); + assert.strictEqual(response.body, `Hello ${TARGET}!`, `Expected override "${TARGET}" not found`); + }); + + it('Fixed resource uses the TARGET override', async () => { + const response = await get('/improved', BASE_URL_OVERRIDE); + assert.strictEqual(response.statusCode, 200, 'Did not fallback to default as expected'); + assert.strictEqual(response.body, `Hello ${TARGET}!`, `Expected override "${TARGET}" not found`); + }); +}); diff --git a/run/hello-broken/test/url.sh b/run/hello-broken/test/url.sh new file mode 100755 index 0000000000..818c818eac --- /dev/null +++ b/run/hello-broken/test/url.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env 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. + +set -eo pipefail; + +requireEnv() { + test "${!1}" || (echo "Environment Variable '$1' not found" && exit 1) +} + +requireEnv SERVICE_NAME + +set -x +gcloud beta run services \ + describe "${SERVICE_NAME}" \ + --region="${REGION:-us-central1}" \ + --format='value(status.domain)' \ + --platform=managed From 1df21982e0e8f883c29904bebf9db1490979427e Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Wed, 18 Sep 2019 10:34:59 -0700 Subject: [PATCH 2/9] run: incorporate --quiet and comment feedback --- run/cloud-run-api/index.js | 21 ++++++ run/hello-broken/README.md | 12 +-- run/hello-broken/index.js | 10 ++- run/hello-broken/test/deploy.sh | 5 +- run/hello-broken/test/runner.sh | 10 ++- run/hello-broken/test/system.test.js | 106 +++++++++++++++++---------- run/logging-manual/README.md | 12 +-- run/logging-manual/test/deploy.sh | 5 +- run/logging-manual/test/runner.sh | 5 +- 9 files changed, 124 insertions(+), 62 deletions(-) create mode 100644 run/cloud-run-api/index.js diff --git a/run/cloud-run-api/index.js b/run/cloud-run-api/index.js new file mode 100644 index 0000000000..7dd6a11d41 --- /dev/null +++ b/run/cloud-run-api/index.js @@ -0,0 +1,21 @@ +const request = require('got'); + +// Load the project ID from GCP metadata server. +// You can also use https://www.npmjs.com/package/gcp-metadata. +exports.getProjectId = async () => { + const METADATA_PROJECT_ID_URL = + 'http://metadata.google.internal/computeMetadata/v1/project/project-id'; + const options = { + headers: {'Metadata-Flavor': 'Google'}, + }; + const response = await request(METADATA_PROJECT_ID_URL, options); + return response.body; +}; + +// Interacting with the Cloud Run API requires an access token. +exports.getServiceUrl = async (project, region, service) => { + const name = `projects/${project}/locations/${region}/services/${service}`; + const CLOUD_RUN_API_URL = `https://run.googleapis.com/v1alpha1/${name}`; + const response = await request(CLOUD_RUN_API_URL); + return response.body.status.domain; +}; diff --git a/run/hello-broken/README.md b/run/hello-broken/README.md index 598e75ae3c..adc124e0f5 100644 --- a/run/hello-broken/README.md +++ b/run/hello-broken/README.md @@ -44,7 +44,7 @@ test/deploy.sh ### runner.sh -The `runner.sh` script is a helper that facilitates: +The `runner.sh` script will: * Deploy the service to Cloud Run based on the `deploy.sh` script. * Set the `BASE_URL` and `ID_TOKEN` environment variables. @@ -57,11 +57,13 @@ test/runner.sh sleep 20 ## Environment Variables (Testing) -* `BASE_URL`: Used to designate the URL of the Cloud Run service under system test. The URL is not deterministic, so the options are to record it at deploy time or use the Cloud Run API to retrieve the value. -* `ID_TOKEN`: Used for authenticated HTTP requests to the Cloud Run service. +* `BASE_URL`: Specifies the Cloud Run service URL for end-to-end tests. + The default assigned URL of a Cloud Run service must be derived after deployment. +* `ID_TOKEN`: JWT token used to authenticate with Cloud Run's IAM-based authentication. * `REGION`: [`us-central1`] Optional override region for the location of the Cloud Run service. -* `SERVICE_NAME`: Used in testing to specify the name of the service. The name may be included in API calls and test conditions. -* `GOOGLE_CLOUD_PROJECT`: Used in production as an override to determination of the GCP Project from the GCP metadata server. Used in testing to configure the Logging client library. +* `SERVICE_NAME`: Specify the name of the deployed service, used in some API calls and test assertions. +* `GOOGLE_CLOUD_PROJECT`: Required for local testing, overrides use of the GCP metadata server to determine the current GCP project. + Required by the logging client library. ## Dependencies diff --git a/run/hello-broken/index.js b/run/hello-broken/index.js index 09bf1baaf5..c2ae88858e 100644 --- a/run/hello-broken/index.js +++ b/run/hello-broken/index.js @@ -29,10 +29,12 @@ app.get('/improved', (req, res) => { // [START run_broken_service_upgrade] const TARGET = process.env.TARGET || 'World'; if (!process.env.TARGET) { - console.log(JSON.stringify({ - severity: 'WARNING', - message: `TARGET not set, default to '${TARGET}'` - })); + console.log( + JSON.stringify({ + severity: 'WARNING', + message: `TARGET not set, default to '${TARGET}'`, + }) + ); } // [END run_broken_service_upgrade] res.send(`Hello ${TARGET}!`); diff --git a/run/hello-broken/test/deploy.sh b/run/hello-broken/test/deploy.sh index 630ac5d386..bfa5c163ec 100755 --- a/run/hello-broken/test/deploy.sh +++ b/run/hello-broken/test/deploy.sh @@ -25,11 +25,12 @@ requireEnv CONTAINER_IMAGE # Deploy the service set -x -gcloud beta --quiet run deploy "${SERVICE_NAME}" \ +gcloud beta run deploy "${SERVICE_NAME}" \ --image="${CONTAINER_IMAGE}" \ --region="${REGION:-us-central1}" \ ${FLAGS} \ - --platform=managed + --platform=managed \ + --quiet echo 'Cloud Run Links:' echo "- Logs: https://console.cloud.google.com/logs/viewer?project=${GOOGLE_CLOUD_PROJECT}&resource=cloud_run_revision%2Fservice_name%2F${SERVICE_NAME}" diff --git a/run/hello-broken/test/runner.sh b/run/hello-broken/test/runner.sh index 7059233ecd..409fac087c 100755 --- a/run/hello-broken/test/runner.sh +++ b/run/hello-broken/test/runner.sh @@ -37,12 +37,14 @@ echo # Only needed if deploy completed. function cleanup { set -x - gcloud --quiet beta run services delete ${SERVICE_NAME} \ + gcloud beta run services delete ${SERVICE_NAME} \ --platform=managed \ - --region="${REGION:-us-central1}" - gcloud --quiet beta run services delete ${SERVICE_OVERRIDE} \ + --region="${REGION:-us-central1}" \ + --quiet + gcloud beta run services delete ${SERVICE_OVERRIDE} \ --platform=managed \ - --region="${REGION:-us-central1}" + --region="${REGION:-us-central1}" \ + --quiet } trap cleanup EXIT diff --git a/run/hello-broken/test/system.test.js b/run/hello-broken/test/system.test.js index b7326a047d..be64cd8994 100644 --- a/run/hello-broken/test/system.test.js +++ b/run/hello-broken/test/system.test.js @@ -16,38 +16,11 @@ const get = (route, base_url) => { headers: { Authorization: `Bearer ${ID_TOKEN.trim()}`, }, - throwHttpErrors: false + throwHttpErrors: false, }); -} - -describe('Default Service', () => { - const {BASE_URL} = process.env; - if (!BASE_URL) { - throw Error( - '"BASE_URL" environment variable is required. For example: https://service-x8xabcdefg-uc.a.run.app' - ); - } - - it('Broken resource fails on any request', async () => { - const response = await get('/', BASE_URL); - assert.strictEqual(response.statusCode, 500, 'Internal service error not found'); - }); - - it('Fixed resource successfully falls back to a default value', async () => { - const response = await get('/improved', BASE_URL); - assert.strictEqual(response.statusCode, 200, 'Did not fallback to default as expected'); - assert.strictEqual(response.body, `Hello ${TARGET}!`, `Expected fallback "World" not found`); - }); -}); - -describe('Overridden Service', () => { - const {BASE_URL_OVERRIDE} = process.env; - if (!BASE_URL_OVERRIDE) { - throw Error( - '"BASE_URL_OVERRIDE" environment variable is required. For example: https://service-x8xabcdefg-uc.a.run.app' - ); - } +}; +describe('End-to-End Tests', () => { const {TARGET} = process.env; if (!TARGET) { throw Error( @@ -55,15 +28,72 @@ describe('Overridden Service', () => { ); } - it('Broken resource uses the TARGET override', async () => { - const response = await get('/', BASE_URL); - assert.strictEqual(response.statusCode, 200, 'Did not use the TARGET override'); - assert.strictEqual(response.body, `Hello ${TARGET}!`, `Expected override "${TARGET}" not found`); + describe('Default Service', () => { + const {BASE_URL} = process.env; + if (!BASE_URL) { + throw Error( + '"BASE_URL" environment variable is required. For example: https://service-x8xabcdefg-uc.a.run.app' + ); + } + + it('Broken resource fails on any request', async () => { + const response = await get('/', BASE_URL); + assert.strictEqual( + response.statusCode, + 500, + 'Internal service error not found' + ); + }); + + it('Fixed resource successfully falls back to a default value', async () => { + const response = await get('/improved', BASE_URL); + assert.strictEqual( + response.statusCode, + 200, + 'Did not fallback to default as expected' + ); + assert.strictEqual( + response.body, + `Hello ${TARGET}!`, + `Expected fallback "World" not found` + ); + }); }); - it('Fixed resource uses the TARGET override', async () => { - const response = await get('/improved', BASE_URL_OVERRIDE); - assert.strictEqual(response.statusCode, 200, 'Did not fallback to default as expected'); - assert.strictEqual(response.body, `Hello ${TARGET}!`, `Expected override "${TARGET}" not found`); + describe('Overridden Service', () => { + const {BASE_URL_OVERRIDE} = process.env; + if (!BASE_URL_OVERRIDE) { + throw Error( + '"BASE_URL_OVERRIDE" environment variable is required. For example: https://service-x8xabcdefg-uc.a.run.app' + ); + } + + it('Broken resource uses the TARGET override', async () => { + const response = await get('/', BASE_URL_OVERRIDE); + assert.strictEqual( + response.statusCode, + 200, + 'Did not use the TARGET override' + ); + assert.strictEqual( + response.body, + `Hello ${TARGET}!`, + `Expected override "${TARGET}" not found` + ); + }); + + it('Fixed resource uses the TARGET override', async () => { + const response = await get('/improved', BASE_URL_OVERRIDE); + assert.strictEqual( + response.statusCode, + 200, + 'Did not fallback to default as expected' + ); + assert.strictEqual( + response.body, + `Hello ${TARGET}!`, + `Expected override "${TARGET}" not found` + ); + }); }); }); diff --git a/run/logging-manual/README.md b/run/logging-manual/README.md index d6fd78a199..8560d60ae7 100644 --- a/run/logging-manual/README.md +++ b/run/logging-manual/README.md @@ -42,7 +42,7 @@ test/deploy.sh ### runner.sh -The `runner.sh` script is a helper that facilitates: +The `runner.sh` script will: * Deploy the service to Cloud Run based on the `deploy.sh` script. * Set the `BASE_URL` and `ID_TOKEN` environment variables. @@ -55,11 +55,13 @@ test/runner.sh sleep 20 ## Environment Variables (Testing) -* `BASE_URL`: Used to designate the URL of the Cloud Run service under system test. The URL is not deterministic, so the options are to record it at deploy time or use the Cloud Run API to retrieve the value. -* `ID_TOKEN`: Used for authenticated HTTP requests to the Cloud Run service. +* `BASE_URL`: Specifies the Cloud Run service URL for end-to-end tests. + The default assigned URL of a Cloud Run service must be derived after deployment. +* `ID_TOKEN`: JWT token used to authenticate with Cloud Run's IAM-based authentication. * `REGION`: [`us-central1`] Optional override region for the location of the Cloud Run service. -* `SERVICE_NAME`: Used in testing to specify the name of the service. The name may be included in API calls and test conditions. -* `GOOGLE_CLOUD_PROJECT`: Used in production as an override to determination of the GCP Project from the GCP metadata server. Used in testing to configure the Logging client library. +* `SERVICE_NAME`: Specify the name of the deployed service, used in some API calls and test assertions. +* `GOOGLE_CLOUD_PROJECT`: Required for local testing, overrides use of the GCP metadata server to determine the current GCP project. + Required by the logging client library. ## Dependencies diff --git a/run/logging-manual/test/deploy.sh b/run/logging-manual/test/deploy.sh index f4169587ab..9346d0e707 100755 --- a/run/logging-manual/test/deploy.sh +++ b/run/logging-manual/test/deploy.sh @@ -25,10 +25,11 @@ requireEnv CONTAINER_IMAGE # Deploy the service set -x -gcloud beta --quiet run deploy "${SERVICE_NAME}" \ +gcloud beta run deploy "${SERVICE_NAME}" \ --image="${CONTAINER_IMAGE}" \ --region="${REGION:-us-central1}" \ - --platform=managed + --platform=managed \ + --quiet echo 'Cloud Run Links:' echo "- Logs: https://console.cloud.google.com/logs/viewer?project=${GOOGLE_CLOUD_PROJECT}&resource=cloud_run_revision%2Fservice_name%2F${SERVICE_NAME}" diff --git a/run/logging-manual/test/runner.sh b/run/logging-manual/test/runner.sh index 8d7ebdf321..76c6e29b1f 100755 --- a/run/logging-manual/test/runner.sh +++ b/run/logging-manual/test/runner.sh @@ -32,9 +32,10 @@ echo # Only needed if deploy completed. function cleanup { set -x - gcloud --quiet beta run services delete ${SERVICE_NAME} \ + gcloud beta run services delete ${SERVICE_NAME} \ --platform=managed \ - --region="${REGION:-us-central1}" + --region="${REGION:-us-central1}" \ + --quiet } trap cleanup EXIT From f316b33d176211976fa1cb60a5d8a268f149a30e Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Wed, 18 Sep 2019 13:35:33 -0700 Subject: [PATCH 3/9] run/broken: add full license headers --- run/hello-broken/Dockerfile | 16 +++++++++++++--- run/hello-broken/index.js | 16 +++++++++++++--- run/hello-broken/test/system.test.js | 16 +++++++++++++--- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/run/hello-broken/Dockerfile b/run/hello-broken/Dockerfile index dedd8deb5a..4dc6de2b97 100644 --- a/run/hello-broken/Dockerfile +++ b/run/hello-broken/Dockerfile @@ -1,6 +1,16 @@ -# 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. +# 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. # [START run_broken_dockerfile] diff --git a/run/hello-broken/index.js b/run/hello-broken/index.js index c2ae88858e..4d6c49e489 100644 --- a/run/hello-broken/index.js +++ b/run/hello-broken/index.js @@ -1,6 +1,16 @@ -// 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. +// 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. // [START run_broken_service] const express = require('express'); diff --git a/run/hello-broken/test/system.test.js b/run/hello-broken/test/system.test.js index be64cd8994..c73533ffbb 100644 --- a/run/hello-broken/test/system.test.js +++ b/run/hello-broken/test/system.test.js @@ -1,6 +1,16 @@ -// 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. +// 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. const assert = require('assert'); const request = require('got'); From b85de4da4988d0f71d8be92841d719256d673ad0 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Wed, 18 Sep 2019 15:24:34 -0700 Subject: [PATCH 4/9] run: remove unintended wip sample --- run/cloud-run-api/index.js | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 run/cloud-run-api/index.js diff --git a/run/cloud-run-api/index.js b/run/cloud-run-api/index.js deleted file mode 100644 index 7dd6a11d41..0000000000 --- a/run/cloud-run-api/index.js +++ /dev/null @@ -1,21 +0,0 @@ -const request = require('got'); - -// Load the project ID from GCP metadata server. -// You can also use https://www.npmjs.com/package/gcp-metadata. -exports.getProjectId = async () => { - const METADATA_PROJECT_ID_URL = - 'http://metadata.google.internal/computeMetadata/v1/project/project-id'; - const options = { - headers: {'Metadata-Flavor': 'Google'}, - }; - const response = await request(METADATA_PROJECT_ID_URL, options); - return response.body; -}; - -// Interacting with the Cloud Run API requires an access token. -exports.getServiceUrl = async (project, region, service) => { - const name = `projects/${project}/locations/${region}/services/${service}`; - const CLOUD_RUN_API_URL = `https://run.googleapis.com/v1alpha1/${name}`; - const response = await request(CLOUD_RUN_API_URL); - return response.body.status.domain; -}; From 8bb81991b71eede8fb97c79642d16a125f59e5ae Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Wed, 18 Sep 2019 15:41:10 -0700 Subject: [PATCH 5/9] rename TARGET env var to NAME --- run/hello-broken/index.js | 20 ++++++++++---------- run/hello-broken/test/runner.sh | 4 ++-- run/hello-broken/test/system.test.js | 22 +++++++++++----------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/run/hello-broken/index.js b/run/hello-broken/index.js index 4d6c49e489..2ed9d1981d 100644 --- a/run/hello-broken/index.js +++ b/run/hello-broken/index.js @@ -17,11 +17,11 @@ const express = require('express'); const app = express(); app.get('/', (req, res) => { - console.log('hello-broken: received request.'); + console.log('hello: received request.'); // [START run_broken_service_problem] - const {TARGET} = process.env; - if (!TARGET) { + const {NAME} = process.env; + if (!NAME) { // Plain error logs do not appear in Stackdriver Error Reporting. console.error('Environment validation failed.'); console.error(new Error('Missing required server parameter')); @@ -29,30 +29,30 @@ app.get('/', (req, res) => { return; } // [END run_broken_service_problem] - res.send(`Hello ${TARGET}!`); + res.send(`Hello ${NAME}!`); }); // [END run_broken_service] app.get('/improved', (req, res) => { - console.log('hello-broken: received request.'); + console.log('hello: received request.'); // [START run_broken_service_upgrade] - const TARGET = process.env.TARGET || 'World'; - if (!process.env.TARGET) { + const NAME = process.env.NAME || 'World'; + if (!process.env.NAME) { console.log( JSON.stringify({ severity: 'WARNING', - message: `TARGET not set, default to '${TARGET}'`, + message: `NAME not set, default to '${NAME}'`, }) ); } // [END run_broken_service_upgrade] - res.send(`Hello ${TARGET}!`); + res.send(`Hello ${NAME}!`); }); // [START run_broken_service] const port = process.env.PORT || 8080; app.listen(port, () => { - console.log(`hello-broken: listening on port ${port}`); + console.log(`hello: listening on port ${port}`); }); // [END run_broken_service] diff --git a/run/hello-broken/test/runner.sh b/run/hello-broken/test/runner.sh index 409fac087c..f814994f7b 100755 --- a/run/hello-broken/test/runner.sh +++ b/run/hello-broken/test/runner.sh @@ -21,13 +21,13 @@ requireEnv() { } requireEnv SERVICE_NAME -# The hello-broken sample needs to be tested with the TARGET environment variable +# The hello-broken sample needs to be tested with the NAME environment variable # both set and unset. SERVICE_OVERRIDE="${SERVICE_NAME}-override" echo '---' test/deploy.sh -FLAGS="--set-env-vars TARGET=$TARGET" SERVICE_NAME=${SERVICE_OVERRIDE} test/deploy.sh +FLAGS="--set-env-vars NAME=$NAME" SERVICE_NAME=${SERVICE_OVERRIDE} test/deploy.sh echo echo '---' diff --git a/run/hello-broken/test/system.test.js b/run/hello-broken/test/system.test.js index c73533ffbb..81213ef066 100644 --- a/run/hello-broken/test/system.test.js +++ b/run/hello-broken/test/system.test.js @@ -31,10 +31,10 @@ const get = (route, base_url) => { }; describe('End-to-End Tests', () => { - const {TARGET} = process.env; - if (!TARGET) { + const {NAME} = process.env; + if (!NAME) { throw Error( - '"TARGET" environment variable is required. For example: Cosmos' + '"NAME" environment variable is required. For example: Cosmos' ); } @@ -64,7 +64,7 @@ describe('End-to-End Tests', () => { ); assert.strictEqual( response.body, - `Hello ${TARGET}!`, + `Hello ${NAME}!`, `Expected fallback "World" not found` ); }); @@ -78,21 +78,21 @@ describe('End-to-End Tests', () => { ); } - it('Broken resource uses the TARGET override', async () => { + it('Broken resource uses the NAME override', async () => { const response = await get('/', BASE_URL_OVERRIDE); assert.strictEqual( response.statusCode, 200, - 'Did not use the TARGET override' + 'Did not use the NAME override' ); assert.strictEqual( response.body, - `Hello ${TARGET}!`, - `Expected override "${TARGET}" not found` + `Hello ${NAME}!`, + `Expected override "${NAME}" not found` ); }); - it('Fixed resource uses the TARGET override', async () => { + it('Fixed resource uses the NAME override', async () => { const response = await get('/improved', BASE_URL_OVERRIDE); assert.strictEqual( response.statusCode, @@ -101,8 +101,8 @@ describe('End-to-End Tests', () => { ); assert.strictEqual( response.body, - `Hello ${TARGET}!`, - `Expected override "${TARGET}" not found` + `Hello ${NAME}!`, + `Expected override "${NAME}" not found` ); }); }); From 6a9a8aac6ecb8686bb4b247e2c09d06dbf9e75e2 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Fri, 20 Sep 2019 14:12:44 -0700 Subject: [PATCH 6/9] fix eslint spacing on error text --- run/hello-broken/test/system.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/run/hello-broken/test/system.test.js b/run/hello-broken/test/system.test.js index 81213ef066..8ecf4f1f4b 100644 --- a/run/hello-broken/test/system.test.js +++ b/run/hello-broken/test/system.test.js @@ -33,9 +33,7 @@ const get = (route, base_url) => { describe('End-to-End Tests', () => { const {NAME} = process.env; if (!NAME) { - throw Error( - '"NAME" environment variable is required. For example: Cosmos' - ); + throw Error('"NAME" environment variable is required. For example: Cosmos'); } describe('Default Service', () => { From eb5bb2c0e39dc5e621ec5a58ace0fe7cdc1aba39 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Fri, 20 Sep 2019 16:39:01 -0700 Subject: [PATCH 7/9] README wording refinements --- run/hello-broken/README.md | 15 ++++++--------- run/logging-manual/README.md | 24 +++++++++++++----------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/run/hello-broken/README.md b/run/hello-broken/README.md index adc124e0f5..8c1e10314b 100644 --- a/run/hello-broken/README.md +++ b/run/hello-broken/README.md @@ -44,12 +44,12 @@ test/deploy.sh ### runner.sh -The `runner.sh` script will: +The `runner.sh` script: -* Deploy the service to Cloud Run based on the `deploy.sh` script. -* Set the `BASE_URL` and `ID_TOKEN` environment variables. -* Run any arguments passed to the `runner.sh` script. -* Tear down the Cloud Run service on completion. +* Deploys the service to Cloud Run based on the `deploy.sh` script. +* Sets the `BASE_URL` and `ID_TOKEN` environment variables. +* Runs any arguments passed to the `runner.sh` script. +* Tears down the Cloud Run service on completion. ```sh test/runner.sh sleep 20 @@ -58,12 +58,9 @@ test/runner.sh sleep 20 ## Environment Variables (Testing) * `BASE_URL`: Specifies the Cloud Run service URL for end-to-end tests. - The default assigned URL of a Cloud Run service must be derived after deployment. * `ID_TOKEN`: JWT token used to authenticate with Cloud Run's IAM-based authentication. * `REGION`: [`us-central1`] Optional override region for the location of the Cloud Run service. -* `SERVICE_NAME`: Specify the name of the deployed service, used in some API calls and test assertions. -* `GOOGLE_CLOUD_PROJECT`: Required for local testing, overrides use of the GCP metadata server to determine the current GCP project. - Required by the logging client library. +* `SERVICE_NAME`: The name of the deployed service, used in some API calls and test assertions. ## Dependencies diff --git a/run/logging-manual/README.md b/run/logging-manual/README.md index 8560d60ae7..6d1692fdf5 100644 --- a/run/logging-manual/README.md +++ b/run/logging-manual/README.md @@ -42,12 +42,12 @@ test/deploy.sh ### runner.sh -The `runner.sh` script will: +The `runner.sh` script: -* Deploy the service to Cloud Run based on the `deploy.sh` script. -* Set the `BASE_URL` and `ID_TOKEN` environment variables. -* Run any arguments passed to the `runner.sh` script. -* Tear down the Cloud Run service on completion. +* Deploys the service to Cloud Run based on the `deploy.sh` script. +* Sets the `BASE_URL` and `ID_TOKEN` environment variables. +* Runs any arguments passed to the `runner.sh` script. +* Tears down the Cloud Run service on completion. ```sh test/runner.sh sleep 20 @@ -55,16 +55,18 @@ test/runner.sh sleep 20 ## Environment Variables (Testing) -* `BASE_URL`: Specifies the Cloud Run service URL for end-to-end tests. - The default assigned URL of a Cloud Run service must be derived after deployment. +* `BASE_URL`: The Cloud Run service URL for end-to-end tests. * `ID_TOKEN`: JWT token used to authenticate with Cloud Run's IAM-based authentication. * `REGION`: [`us-central1`] Optional override region for the location of the Cloud Run service. -* `SERVICE_NAME`: Specify the name of the deployed service, used in some API calls and test assertions. -* `GOOGLE_CLOUD_PROJECT`: Required for local testing, overrides use of the GCP metadata server to determine the current GCP project. - Required by the logging client library. +* `SERVICE_NAME`: The name of the deployed service, used in some API calls and test assertions. +* `GOOGLE_CLOUD_PROJECT`: If used, overrides GCP metadata server to determine + the current GCP project. Required by the logging client library. ## Dependencies * **express**: Web server framework. -* **got**: Used to pull the project ID of the running service from the [compute metadata server](https://cloud.google.com/compute/docs/storing-retrieving-metadata) and make system test HTTP requests. This is required in production for log correlation without manually setting the $GOOGLE_CLOUD_PROJECT environment variable. +* **got**: Used to pull the project ID of the running service from the + [compute metadata server](https://cloud.google.com/compute/docs/storing-retrieving-metadata) + and make system test HTTP requests. This is required in production for log correlation without + manually setting the $GOOGLE_CLOUD_PROJECT environment variable. From 09f7f49ebbd1228dfac650f0ae7839f5736e83ed Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Mon, 23 Sep 2019 07:45:58 -0700 Subject: [PATCH 8/9] return error response on same line --- run/hello-broken/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/run/hello-broken/index.js b/run/hello-broken/index.js index 2ed9d1981d..a0a89b1d7a 100644 --- a/run/hello-broken/index.js +++ b/run/hello-broken/index.js @@ -25,8 +25,7 @@ app.get('/', (req, res) => { // Plain error logs do not appear in Stackdriver Error Reporting. console.error('Environment validation failed.'); console.error(new Error('Missing required server parameter')); - res.status(500).send('Internal Server Error'); - return; + return res.status(500).send('Internal Server Error'); } // [END run_broken_service_problem] res.send(`Hello ${NAME}!`); From a06ed5965c99b32408f5e74bd9ed7bda56619cb6 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Mon, 23 Sep 2019 07:47:26 -0700 Subject: [PATCH 9/9] align package.json with feedback on system package, add eslint scripts --- run/hello-broken/package.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/run/hello-broken/package.json b/run/hello-broken/package.json index 72ab404d96..e4c08eb4a8 100644 --- a/run/hello-broken/package.json +++ b/run/hello-broken/package.json @@ -1,19 +1,18 @@ { "name": "hello-broken", - "private": true, "description": "Broken Cloud Run service for troubleshooting practice", + "version": "1.0.0", + "private": true, "main": "index.js", "scripts": { "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 0", - "e2e-test": "TARGET=Cloud test/runner.sh mocha test/system.test.js --timeout=20000" + "e2e-test": "TARGET=Cloud test/runner.sh mocha test/system.test.js --timeout=20000", + "lint": "eslint '**/*.js'", + "fix": "eslint --fix '**/*.js'" }, "author": "Google LLC", "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" - }, "dependencies": { "express": "^4.17.1" },