-
Notifications
You must be signed in to change notification settings - Fork 2k
[Cloud Run] Identity Platform + Cloud SQL sample #1984
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
Changes from 27 commits
a5f92fb
c41ff9a
012918f
f262529
18c0063
5e2363d
e9c54de
816a95a
4cc2a23
7e44fe7
08eb839
c04234e
bd25555
fffabc2
3718c26
b17cf63
8af2289
1699e48
e039444
b7ccc59
7ac9172
b6b3d84
b5194ed
86a74d4
60d5254
1bdc6e0
eff522c
eee77a1
9777c17
11bd6ec
71a7bda
4e4295d
1091913
5577713
072391e
3e8b565
e90bb27
be99235
f540bbe
3a4b161
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# Format: //devtools/kokoro/config/proto/build.proto | ||
|
||
# Set the folder in which the tests are run | ||
env_vars: { | ||
key: "PROJECT" | ||
value: "run/idp-sql" | ||
} | ||
|
||
# Specify Cloud SQL set up | ||
env_vars: { | ||
key: "SQL_CLIENT" | ||
value: "pg" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Copyright 2020 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. | ||
|
||
# Use the official lightweight Node.js 10 image. | ||
# https://hub.docker.com/_/node | ||
FROM node:12-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 both package.json AND package-lock.json are copied. | ||
# Copying this separately prevents re-running npm install on every code change. | ||
COPY package*.json ./ | ||
|
||
# Install dependencies. | ||
# If you add a package-lock.json speed your build by switching to 'npm ci'. | ||
# RUN npm ci --only=production | ||
RUN npm install --production | ||
|
||
# Copy local code to the container image. | ||
COPY . ./ | ||
|
||
# Run the web service on container startup. | ||
CMD [ "node", "index.js" ] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# Cloud Run End User Authentication with PostgreSQL Database Sample | ||
|
||
This sample integrates with the Identity Platform to authenticate users to the | ||
application and connects to a Cloud SQL postgreSQL database for data storage. | ||
|
||
Use it with the [End user Authentication for Cloud Run](http://cloud.google.com/run/docs/tutorials/identity-platform). | ||
|
||
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). | ||
|
||
[](https://deploy.cloud.run) | ||
|
||
## Dependencies | ||
|
||
* **express**: Web server framework | ||
* **winston**: Logging library | ||
* **@google-cloud/secret-manager**: Google Secret Manager client library | ||
averikitsch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* **firebase-admin**: Verifying JWT token | ||
* **knex** + **pg**: A postgreSQL query builder library | ||
* **handlebars.js**: Template engine | ||
* **google-auth-library-nodejs**: Access [compute metadata server](https://cloud.google.com/compute/docs/storing-retrieving-metadata) for project ID | ||
grayside marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* **Firebase JavaScript SDK**: client-side library for authentication flow | ||
|
||
averikitsch marked this conversation as resolved.
Show resolved
Hide resolved
averikitsch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
## Environment Variables | ||
|
||
Cloud Run services can be [configured with Environment Variables](https://cloud.google.com/run/docs/configuring/environment-variables). | ||
Required variables for this sample include: | ||
|
||
* `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. | ||
|
||
OR | ||
|
||
* `CLOUD_SQL_CONNECTION_NAME`: Cloud SQL instance name, in format: `<MY-PROJECT>:<INSTANCE-REGION>:<MY-DATABASE>` | ||
* `DB_NAME`: Cloud SQL postgreSQL database name | ||
* `DB_USER`: database user | ||
* `DB_PASSWORD`: database password | ||
|
||
Other environment variables: | ||
|
||
* Set `TABLE` to change the postgreSQL database table name. | ||
|
||
* Set `DB_HOST` to use the proxy with TCP. See instructions below. | ||
|
||
* Set `DB_SOCKET_PATH` to change the directory when using the proxy with Unix sockets. | ||
See instructions below. | ||
|
||
## Production Considerations | ||
|
||
* Both `postgres-secrets.json` and `static/config.js` should not be committed to | ||
a git repository and should be added to `.gitignore`. | ||
|
||
* Saving credentials directly as environment variables is convenient for local testing, | ||
but not secure for production; therefore using `CLOUD_SQL_CREDENTIALS_SECRET` | ||
in combination with the Cloud Secrets Manager is recommended. | ||
|
||
## Running Locally | ||
|
||
1. Set [environment variables](#environment-variables). | ||
|
||
1. To run this application locally, download and install the `cloud_sql_proxy` by | ||
[following the instructions](https://cloud.google.com/sql/docs/postgres/sql-proxy#install). | ||
|
||
The proxy can be used with a TCP connection or a Unix Domain Socket. On Linux or | ||
Mac OS you can use either option, but on Windows the proxy currently requires a TCP | ||
connection. | ||
|
||
* [Instructions to launch proxy with Unix Domain Socket](../../cloud-sql/postgres/knex#launch-proxy-with-unix-domain-socket) | ||
|
||
* [Instructions to launch proxy with TCP](../../cloud-sql/postgres/knex#launch-proxy-with-tcp) | ||
|
||
## Testing | ||
|
||
Tests expect the Cloud SQL instance to already be created and environment Variables | ||
to be set. | ||
|
||
### Unit tests | ||
|
||
``` | ||
npm run test | ||
``` | ||
|
||
### System tests | ||
|
||
``` | ||
npm run system-test | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
// Copyright 2020 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. | ||
|
||
'use strict'; | ||
|
||
const { getVotes, getVoteCount, insertVote } = require('./cloud-sql'); | ||
const express = require('express'); | ||
const admin = require('firebase-admin'); | ||
averikitsch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const { buildRenderedHtml } = require('./handlebars'); | ||
const { logger } = require('./logging'); | ||
const { authenticateJWT, getTrace } = require('./middleware'); | ||
|
||
const app = express(); | ||
app.use(express.static(__dirname + '/static')); | ||
|
||
// Automatically parse request body as form data. | ||
app.use(express.urlencoded({extended: false})); | ||
app.use(express.json()); | ||
|
||
// Set Content-Type for all responses for these routes. | ||
app.use((req, res, next) => { | ||
res.set('Content-Type', 'text/html'); | ||
next(); | ||
}); | ||
|
||
// Initialize Firebase Admin SDK | ||
admin.initializeApp(); | ||
|
||
app.get('/', getTrace, async (req, res) => { | ||
try { | ||
// Query the total count of "CATS" from the database. | ||
const catsResult = await getVoteCount('CATS'); | ||
const catsTotalVotes = parseInt(catsResult[0].count); | ||
// Query the total count of "DOGS" from the database. | ||
const dogsResult = await getVoteCount('DOGS'); | ||
const dogsTotalVotes = parseInt(dogsResult[0].count); | ||
// Query the last 5 votes from the database. | ||
const votes = await getVotes(); | ||
// Calculate and set leader values. | ||
let leadTeam = ''; | ||
let voteDiff = 0; | ||
let leaderMessage = ''; | ||
if (catsTotalVotes !== dogsTotalVotes) { | ||
if (catsTotalVotes > dogsTotalVotes) { | ||
leadTeam = 'CATS'; | ||
voteDiff = catsTotalVotes - dogsTotalVotes; | ||
} else { | ||
leadTeam = 'DOGS'; | ||
voteDiff = dogsTotalVotes - catsTotalVotes; | ||
} | ||
leaderMessage = `${leadTeam} are winning by ${voteDiff} vote${voteDiff > 1 ? 's' : ''}.`; | ||
} else { | ||
leaderMessage = 'CATS and DOGS are evenly matched!'; | ||
} | ||
|
||
// Add variables to Handlebars.js template | ||
const renderedHtml = await buildRenderedHtml({ | ||
votes: votes, | ||
catsCount: catsTotalVotes, | ||
dogsCount: dogsTotalVotes, | ||
leadTeam: leadTeam, | ||
voteDiff: voteDiff, | ||
leaderMessage: leaderMessage, | ||
}); | ||
res.status(200).send(renderedHtml); | ||
} catch (err) { | ||
const message = "Error while connecting to the Cloud SQL database. " + | ||
"Check that your username and password are correct, that the Cloud SQL " + | ||
"proxy is running (locally), and that the database/table exists and is " + | ||
`ready for use: ${err}`; | ||
logger.error({message, traceId: req.traceId}); | ||
res | ||
.status(500) | ||
.send('Unable to load page; see logs for more details.') | ||
.end(); | ||
} | ||
}); | ||
|
||
app.post('/', getTrace, authenticateJWT, async (req, res) => { | ||
// Get decoded Id Platform user id | ||
const uid = req.uid; | ||
// Get the team from the request and record the time of the vote. | ||
const {team} = req.body; | ||
const timestamp = new Date(); | ||
|
||
if (!team || (team !== 'CATS' && team !== 'DOGS')) { | ||
res.status(400).send('Invalid team specified.').end(); | ||
return; | ||
} | ||
|
||
// Create a vote record to be stored in the database. | ||
const vote = { | ||
candidate: team, | ||
time_cast: timestamp, | ||
uid, | ||
}; | ||
|
||
// Save the data to the database. | ||
try { | ||
await insertVote(vote); | ||
logger.info({message: 'vote_inserted', vote, traceId: req.traceId}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit/future: traceId here is ok, but makes the trace elements higher visibility around the code. The pattern I'd expect is for the middleware to duplicate the global logger, set request-specific defaults, and provide a request-scoped logger for this controller. Given this code is not region tagged, it's a low priority. |
||
} catch (err) { | ||
logger.error({message: `Error while attempting to submit vote: ${err}`, traceId: req.traceId}); | ||
res | ||
.status(500) | ||
.send('Unable to cast vote; see logs for more details.') | ||
.end(); | ||
return; | ||
} | ||
res.status(200).send(`Successfully voted for ${team} at ${timestamp}`).end(); | ||
}); | ||
|
||
module.exports = app; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
averikitsch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"name": "idp-sql", | ||
"env": { | ||
"DB_PASSWORD": { | ||
"description": "postgreSQL password" | ||
}, | ||
"CLOUD_SQL_INSTANCE_NAME": { | ||
"description": "Cloud SQL instance name", | ||
"value": "vote-sql-instance" | ||
} | ||
}, | ||
"hooks": { | ||
"postcreate": { | ||
"commands": [ | ||
"./cloud-run-button-script.sh" | ||
grayside marked this conversation as resolved.
Show resolved
Hide resolved
|
||
] | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# Copyright 2020 Google LLC | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: script provisions infrastructure when running cloud run button There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this script only usable in the context of cloud run button? If not, maybe it should have a name like |
||
# | ||
# 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. | ||
|
||
export SECRET_NAME="idp-sql-secrets" | ||
export SERVICE_ACCOUNT="idp-sql-indentity" | ||
|
||
gcloud config set project $GOOGLE_CLOUD_PROJECT | ||
|
||
# Enable Cloud SQl and Secret Manager APIs | ||
gcloud services enable sqladmin.googleapis.com secretmanager.googleapis.com | ||
|
||
gcloud sql instances describe ${CLOUD_SQL_INSTANCE_NAME} | ||
if [ $? -eq 1 ]; then | ||
echo "Create Cloud SQL instance with postgreSQL database (this might take a few minutes)..." | ||
gcloud sql instances create ${CLOUD_SQL_INSTANCE_NAME} \ | ||
--database-version=POSTGRES_12 \ | ||
--region=${GOOGLE_CLOUD_REGION} \ | ||
--cpu=2 \ | ||
--memory=7680MB \ | ||
--root-password=${DB_PASSWORD} | ||
fi | ||
|
||
sed -i "s/PROJECT_ID/$GOOGLE_CLOUD_PROJECT/" postgres-secrets.json | ||
sed -i "s/REGION/$GOOGLE_CLOUD_REGION/" postgres-secrets.json | ||
sed -i "s/PASSWORD_SECRET/$DB_PASSWORD/" postgres-secrets.json | ||
sed -i "s/INSTANCE/$CLOUD_SQL_INSTANCE_NAME/" postgres-secrets.json | ||
|
||
gcloud secrets describe ${SECRET_NAME} | ||
if [ $? -eq 1 ]; then | ||
echo "Creating secret ..." | ||
gcloud secrets create ${SECRET_NAME} \ | ||
--replication-policy="automatic" | ||
fi | ||
echo "Adding secret version ..." | ||
gcloud secrets versions add ${SECRET_NAME} --data-file=postgres-secrets.json | ||
|
||
# Create service account | ||
gcloud iam service-accounts create ${SERVICE_ACCOUNT} | ||
# Allow service account to access secret | ||
gcloud secrets add-iam-policy-binding ${SECRET_NAME} \ | ||
--member serviceAccount:${SERVICE_ACCOUNT}@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com \ | ||
--role roles/secretmanager.secretAccessor | ||
# Allow service account to access Cloud SQL | ||
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \ | ||
--member serviceAccount:${SERVICE_ACCOUNT}@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com \ | ||
--role roles/cloudsql.client | ||
|
||
gcloud run services update ${K_SERVICE} \ | ||
--platform managed \ | ||
--region ${GOOGLE_CLOUD_REGION} \ | ||
--service-account ${SERVICE_ACCOUNT}@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com \ | ||
--add-cloudsql-instances ${GOOGLE_CLOUD_PROJECT}:${GOOGLE_CLOUD_REGION}:${CLOUD_SQL_INSTANCE_NAME} \ | ||
--update-env-vars CLOUD_SQL_CREDENTIALS_SECRET=projects/${GOOGLE_CLOUD_PROJECT}/secrets/${SECRET_NAME}/versions/latest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great to have this capability for integration tests.
future consideration: Can we mock the database interactions for unit tests and leave database related tests to end-to-end?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is needed for the e2e tests to return status 200
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An end-to-end test of an app should not require local access to the database. System/integration tests where you run database interaction code locally might, but I think directly hitting the endpoints in a deployed service would be a better use of testing time if we can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was not thinking when I wrote this response 🤦🏼♀️. You are right, this is for the unit tests to respond correctly.