Skip to content

Commit edae294

Browse files
authored
[Cloud Run] Identity Platform + Cloud SQL sample (#1984)
1 parent ce1bde3 commit edae294

25 files changed

+1351
-0
lines changed

.kokoro/build-with-run.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,13 @@ if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"release"* ]]; then
8383
trap notify_buildcop EXIT HUP
8484
fi
8585

86+
# Configure Cloud SQL variables for deploying idp-sql sample
87+
export DB_NAME="kokoro_ci"
88+
export DB_USER="kokoro_ci"
89+
export DB_PASSWORD=$(cat $KOKORO_GFILE_DIR/secrets-sql-password.txt)
90+
export CLOUD_SQL_CONNECTION_NAME=$(cat $KOKORO_GFILE_DIR/secrets-pg-connection-name.txt)
91+
92+
export IDP_KEY=$(gcloud secrets versions access latest --secret="nodejs-docs-samples-idp-key" --project="${GOOGLE_CLOUD_PROJECT}")
93+
8694
npm test
8795
npm run --if-present system-test

.kokoro/run/idp-sql.cfg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Format: //devtools/kokoro/config/proto/build.proto
2+
3+
# Set the folder in which the tests are run
4+
env_vars: {
5+
key: "PROJECT"
6+
value: "run/idp-sql"
7+
}

run/idp-sql/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

run/idp-sql/Dockerfile

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2020 Google LLC. All rights reserved.
2+
# Use of this source code is governed by the Apache 2.0
3+
# license that can be found in the LICENSE file.
4+
5+
# Use the official lightweight Node.js 10 image.
6+
# https://hub.docker.com/_/node
7+
FROM node:12-slim
8+
9+
# Create and change to the app directory.
10+
WORKDIR /usr/src/app
11+
12+
# Copy application dependency manifests to the container image.
13+
# A wildcard is used to ensure both package.json AND package-lock.json are copied.
14+
# Copying this separately prevents re-running npm install on every code change.
15+
COPY package*.json ./
16+
17+
# Install dependencies.
18+
# If you add a package-lock.json speed your build by switching to 'npm ci'.
19+
# RUN npm ci --only=production
20+
RUN npm install --production
21+
22+
# Copy local code to the container image.
23+
COPY . ./
24+
25+
# Run the web service on container startup.
26+
CMD [ "node", "index.js" ]

