Skip to content

Commit

Permalink
Add Endpoints gRPC + JWT sample (#419)
Browse files Browse the repository at this point in the history
* Add Endpoints gRPC + JWT sample

* Report parameter error through yargs instead of throwing it

* Fix lint

* Address comments

* Address jeffmendoza's comments
  • Loading branch information
Ace Nassri authored Jul 10, 2017
1 parent f6d008d commit b6eb5ec
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 31 deletions.
72 changes: 56 additions & 16 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]
```
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,50 @@ $ 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).
```
$ node client.js -h [YOUR_CLUSTER_IP_ADDRESS]:80 -k [YOUR_API_KEY]
```
### Testing your service
You can use the included client to test your Endpoints deployment.

1. Determine your service's IP address.

* If your service is hosted on Compute Engine, this will be your _instance's_ external IP address.

* If your service is hosted on Container Engine, this will be your _service's_ external IP address.

2. Run the client to connect to your service. When running the following commands, replace `[YOUR_IP_ADDRESS]` with the IP address you found in Step 1.

* 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 -j [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/
);
});

0 comments on commit b6eb5ec

Please sign in to comment.