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

Add Endpoints gRPC + JWT sample #419

Merged
merged 6 commits into from
Jul 10, 2017
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
57 changes: 44 additions & 13 deletions endpoints/getting-started-grpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This sample demonstrates how to use Google Cloud Endpoints with Node.js.

For a complete walkthrough showing how to run this sample in different
environments, see the [Google Cloud Endpoints Quickstarts](https://cloud.google.com/endpoints/docs/quickstarts).
environments, see the [Google Cloud Endpoints Quickstarts][docs_quickstart].

## Running locally

Expand All @@ -19,19 +19,26 @@ $ node client.js -h localhost:50051

## Running on Google Cloud Platform
### Setup
Make sure you have [gcloud](https://cloud.google.com/sdk/gcloud/) and [Node.js](https://nodejs.org/) installed.
Make sure you have [gcloud][gcloud] and [Node.js][nodejs] installed.

To update `gcloud`, use the `gcloud components update` command.

### Selecting an authentication method
1. Determine the appropriate API configuration file to use based on your authentication method.
- [JSON Web Tokens][jwt_io]: use `api_config.jwt.yaml`
- [API keys][gcp_api_key]: use `api_config.key.yaml`

2. Rename the `api_config.*.yaml` file you chose in Step 1 to `api_config.yaml`.

### Deploying to Endpoints
1. Install [protoc](https://github.com/google/protobuf/#protocol-compiler-installation).
1. Install [protoc][protoc].

1. Compile the proto file using protoc.
```
$ protoc --include_imports --include_source_info protos/helloworld.proto --descriptor_set_out out.pb
```

1. In `api_config.yaml`, replace `MY_PROJECT_ID` with your Project ID.
1. In `api_config.yaml`, replace `MY_PROJECT_ID` and `SERVICE-ACCOUNT-ID` with your Project ID and your service account's email address respectively.

1. Deploy your service's configuration to Endpoints. Take note of your service's config ID and name once the deployment completes.
```
Expand All @@ -47,11 +54,11 @@ $ gcloud container builds submit --tag gcr.io/[YOUR_PROJECT_ID]/endpoints-exampl

### Running your service
#### Compute Engine
1. [Create](https://console.cloud.google.com/compute/instancesAdd) a Compute Engine instance. Be sure to check **Allow HTTP traffic** and **Allow HTTPS traffic** when creating the instance.
1. [Create][console_gce_create] a Compute Engine instance. Be sure to check **Allow HTTP traffic** and **Allow HTTPS traffic** when creating the instance.

1. Once your instance is created, take note of its IP address.

Note: this IP address is _ephemeral_ by default, and may change unexpectedly. If you plan to use this instance in the future, [reserve a static IP address](https://cloud.google.com/compute/docs/configure-ip-addresses#reserve_new_static) instead.
Note: this IP address is _ephemeral_ by default, and may change unexpectedly. If you plan to use this instance in the future, [reserve a static IP address][docs_gce_static_ip] instead.

1. SSH into your instance, and install Docker.
```
Expand All @@ -75,7 +82,7 @@ $ sudo docker run --detach --name=esp \
-a grpc://helloworld:50051
```

1. On your local machine, use the client to test your Endpoints deployment. Replace `[YOUR_INSTANCE_IP_ADDRESS]` with your instance's external IP address, and `[YOUR_API_KEY]` with a [valid Google Cloud Platform API key](https://support.google.com/cloud/answer/6158862?hl=en).
1. On your local machine, use the client to test your Endpoints deployment. Replace `[YOUR_INSTANCE_IP_ADDRESS]` with your instance's external IP address, and `[YOUR_API_KEY]` with a [valid Google Cloud Platform API key][gcp_api_key].
```
$ node client.js -h [YOUR_INSTANCE_IP_ADDRESS]:80 -k [YOUR_API_KEY]
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to add instructions for using jwt here too.

Expand All @@ -86,7 +93,7 @@ $ node client.js -h [YOUR_INSTANCE_IP_ADDRESS]:80 -k [YOUR_API_KEY]
$ gcloud components install kubectl
```

1. [Create](https://console.cloud.google.com/kubernetes/add) a container cluster with the default settings. Remember the cluster's name and zone, as you will need these later.
1. [Create][console_gke_create] a container cluster with the default settings. Remember the cluster's name and zone, as you will need these later.


1. Configure `kubectl` to have access to the cluster. Replace `[YOUR_CLUSTER_NAME]` and `[YOUR_CLUSTER_ZONE]` with your cluster's name and zone respectively.
Expand All @@ -96,7 +103,7 @@ $ gcloud container clusters get-credentials [YOUR_CLUSTER_NAME] --zone [YOUR_CLU

1. Edit the `container_engine.yaml` file, and replace `GCLOUD_PROJECT`, `SERVICE_NAME`, and `SERVICE_CONFIG` with your Project ID and your Endpoints service's name and config ID respectively.

1. Add a [Kubernetes service](https://kubernetes.io/docs/user-guide/services/) to the cluster you created. Note that Kubernetes services should not be confused with [Endpoints services](https://cloud.google.com/endpoints/docs/grpc).
1. Add a [Kubernetes service][docs_k8s_services] to the cluster you created. Note that Kubernetes services should not be confused with [Endpoints services][docs_endpoints_services].
```
$ kubectl create -f container-engine.yaml
```
Expand All @@ -106,17 +113,41 @@ $ kubectl create -f container-engine.yaml
$ kubectl get service
```

1. Use the client to test your Endpoints deployment. Replace `[YOUR_CLUSTER_IP_ADDRESS]` with your service's external IP address, and `[YOUR_API_KEY]` with a [valid Google Cloud Platform API key](https://support.google.com/cloud/answer/6158862?hl=en).
1. Use the client to test your Endpoints deployment. When running the following commands, replace `[YOUR_CLUSTER_IP_ADDRESS]` with your service's external IP address.

If you're using an API key, run the following command and replace `[YOUR_API_KEY]` with the appropriate [API key][gcp_api_key].
```
$ node client.js -h [YOUR_CLUSTER_IP_ADDRESS]:80 -k [YOUR_API_KEY]
```

If you're using a [JSON Web Token][jwt_io], run the following command and replace `[YOUR_JWT_AUTHTOKEN]` with a valid JSON Web Token.
```
$ node client.js -h [YOUR_CLUSTER_IP_ADDRESS]:80 -k [YOUR_JWT_AUTHTOKEN]
```

## Cleanup
If you do not intend to use the resources you created for this tutorial in the future, delete your [VM instances](https://console.cloud.google.com/compute/instances) and/or [container clusters](https://console.cloud.google.com/kubernetes/list) to prevent additional charges.
If you do not intend to use the resources you created for this tutorial in the future, delete your [VM instances][console_gce_instances] and/or [container clusters][console_gke_instances] to prevent additional charges.

## Troubleshooting
If you're having issues with this tutorial, here are some things to try:
- [Check](https://console.cloud.google.com/logs/viewer) your VM instance's/cluster's logs
- [Check][console_logs] your GCE/GKE instance's logs
- Make sure your Compute Engine instance's [firewall](https://console.cloud.google.com/networking/firewalls/list) permits TCP access to port 80

If those suggestions don't solve your problem, please [let us know](https://github.com/GoogleCloudPlatform/nodejs-docs-samples/issues) or [submit a PR](https://github.com/GoogleCloudPlatform/nodejs-docs-samples/pulls).
If those suggestions don't solve your problem, please [let us know][github_issues] or [submit a pull request][github_pulls].

[nodejs]: https://nodejs.org/
[gcloud]: https://cloud.google.com/sdk/gcloud/
[jwt_io]: https://jwt.io
[protoc]: https://github.com/google/protobuf/#protocol-compiler-installation
[gcp_api_key]: https://support.google.com/cloud/answer/6158862?hl=en
[github_issues]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/issues
[github_pulls]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/pulls
[console_gce_instances]: https://console.cloud.google.com/compute/instances
[console_gce_create]: https://console.cloud.google.com/compute/instancesAdd
[console_gke_instances]: https://console.cloud.google.com/kubernetes/list
[console_gke_create]: https://console.cloud.google.com/kubernetes/add
[console_logs]: https://console.cloud.google.com/logs/viewer
[docs_k8s_services]: https://kubernetes.io/docs/user-guide/services/
[docs_endpoints_services]: https://cloud.google.com/endpoints/docs/grpc
[docs_gce_static_ip]: https://cloud.google.com/compute/docs/configure-ip-addresses#reserve_new_static
[docs_quickstart]: https://cloud.google.com/endpoints/docs/quickstarts
61 changes: 61 additions & 0 deletions endpoints/getting-started-grpc/api_config.jwt.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2017 Google Inc.
#
# 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.

#
# An example API configuration.
#
# Below, replace MY_PROJECT_ID with your Google Cloud Project ID.
#

# The configuration schema is defined by service.proto file
# https://github.com/googleapis/googleapis/blob/master/google/api/service.proto
type: google.api.Service
config_version: 3

#
# Name of the service configuration.
#
name: hellogrpc.endpoints.MY_PROJECT_ID.cloud.goog

#
# API title to appear in the user interface (Google Cloud Console).
#
title: Hello gRPC API
apis:
- name: helloworld.Greeter

#
# API usage restrictions
#
usage:
rules:
# None of these API methods require an API key
# N.B: JWTs are not a substitute for API keys
- selector: "*"
allow_unregistered_calls: true

#
# Request authentication (in this case, a JWT)
#
authentication:
providers:
- id: google_service_account
# Replace SERVICE-ACCOUNT-ID with your service account's email address.
issuer: SERVICE-ACCOUNT-ID
jwks_uri: https://www.googleapis.com/robot/v1/metadata/x509/SERVICE-ACCOUNT-ID
rules:
# This auth rule will apply to all methods.
- selector: "*"
requirements:
- provider_id: google_service_account
28 changes: 24 additions & 4 deletions endpoints/getting-started-grpc/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@

'use strict';

function makeGrpcRequest (API_KEY, HOST, GREETEE) {
function makeGrpcRequest (JWT_AUTH_TOKEN, API_KEY, HOST, GREETEE) {
// Uncomment these lines to set their values
// const JWT_AUTH_TOKEN = 'YOUR_JWT_AUTH_TOKEN';
// const API_KEY = 'YOUR_API_KEY';
// const HOST = 'localhost:50051'; // The IP address of your endpoints host
// const GREETEE = 'world';
Expand All @@ -34,7 +35,11 @@ function makeGrpcRequest (API_KEY, HOST, GREETEE) {

// Build gRPC request
const metadata = new grpc.Metadata();
metadata.add('x-api-key', API_KEY);
if (API_KEY) {
metadata.add('x-api-key', API_KEY);
} else if (JWT_AUTH_TOKEN) {
metadata.add('authorization', `Bearer ${JWT_AUTH_TOKEN}`);
}

// Execute gRPC request
client.sayHello({ name: GREETEE }, metadata, (err, response) => {
Expand All @@ -50,7 +55,13 @@ function makeGrpcRequest (API_KEY, HOST, GREETEE) {

// The command-line program
const argv = require('yargs')
.usage('Usage: node $0 -k YOUR_API_KEY [-h YOUR_ENDPOINTS_HOST] [-g GREETEE_NAME]')
.usage('Usage: node $0 {-k YOUR_API_KEY>, <-j YOUR_JWT_AUTH_TOKEN} [-h YOUR_ENDPOINTS_HOST] [-g GREETEE_NAME]')
.option('jwtAuthToken', {
alias: 'j',
type: 'string',
global: true,
default: ''
})
.option('apiKey', {
alias: 'k',
type: 'string',
Expand All @@ -69,8 +80,17 @@ const argv = require('yargs')
default: 'world',
global: true
})
.check((argv) => {
const valid = !!(argv.jwtAuthToken || argv.apiKey);
if (!valid) {
console.error('One of API_KEY or JWT_AUTH_TOKEN must be set.');
}
return valid;
})
.wrap(120)
.help()
.strict()
.epilogue(`For more information, see https://cloud.google.com/endpoints/docs`)
.argv;

makeGrpcRequest(argv.apiKey, argv.host, argv.greetee);
makeGrpcRequest(argv.jwtAuthToken, argv.apiKey, argv.host, argv.greetee);
12 changes: 8 additions & 4 deletions endpoints/getting-started-grpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@
"scripts": {
"start": "node server.js",
"system-test": "ava -T 30s --verbose system-test/*.test.js",
"test": "npm run system-test"
"test": "samples lint && npm run system-test"
},
"dependencies": {
"body-parser": "1.17.2",
"express": "4.15.3",
"grpc": "1.3.8",
"yargs": "8.0.2"
"yargs": "8.0.2",
"google-auth-library": "^0.10.0",
"jsonwebtoken": "^7.4.1"
},
"devDependencies": {
"@google-cloud/nodejs-repo-tools": "1.4.15",
"@google-cloud/nodejs-repo-tools": "^1.4.15",
"ava": "0.19.1"
},
"cloud-repo-tools": {
Expand All @@ -33,7 +35,9 @@
"requiredEnvVars": [
"ENDPOINTS_API_KEY",
"ENDPOINTS_GCE_HOST",
"ENDPOINTS_GKE_HOST"
"ENDPOINTS_GKE_HOST",
"ENDPOINTS_SERVICE_NAME",
"GOOGLE_APPLICATION_CREDENTIALS"
]
}
}
Expand Down
61 changes: 54 additions & 7 deletions endpoints/getting-started-grpc/system-test/endpoints.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
const childProcess = require('child_process');
const path = require('path');
const test = require('ava');
const fs = require(`fs`);
const jwt = require('jsonwebtoken');
const tools = require('@google-cloud/nodejs-repo-tools');

const clientCmd = `node client.js`;
Expand All @@ -26,36 +28,81 @@ const serverCmd = `node server.js`;
const cwd = path.join(__dirname, `..`);

const API_KEY = process.env.ENDPOINTS_API_KEY;
const GOOGLE_KEYFILE = JSON.parse(fs.readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'utf8'));
const SERVICE_NAME = process.env.ENDPOINTS_SERVICE_NAME;
const GCE_HOST = process.env.ENDPOINTS_GCE_HOST;
const GKE_HOST = process.env.ENDPOINTS_GKE_HOST;

test.before((t) => {
t.truthy(API_KEY, 'Must set API_KEY environment variable!');
t.truthy(GCE_HOST, 'Must set GCE_HOST environment variable!');
t.truthy(GKE_HOST, 'Must set GKE_HOST environment variable!');
t.truthy(API_KEY, 'Must set ENDPOINTS_API_KEY environment variable!');
t.truthy(GCE_HOST, 'Must set ENDPOINTS_GCE_HOST environment variable!');
t.truthy(GKE_HOST, 'Must set ENDPOINTS_GKE_HOST environment variable!');
t.truthy(SERVICE_NAME, 'Must set ENDPOINTS_SERVICE_NAME environment variable!');
t.truthy(GOOGLE_KEYFILE, 'GOOGLE_APPLICATION_CREDENTIALS environment variable must point to a service account keyfile!');
t.truthy(GOOGLE_KEYFILE.client_email, 'Service account keyfile must contain a "client_email" field!');
t.truthy(GOOGLE_KEYFILE.private_key, 'Service account keyfile must contain a "private_key" field!');
});

// Generate JWT based on GOOGLE_APPLICATION_CREDENTIALS and ENDPOINTS_SERVICE_NAME
const JWT_AUTH_TOKEN = jwt.sign({
'aud': SERVICE_NAME,
'iss': GOOGLE_KEYFILE.client_email,
'iat': parseInt(Date.now() / 1000),
'exp': parseInt(Date.now() / 1000) + (20 * 60), // 20 minutes
'email': GOOGLE_KEYFILE.client_email,
'sub': GOOGLE_KEYFILE.client_email
}, GOOGLE_KEYFILE.private_key, { algorithm: 'RS256' });

const delay = (mSec) => {
return new Promise((resolve) => setTimeout(resolve, mSec));
};

test.serial(`should request a greeting from a remote Compute Engine instance`, async (t) => {
// API key
test(`should request a greeting from a remote Compute Engine instance using an API key`, async (t) => {
const output = await tools.runAsync(`${clientCmd} -h ${GCE_HOST} -k ${API_KEY}`, cwd);
t.regex(output, /Hello world/);
});

test.serial(`should request a greeting from a remote Container Engine cluster`, async (t) => {
test(`should request a greeting from a remote Container Engine cluster using an API key`, async (t) => {
const output = await tools.runAsync(`${clientCmd} -h ${GKE_HOST} -k ${API_KEY}`, cwd);
t.regex(output, /Hello world/);
});

test.serial(`should request and handle a greeting locally`, async (t) => {
test.serial(`should request and handle a greeting locally using an API key`, async (t) => {
const PORT = 50051;
const server = childProcess.exec(`${serverCmd} -p ${PORT}`, { cwd: cwd });

await delay(1000);
console.log(`${clientCmd} -h localhost:${PORT} -k ${API_KEY}`);
const clientOutput = await tools.runAsync(`${clientCmd} -h localhost:${PORT} -k ${API_KEY}`, cwd);
t.regex(clientOutput, /Hello world/);
server.kill();
});

// Authtoken
test(`should request a greeting from a remote Compute Engine instance using a JWT Auth Token`, async (t) => {
const output = await tools.runAsync(`${clientCmd} -h ${GCE_HOST} -j ${JWT_AUTH_TOKEN}`, cwd);
t.regex(output, /Hello world/);
});

test(`should request a greeting from a remote Container Engine cluster using a JWT Auth Token`, async (t) => {
const output = await tools.runAsync(`${clientCmd} -h ${GKE_HOST} -j ${JWT_AUTH_TOKEN}`, cwd);
t.regex(output, /Hello world/);
});

test.serial(`should request and handle a greeting locally using a JWT Auth Token`, async (t) => {
const PORT = 50051;
const server = childProcess.exec(`${serverCmd} -p ${PORT}`, { cwd: cwd });

await delay(1000);
const clientOutput = await tools.runAsync(`${clientCmd} -h localhost:${PORT} -j ${JWT_AUTH_TOKEN}`, cwd);
t.regex(clientOutput, /Hello world/);
server.kill();
});

// Misc
test('should require either an API key or a JWT Auth Token', async (t) => {
await t.throws(
tools.runAsync(`${clientCmd} -h ${GCE_HOST}`, cwd),
/One of API_KEY or JWT_AUTH_TOKEN must be set/
);
});