run/idp-sql/README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Cloud Run End User Authentication with PostgreSQL Database Sample
2+
3+
This sample integrates with the Identity Platform to authenticate users to the
4+
application and connects to a Cloud SQL postgreSQL database for data storage.
5+
6+
Use it with the [End user Authentication for Cloud Run](http://cloud.google.com/run/docs/tutorials/identity-platform).
7+
8+
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/tree/master/run).
9+
10+
[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run)
11+
12+
## Dependencies
13+
14+
* **express**: Web server framework
15+
* **winston**: Logging library
16+
* **@google-cloud/secret-manager**: Google Secret Manager client library
17+
* **firebase-admin**: Verifying JWT token
18+
* **knex** + **pg**: A postgreSQL query builder library
19+
* **handlebars.js**: Template engine
20+
* **google-auth-library-nodejs**: Access [compute metadata server](https://cloud.google.com/compute/docs/storing-retrieving-metadata) for project ID
21+
* **Firebase JavaScript SDK**: client-side library for authentication flow
22+
23+
## Environment Variables
24+
25+
Cloud Run services can be [configured with Environment Variables](https://cloud.google.com/run/docs/configuring/environment-variables).
26+
Required variables for this sample include:
27+
28+
* `CLOUD_SQL_CREDENTIALS_SECRET`: the resource ID of the secret, in format: `projects/PROJECT_ID/secrets/SECRET_ID/versions/VERSION`. See [postgres-secrets.json](postgres-secrets.json) for secret content.
29+
30+
OR
31+
32+
* `CLOUD_SQL_CONNECTION_NAME`: Cloud SQL instance name, in format: `<MY-PROJECT>:<INSTANCE-REGION>:<MY-DATABASE>`
33+
* `DB_NAME`: Cloud SQL postgreSQL database name
34+
* `DB_USER`: database user
35+
* `DB_PASSWORD`: database password
36+
37+
Other environment variables:
38+
39+
* Set `TABLE` to change the postgreSQL database table name.
40+
41+
* Set `DB_HOST` to use the proxy with TCP. See instructions below.
42+
43+
* Set `DB_SOCKET_PATH` to change the directory when using the proxy with Unix sockets.
44+
See instructions below.
45+
46+
## Production Considerations
47+
48+
* Both `postgres-secrets.json` and `static/config.js` should not be committed to
49+
a git repository and should be added to `.gitignore`.
50+
51+
* Saving credentials directly as environment variables is convenient for local testing,
52+
but not secure for production; therefore using `CLOUD_SQL_CREDENTIALS_SECRET`
53+
in combination with the Cloud Secrets Manager is recommended.
54+
55+
## Running Locally
56+
57+
1. Set [environment variables](#environment-variables).
58+
59+
1. To run this application locally, download and install the `cloud_sql_proxy` by
60+
[following the instructions](https://cloud.google.com/sql/docs/postgres/sql-proxy#install).
61+
62+
The proxy can be used with a TCP connection or a Unix Domain Socket. On Linux or
63+
Mac OS you can use either option, but on Windows the proxy currently requires a TCP
64+
connection.
65+
66+
* [Instructions to launch proxy with Unix Domain Socket](../../cloud-sql/postgres/knex#launch-proxy-with-unix-domain-socket)
67+
68+
* [Instructions to launch proxy with TCP](../../cloud-sql/postgres/knex#launch-proxy-with-tcp)
69+
70+
## Testing
71+
72+
Tests expect the Cloud SQL instance to already be created and environment Variables
73+
to be set.
74+
75+
### Unit tests
76+
77+
```
78+
npm run test
79+
```
80+
81+
### System tests
82+
83+
```
84+
npm run system-test
85+
```

run/idp-sql/app.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const { getVotes, getVoteCount, insertVote } = require('./cloud-sql');
18+
const express = require('express');
19+
const { buildRenderedHtml } = require('./handlebars');
20+
const { authenticateJWT, requestLogger } = require('./middleware');
21+
22+
const app = express();
23+
app.use(express.static(__dirname + '/static'));
24+
25+
// Automatically parse request body as form data.
26+
app.use(express.urlencoded({extended: false}));
27+
app.use(express.json());
28+
29+
// Set Content-Type for all responses for these routes.
30+
app.use((req, res, next) => {
31+
res.set('Content-Type', 'text/html');
32+
next();
33+
});
34+
35+
app.get('/', requestLogger, async (req, res) => {
36+
try {
37+
// Query the total count of "CATS" from the database.
38+
const catsResult = await getVoteCount('CATS');
39+
const catsTotalVotes = parseInt(catsResult[0].count);
40+
// Query the total count of "DOGS" from the database.
41+
const dogsResult = await getVoteCount('DOGS');
42+
const dogsTotalVotes = parseInt(dogsResult[0].count);
43+
// Query the last 5 votes from the database.
44+
const votes = await getVotes();
45+
// Calculate and set leader values.
46+
let leadTeam = '';
47+
let voteDiff = 0;
48+
let leaderMessage = '';
49+
if (catsTotalVotes !== dogsTotalVotes) {
50+
if (catsTotalVotes > dogsTotalVotes) {
51+
leadTeam = 'CATS';
52+
voteDiff = catsTotalVotes - dogsTotalVotes;
53+
} else {
54+
leadTeam = 'DOGS';
55+
voteDiff = dogsTotalVotes - catsTotalVotes;
56+
}
57+
leaderMessage = `${leadTeam} are winning by ${voteDiff} vote${voteDiff > 1 ? 's' : ''}.`;
58+
} else {
59+
leaderMessage = 'CATS and DOGS are evenly matched!';
60+
}
61+
62+
// Add variables to Handlebars.js template
63+
const renderedHtml = await buildRenderedHtml({
64+
votes: votes,
65+
catsCount: catsTotalVotes,
66+
dogsCount: dogsTotalVotes,
67+
leadTeam: leadTeam,
68+
voteDiff: voteDiff,
69+
leaderMessage: leaderMessage,
70+
});
71+
res.status(200).send(renderedHtml);
72+
} catch (err) {
73+
const message = "Error while connecting to the Cloud SQL database. " +
74+
"Check that your username and password are correct, that the Cloud SQL " +
75+
"proxy is running (locally), and that the database/table exists and is " +
76+
`ready for use: ${err}`;
77+
req.logger.error(message); // request-based logger with trace support
78+
res
79+
.status(500)
80+
.send('Unable to load page; see logs for more details.')
81+
.end();
82+
}
83+
});
84+
85+
app.post('/', requestLogger, authenticateJWT, async (req, res) => {
86+
// Get decoded Id Platform user id
87+
const uid = req.uid;
88+
// Get the team from the request and record the time of the vote.
89+
const {team} = req.body;
90+
const timestamp = new Date();
91+
92+
if (!team || (team !== 'CATS' && team !== 'DOGS')) {
93+
res.status(400).send('Invalid team specified.').end();
94+
return;
95+
}
96+
97+
// Create a vote record to be stored in the database.
98+
const vote = {
99+
candidate: team,
100+
time_cast: timestamp,
101+
uid,
102+
};
103+
104+
// Save the data to the database.
105+
try {
106+
await insertVote(vote);
107+
req.logger.info({message: 'vote_inserted', vote}); // request-based logger with trace support
108+
} catch (err) {
109+
req.logger.error(`Error while attempting to submit vote: ${err}`);
110+
res
111+
.status(500)
112+
.send('Unable to cast vote; see logs for more details.')
113+
.end();
114+
return;
115+
}
116+
res.status(200).send(`Successfully voted for ${team} at ${timestamp}`).end();
117+
});
118+
119+
module.exports = app;

run/idp-sql/app.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "idp-sql",
3+
"env": {
4+
"DB_PASSWORD": {
5+
"description": "postgreSQL password for root user"
6+
},
7+
"CLOUD_SQL_INSTANCE_NAME": {
8+
"description": "Cloud SQL instance name",
9+
"value": "idp-sql-instance"
10+
},
11+
"API_KEY": {
12+
"description": "Identity Platform API key from Application Setup Details"
13+
}
14+
},
15+
"hooks": {
16+
"precreate": {
17+
"commands": [
18+
"./setup.sh"
19+
]
20+
},
21+
"postcreate": {
22+
"commands": [
23+
"./postcreate.sh"
24+
]
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)