Skip to content

[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

Merged
merged 40 commits into from
Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a5f92fb
App Drafted
averikitsch Sep 16, 2020
c41ff9a
Rename
averikitsch Sep 24, 2020
012918f
updates
averikitsch Sep 25, 2020
f262529
minor style fixes
averikitsch Sep 25, 2020
18c0063
Merge branch 'master' into cats-v-dogs
averikitsch Sep 25, 2020
5e2363d
Merge branch 'master' into cats-v-dogs
averikitsch Sep 25, 2020
e9c54de
Updates
averikitsch Oct 1, 2020
816a95a
Update 2
averikitsch Oct 1, 2020
4cc2a23
Add more tests and updates
averikitsch Oct 1, 2020
7e44fe7
Merge branch 'cats-v-dogs' of https://github.com/GoogleCloudPlatform/…
averikitsch Oct 1, 2020
08eb839
Comment updates
averikitsch Oct 5, 2020
c04234e
Tests updated
averikitsch Oct 6, 2020
bd25555
Add createTable
averikitsch Oct 6, 2020
fffabc2
Add scripts
averikitsch Oct 7, 2020
3718c26
secret manager clean up
averikitsch Oct 7, 2020
b17cf63
Add Kokoro set up
averikitsch Oct 7, 2020
8af2289
Remove trace agent
averikitsch Oct 8, 2020
1699e48
Merge branch 'master' into cats-v-dogs
fhinkel Oct 8, 2020
e039444
Merge branch 'master' into cats-v-dogs
averikitsch Oct 8, 2020
b7ccc59
Update tests
averikitsch Oct 8, 2020
7ac9172
Merge branch 'cats-v-dogs' of https://github.com/GoogleCloudPlatform/…
averikitsch Oct 8, 2020
b6b3d84
Add env vars
averikitsch Oct 8, 2020
b5194ed
Update kokoro
averikitsch Oct 8, 2020
86a74d4
Add and drop table for unit tests
averikitsch Oct 8, 2020
60d5254
Add debugging
averikitsch Oct 8, 2020
1bdc6e0
Update README
averikitsch Oct 8, 2020
eff522c
PR comments
averikitsch Oct 13, 2020
eee77a1
Restructure startup
averikitsch Oct 15, 2020
9777c17
Update retry
averikitsch Oct 15, 2020
11bd6ec
Update testing
averikitsch Oct 15, 2020
71a7bda
add more tests
averikitsch Oct 15, 2020
4e4295d
Small updates
averikitsch Oct 19, 2020
1091913
Update CR button and cloud sql create table
averikitsch Oct 20, 2020
5577713
Merge branch 'master' into cats-v-dogs
averikitsch Oct 20, 2020
072391e
fix tests
averikitsch Oct 20, 2020
3e8b565
Merge branch 'cats-v-dogs' of https://github.com/GoogleCloudPlatform/…
averikitsch Oct 20, 2020
e90bb27
Update logging
averikitsch Oct 20, 2020
be99235
Update comment
averikitsch Oct 20, 2020
f540bbe
Add sigterm handling
averikitsch Oct 21, 2020
3a4b161
Update log
averikitsch Oct 21, 2020
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
15 changes: 15 additions & 0 deletions .kokoro/build-with-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,20 @@ if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"release"* ]]; then
trap notify_buildcop EXIT HUP
fi


# Download and run the proxy if testing a Cloud SQL sample
if [[ $SQL_CLIENT ]]; then
# Configure Cloud SQL variables
export DB_NAME="kokoro_ci"
export DB_USER="kokoro_ci"
export DB_PASSWORD=$(cat $KOKORO_GFILE_DIR/secrets-sql-password.txt)
export CLOUD_SQL_CONNECTION_NAME=$(cat $KOKORO_GFILE_DIR/secrets-pg-connection-name.txt)

wget --quiet https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy
Copy link
Collaborator

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?

Copy link
Contributor Author

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

Copy link
Collaborator

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.

Copy link
Contributor Author

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.

chmod +x cloud_sql_proxy
export DB_HOST=127.0.0.1:5432
./cloud_sql_proxy -instances="${CLOUD_SQL_CONNECTION_NAME}"=tcp:5432 &>> cloud_sql_proxy.log &
fi

npm test
npm run --if-present system-test
13 changes: 13 additions & 0 deletions .kokoro/run/idp-sql.cfg
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"
}
1 change: 1 addition & 0 deletions run/idp-sql/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
26 changes: 26 additions & 0 deletions run/idp-sql/Dockerfile
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" ]
85 changes: 85 additions & 0 deletions run/idp-sql/README.md
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).

[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run)

## Dependencies

* **express**: Web server framework
* **winston**: Logging library
* **@google-cloud/secret-manager**: Google Secret Manager client library
* **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
* **Firebase JavaScript SDK**: client-side library for authentication flow

## 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
```
124 changes: 124 additions & 0 deletions run/idp-sql/app.js
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');
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})
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
19 changes: 19 additions & 0 deletions run/idp-sql/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"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"
]
}
}
}
64 changes: 64 additions & 0 deletions run/idp-sql/cloud-run-button-script.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright 2020 Google LLC
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: script provisions infrastructure when running cloud run button

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 setup.sh

#
# 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
Loading