From 6e0adde1f6dece1612229f17fb5c082dd9d54f18 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Tue, 24 Mar 2020 00:01:14 -0700 Subject: [PATCH 01/16] wrote Python sample for SQL Server and corresponding readme --- cloud-sql/sql-server/sqlalchemy/.gitignore | 2 + cloud-sql/sql-server/sqlalchemy/README.md | 81 ++++++++ cloud-sql/sql-server/sqlalchemy/app.yaml | 23 +++ cloud-sql/sql-server/sqlalchemy/main.py | 180 ++++++++++++++++++ .../sql-server/sqlalchemy/requirements.txt | 4 + .../sqlalchemy/templates/index.html | 100 ++++++++++ 6 files changed, 390 insertions(+) create mode 100644 cloud-sql/sql-server/sqlalchemy/.gitignore create mode 100644 cloud-sql/sql-server/sqlalchemy/README.md create mode 100644 cloud-sql/sql-server/sqlalchemy/app.yaml create mode 100644 cloud-sql/sql-server/sqlalchemy/main.py create mode 100644 cloud-sql/sql-server/sqlalchemy/requirements.txt create mode 100644 cloud-sql/sql-server/sqlalchemy/templates/index.html diff --git a/cloud-sql/sql-server/sqlalchemy/.gitignore b/cloud-sql/sql-server/sqlalchemy/.gitignore new file mode 100644 index 00000000000..f07124031ac --- /dev/null +++ b/cloud-sql/sql-server/sqlalchemy/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +.pytest_cache \ No newline at end of file diff --git a/cloud-sql/sql-server/sqlalchemy/README.md b/cloud-sql/sql-server/sqlalchemy/README.md new file mode 100644 index 00000000000..4036122bb21 --- /dev/null +++ b/cloud-sql/sql-server/sqlalchemy/README.md @@ -0,0 +1,81 @@ +# Connecting to Cloud SQL - SQL Server + +## Before you begin + +1. If you haven't already, set up a Python Development Environment by following the [python setup guide](https://cloud.google.com/python/setup) and +[create a project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project). + +1. [Create a Google Cloud SQL "SQL Server" instance]( + https://console.cloud.google.com/sql/choose-instance-engine). + +6. Under the instance's "USERS" tab, create a new user. Note the "User name" and "Password". + +7. Create a new database in your Google Cloud SQL instance. + + 1. List your database instances in [Cloud Cloud Console]( + https://console.cloud.google.com/sql/instances/). + + 2. Click your Instance Id to see Instance details. + + 3. Click DATABASES. + + 4. Click **Create database**. + + 2. For **Database name**, enter `votes`. + + 3. Click **CREATE**. + +1. Install the version of [Microsoft ODBC 17 Driver for SQL Server](https://docs.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server?view=sql-server-ver15_) for your operating system. + +1. Create a service account with the 'Cloud SQL Client' permissions by following these +[instructions](https://cloud.google.com/sql/docs/postgres/connect-external-app#4_if_required_by_your_authentication_method_create_a_service_account). +Download a JSON key to use to authenticate your connection. + +1. Use the information noted in the previous steps: +```bash +export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service/account/key.json +export CLOUD_SQL_CONNECTION_NAME='::' +export DB_USER='my-db-user' +export DB_PASS='my-db-pass' +export DB_NAME='my_db' +``` +Note: Saving credentials in environment variables is convenient, but not secure - consider a more +secure solution such as [Cloud KMS](https://cloud.google.com/kms/) to help keep secrets safe. + +## Running locally + +To run this application locally, download and install the `cloud_sql_proxy` by +following the instructions [here](https://cloud.google.com/sql/docs/mysql/sql-proxy#install). + +Then, use the following command to start the proxy in the +background using TCP: +```bash +./cloud_sql_proxy -instances=${CLOUD_SQL_CONNECTION_NAME}=tcp:1433 sqlserver -u ${DB_USER} --host 127.0.0.1 +``` + +Next, setup install the requirements into a virtual enviroment: +```bash +virtualenv --python python3 env +source env/bin/activate +pip install -r requirements.txt +``` + +Finally, start the application: +```bash +python main.py +``` + +Navigate towards `http://127.0.0.1:8080` to verify your application is running correctly. + +## Deploy to App Engine Flexible + +App Engine Flexible supports connecting to your SQL Server instance through TCP + +First, update `app.yaml` with the correct values to pass the environment +variables and instance name into the runtime. + +Next, the following command will deploy the application to your Google Cloud project: +```bash +gcloud beta app deploy +``` + diff --git a/cloud-sql/sql-server/sqlalchemy/app.yaml b/cloud-sql/sql-server/sqlalchemy/app.yaml new file mode 100644 index 00000000000..600d8f9d475 --- /dev/null +++ b/cloud-sql/sql-server/sqlalchemy/app.yaml @@ -0,0 +1,23 @@ +# Copyright 2018 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. + +runtime: python37 + +# Remember - storing secrets in plaintext is potentially unsafe. Consider using +# something like https://cloud.google.com/kms/ to help keep secrets secret. +env_variables: + CLOUD_SQL_CONNECTION_NAME: :: + DB_USER: my-db-user + DB_PASS: my-db-pass + DB_NAME: my_db diff --git a/cloud-sql/sql-server/sqlalchemy/main.py b/cloud-sql/sql-server/sqlalchemy/main.py new file mode 100644 index 00000000000..8aa42bc202e --- /dev/null +++ b/cloud-sql/sql-server/sqlalchemy/main.py @@ -0,0 +1,180 @@ +# Copyright 2018 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. + +import datetime +import logging +import os + +from flask import Flask, render_template, request, Response +import sqlalchemy +from sqlalchemy import MetaData +from sqlalchemy import Table +from sqlalchemy import Column +from sqlalchemy import Integer, String, DateTime + + +# Remember - storing secrets in plaintext is potentially unsafe. Consider using +# something like https://cloud.google.com/kms/ to help keep secrets secret. +db_user = os.environ.get("DB_USER") +db_pass = os.environ.get("DB_PASS") +db_name = os.environ.get("DB_NAME") +cloud_sql_connection_name = os.environ.get("CLOUD_SQL_CONNECTION_NAME") + +app = Flask(__name__) + +logger = logging.getLogger() + +# [START cloud_sql_postgres_sqlalchemy_create] +# The SQLAlchemy engine will help manage interactions, including automatically +# managing a pool of connections to your database +db = sqlalchemy.create_engine( + # Equivalent URL: + # mssql+pyodbc://:@/?unix_sock=/cloudsql/ + sqlalchemy.engine.url.URL( + 'mssql+pyodbc', + username=db_user, + password=db_pass, + database=db_name, + host='127.0.0.1', + port=1433, + query={"driver":"ODBC Driver 17 for SQL Server"} + ), + # ... Specify additional properties here. + # [START_EXCLUDE] + + # [START cloud_sql_postgres_sqlalchemy_limit] + # Pool size is the maximum number of permanent connections to keep. + pool_size=5, + # Temporarily exceeds the set pool_size if no connections are available. + max_overflow=2, + # The total number of concurrent connections for your application will be + # a total of pool_size and max_overflow. + # [END cloud_sql_postgres_sqlalchemy_limit] + + # [START cloud_sql_postgres_sqlalchemy_backoff] + # SQLAlchemy automatically uses delays between failed connection attempts, + # but provides no arguments for configuration. + # [END cloud_sql_postgres_sqlalchemy_backoff] + + # [START cloud_sql_postgres_sqlalchemy_timeout] + # 'pool_timeout' is the maximum number of seconds to wait when retrieving a + # new connection from the pool. After the specified amount of time, an + # exception will be thrown. + pool_timeout=30, # 30 seconds + # [END cloud_sql_postgres_sqlalchemy_timeout] + + # [START cloud_sql_postgres_sqlalchemy_lifetime] + # 'pool_recycle' is the maximum number of seconds a connection can persist. + # Connections that live longer than the specified amount of time will be + # reestablished + pool_recycle=1800, # 30 minutes + # [END cloud_sql_postgres_sqlalchemy_lifetime] + echo=True #debug + # [END_EXCLUDE] +) +# [END cloud_sql_postgres_sqlalchemy_create] + + +@app.before_first_request +def create_tables(): + # Create tables (if they don't already exist) + if not db.has_table('votes'): + metadata = sqlalchemy.MetaData(db) + table = Table('votes', metadata, + Column('vote_id', Integer, primary_key=True, nullable=False), + Column('time_cast', DateTime, nullable=False), + Column('candidate', String(6), nullable=False), + ) + metadata.create_all() + + + +@app.route('/', methods=['GET']) +def index(): + votes = [] + with db.connect() as conn: + # Execute the query and fetch all results + recent_votes = conn.execute( + "SELECT TOP(5) candidate, time_cast FROM votes ORDER BY time_cast DESC" + ).fetchall() + # Convert the results into a list of dicts representing votes + for row in recent_votes: + votes.append({ + 'candidate': row[0], + 'time_cast': row[1] + }) + + stmt = sqlalchemy.text( + "SELECT COUNT(vote_id) FROM votes WHERE candidate=:candidate") + # Count number of votes for tabs + tab_result = conn.execute(stmt, candidate="TABS").fetchone() + tab_count = tab_result[0] + # Count number of votes for spaces + space_result = conn.execute(stmt, candidate="SPACES").fetchone() + space_count = space_result[0] + + return render_template( + 'index.html', + recent_votes=votes, + tab_count=tab_count, + space_count=space_count + ) + + +@app.route('/', methods=['POST']) +def save_vote(): + # Get the team and time the vote was cast. + team = request.form['team'] + time_cast = datetime.datetime.utcnow() + # Verify that the team is one of the allowed options + if team != "TABS" and team != "SPACES": + logger.warning(team) + return Response( + response="Invalid team specified.", + status=400 + ) + + # [START cloud_sql_postgres_sqlalchemy_connection] + # Preparing a statement before hand can help protect against injections. + stmt = sqlalchemy.text( + "INSERT INTO votes (time_cast, candidate)" + " VALUES (:time_cast, :candidate)" + ) + try: + # Using a with statement ensures that the connection is always released + # back into the pool at the end of statement (even if an error occurs) + with db.connect() as conn: + conn.execute(stmt, time_cast=time_cast, candidate=team) + except Exception as e: + # If something goes wrong, handle the error in this section. This might + # involve retrying or adjusting parameters depending on the situation. + # [START_EXCLUDE] + logger.exception(e) + return Response( + status=500, + response="Unable to successfully cast vote! Please check the " + "application logs for more details." + ) + # [END_EXCLUDE] + # [END cloud_sql_postgres_sqlalchemy_connection] + + return Response( + status=200, + response="Vote successfully cast for '{}' at time {}!".format( + team, time_cast) + ) + + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/cloud-sql/sql-server/sqlalchemy/requirements.txt b/cloud-sql/sql-server/sqlalchemy/requirements.txt new file mode 100644 index 00000000000..dd4016dd158 --- /dev/null +++ b/cloud-sql/sql-server/sqlalchemy/requirements.txt @@ -0,0 +1,4 @@ +Flask==1.1.1 +SQLAlchemy==1.3.13 +pyodbc==4.0.30 + diff --git a/cloud-sql/sql-server/sqlalchemy/templates/index.html b/cloud-sql/sql-server/sqlalchemy/templates/index.html new file mode 100644 index 00000000000..e390afaa5e2 --- /dev/null +++ b/cloud-sql/sql-server/sqlalchemy/templates/index.html @@ -0,0 +1,100 @@ + + + + Tabs VS Spaces + + + + + + +
+
+

+ {% if tab_count == space_count %} + TABS and SPACES are evenly matched! + {% elif tab_count > space_count %} + TABS are winning by {{tab_count - space_count}} + {{'votes' if tab_count - space_count > 1 else 'vote'}}! + {% elif space_count > tab_count %} + SPACES are winning by {{space_count - tab_count}} + {{'votes' if space_count - tab_count > 1 else 'vote'}}! + {% endif %} +

+
+
+
+
+ keyboard_tab +

{{tab_count}} votes

+ +
+
+
+
+ space_bar +

{{space_count}} votes

+ +
+
+
+

Recent Votes

+
    + {% for vote in recent_votes %} +
  • + {% if vote.candidate == "TABS" %} + keyboard_tab + {% elif vote.candidate == "SPACES" %} + space_bar + {% endif %} + + A vote for {{vote.candidate}} + +

    was cast at {{vote.time_cast}}

    +
  • + {% endfor %} +
+
+ + + From 5eb6f90179261c77e66a5893571093c95256c848 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Tue, 24 Mar 2020 13:38:47 -0700 Subject: [PATCH 02/16] configured app.yaml and Dockerfile to deploy to GAE flex --- cloud-sql/sql-server/sqlalchemy/.gcloudignore | 19 +++++ cloud-sql/sql-server/sqlalchemy/Dockerfile | 63 ++++++++++++++++ cloud-sql/sql-server/sqlalchemy/app.yaml | 14 ++-- cloud-sql/sql-server/sqlalchemy/main.py | 75 ++++++++----------- 4 files changed, 123 insertions(+), 48 deletions(-) create mode 100644 cloud-sql/sql-server/sqlalchemy/.gcloudignore create mode 100644 cloud-sql/sql-server/sqlalchemy/Dockerfile diff --git a/cloud-sql/sql-server/sqlalchemy/.gcloudignore b/cloud-sql/sql-server/sqlalchemy/.gcloudignore new file mode 100644 index 00000000000..a987f1123d8 --- /dev/null +++ b/cloud-sql/sql-server/sqlalchemy/.gcloudignore @@ -0,0 +1,19 @@ +# This file specifies files that are *not* uploaded to Google Cloud Platform +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +# Python pycache: +__pycache__/ +# Ignored by the build system +/setup.cfg \ No newline at end of file diff --git a/cloud-sql/sql-server/sqlalchemy/Dockerfile b/cloud-sql/sql-server/sqlalchemy/Dockerfile new file mode 100644 index 00000000000..f6a540486e5 --- /dev/null +++ b/cloud-sql/sql-server/sqlalchemy/Dockerfile @@ -0,0 +1,63 @@ +# Copyright 2019 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 the official Python image. +# https://hub.docker.com/_/python +FROM python:3.8-alpine + +RUN apk --no-cache add curl build-base unixodbc-dev + +#Download the desired package(s) for Microsoft ODBC 17 Driver +RUN curl -O https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.2.1-1_amd64.apk +RUN curl -O https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/mssql-tools_17.5.2.1-1_amd64.apk + + +#(Optional) Verify signature: +RUN apk add gnupg +RUN curl -O https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.2.1-1_amd64.sig +RUN curl -O https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/mssql-tools_17.5.2.1-1_amd64.sig + +RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --import - +RUN gpg --verify msodbcsql17_17.5.2.1-1_amd64.sig msodbcsql17_17.5.2.1-1_amd64.apk +RUN gpg --verify mssql-tools_17.5.2.1-1_amd64.sig mssql-tools_17.5.2.1-1_amd64.apk + + +#Install the package(s) +RUN apk add --allow-untrusted msodbcsql17_17.5.2.1-1_amd64.apk +RUN apk add --allow-untrusted mssql-tools_17.5.2.1-1_amd64.apk + +# Copy application dependency manifests to the container image. +# Copying this separately prevents re-running pip install on every code change. +COPY requirements.txt ./ + +# Install production dependencies. +RUN set -ex; \ + pip install -r requirements.txt; \ + pip install gunicorn + +# Copy local code to the container image. +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . ./ + +ENV DB_USER sqlserver +ENV DB_PASS password +ENV DB_NAME votes +ENV PROD true + +# Run the web service on container startup. Here we use the gunicorn +# webserver, with one worker process and 8 threads. +# For environments with multiple CPU cores, increase the number of workers +# to be equal to the cores available. +CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 main:app diff --git a/cloud-sql/sql-server/sqlalchemy/app.yaml b/cloud-sql/sql-server/sqlalchemy/app.yaml index 600d8f9d475..24366231380 100644 --- a/cloud-sql/sql-server/sqlalchemy/app.yaml +++ b/cloud-sql/sql-server/sqlalchemy/app.yaml @@ -12,12 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python37 +runtime: custom +env: flex # Remember - storing secrets in plaintext is potentially unsafe. Consider using # something like https://cloud.google.com/kms/ to help keep secrets secret. env_variables: - CLOUD_SQL_CONNECTION_NAME: :: - DB_USER: my-db-user - DB_PASS: my-db-pass - DB_NAME: my_db + DB_USER: + DB_PASS: + DB_NAME: votes + PROD: true + +beta_settings: + cloud_sql_instances: ::=tcp:1433 \ No newline at end of file diff --git a/cloud-sql/sql-server/sqlalchemy/main.py b/cloud-sql/sql-server/sqlalchemy/main.py index 8aa42bc202e..66147703732 100644 --- a/cloud-sql/sql-server/sqlalchemy/main.py +++ b/cloud-sql/sql-server/sqlalchemy/main.py @@ -23,36 +23,37 @@ from sqlalchemy import Column from sqlalchemy import Integer, String, DateTime +app = Flask(__name__) + +logger = logging.getLogger() # Remember - storing secrets in plaintext is potentially unsafe. Consider using # something like https://cloud.google.com/kms/ to help keep secrets secret. db_user = os.environ.get("DB_USER") db_pass = os.environ.get("DB_PASS") db_name = os.environ.get("DB_NAME") -cloud_sql_connection_name = os.environ.get("CLOUD_SQL_CONNECTION_NAME") - -app = Flask(__name__) -logger = logging.getLogger() +#If this is running in the docker container when deployed to GAE flex, use "172.17.0.1" +host = "172.17.0.1" if os.environ.get("PROD") else "127.0.0.1" # [START cloud_sql_postgres_sqlalchemy_create] # The SQLAlchemy engine will help manage interactions, including automatically # managing a pool of connections to your database + db = sqlalchemy.create_engine( # Equivalent URL: - # mssql+pyodbc://:@/?unix_sock=/cloudsql/ + # mssql+pyodbc://:@/:/?driver=ODBC+Driver+17+for+SQL+Server sqlalchemy.engine.url.URL( - 'mssql+pyodbc', + "mssql+pyodbc", username=db_user, password=db_pass, database=db_name, - host='127.0.0.1', + host=host, port=1433, - query={"driver":"ODBC Driver 17 for SQL Server"} + query={"driver": "ODBC Driver 17 for SQL Server"}, ), # ... Specify additional properties here. # [START_EXCLUDE] - # [START cloud_sql_postgres_sqlalchemy_limit] # Pool size is the maximum number of permanent connections to keep. pool_size=5, @@ -61,26 +62,23 @@ # The total number of concurrent connections for your application will be # a total of pool_size and max_overflow. # [END cloud_sql_postgres_sqlalchemy_limit] - # [START cloud_sql_postgres_sqlalchemy_backoff] # SQLAlchemy automatically uses delays between failed connection attempts, # but provides no arguments for configuration. # [END cloud_sql_postgres_sqlalchemy_backoff] - # [START cloud_sql_postgres_sqlalchemy_timeout] # 'pool_timeout' is the maximum number of seconds to wait when retrieving a # new connection from the pool. After the specified amount of time, an # exception will be thrown. pool_timeout=30, # 30 seconds # [END cloud_sql_postgres_sqlalchemy_timeout] - # [START cloud_sql_postgres_sqlalchemy_lifetime] # 'pool_recycle' is the maximum number of seconds a connection can persist. # Connections that live longer than the specified amount of time will be # reestablished pool_recycle=1800, # 30 minutes # [END cloud_sql_postgres_sqlalchemy_lifetime] - echo=True #debug + echo=True # debug # [END_EXCLUDE] ) # [END cloud_sql_postgres_sqlalchemy_create] @@ -89,18 +87,19 @@ @app.before_first_request def create_tables(): # Create tables (if they don't already exist) - if not db.has_table('votes'): + if not db.has_table("votes"): metadata = sqlalchemy.MetaData(db) - table = Table('votes', metadata, - Column('vote_id', Integer, primary_key=True, nullable=False), - Column('time_cast', DateTime, nullable=False), - Column('candidate', String(6), nullable=False), - ) + table = Table( + "votes", + metadata, + Column("vote_id", Integer, primary_key=True, nullable=False), + Column("time_cast", DateTime, nullable=False), + Column("candidate", String(6), nullable=False), + ) metadata.create_all() - -@app.route('/', methods=['GET']) +@app.route("/", methods=["GET"]) def index(): votes = [] with db.connect() as conn: @@ -110,13 +109,11 @@ def index(): ).fetchall() # Convert the results into a list of dicts representing votes for row in recent_votes: - votes.append({ - 'candidate': row[0], - 'time_cast': row[1] - }) + votes.append({"candidate": row[0], "time_cast": row[1]}) stmt = sqlalchemy.text( - "SELECT COUNT(vote_id) FROM votes WHERE candidate=:candidate") + "SELECT COUNT(vote_id) FROM votes WHERE candidate=:candidate" + ) # Count number of votes for tabs tab_result = conn.execute(stmt, candidate="TABS").fetchone() tab_count = tab_result[0] @@ -125,31 +122,24 @@ def index(): space_count = space_result[0] return render_template( - 'index.html', - recent_votes=votes, - tab_count=tab_count, - space_count=space_count + "index.html", recent_votes=votes, tab_count=tab_count, space_count=space_count ) -@app.route('/', methods=['POST']) +@app.route("/", methods=["POST"]) def save_vote(): # Get the team and time the vote was cast. - team = request.form['team'] + team = request.form["team"] time_cast = datetime.datetime.utcnow() # Verify that the team is one of the allowed options if team != "TABS" and team != "SPACES": logger.warning(team) - return Response( - response="Invalid team specified.", - status=400 - ) + return Response(response="Invalid team specified.", status=400) # [START cloud_sql_postgres_sqlalchemy_connection] # Preparing a statement before hand can help protect against injections. stmt = sqlalchemy.text( - "INSERT INTO votes (time_cast, candidate)" - " VALUES (:time_cast, :candidate)" + "INSERT INTO votes (time_cast, candidate)" " VALUES (:time_cast, :candidate)" ) try: # Using a with statement ensures that the connection is always released @@ -164,17 +154,16 @@ def save_vote(): return Response( status=500, response="Unable to successfully cast vote! Please check the " - "application logs for more details." + "application logs for more details.", ) # [END_EXCLUDE] # [END cloud_sql_postgres_sqlalchemy_connection] return Response( status=200, - response="Vote successfully cast for '{}' at time {}!".format( - team, time_cast) + response="Vote successfully cast for '{}' at time {}!".format(team, time_cast), ) -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8080, debug=True) +if __name__ == "__main__": + app.run(host="127.0.0.1", port=8080, debug=True) From 1a50dfc8c891416bf8d160ec6766609214a51d0f Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Tue, 24 Mar 2020 13:48:59 -0700 Subject: [PATCH 03/16] update readme --- cloud-sql/sql-server/sqlalchemy/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud-sql/sql-server/sqlalchemy/README.md b/cloud-sql/sql-server/sqlalchemy/README.md index 4036122bb21..b7405ddc2cd 100644 --- a/cloud-sql/sql-server/sqlalchemy/README.md +++ b/cloud-sql/sql-server/sqlalchemy/README.md @@ -74,6 +74,8 @@ App Engine Flexible supports connecting to your SQL Server instance through TCP First, update `app.yaml` with the correct values to pass the environment variables and instance name into the runtime. +Then, make sure that the service account `service-{PROJECT_NUMBER}>@gae-api-prod.google.com.iam.gserviceaccount.com` has the IAM role `Cloud SQL Client`. + Next, the following command will deploy the application to your Google Cloud project: ```bash gcloud beta app deploy From 009067dbffb717f066deab9761bf9031d1ed8dd1 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Tue, 24 Mar 2020 13:56:24 -0700 Subject: [PATCH 04/16] update region tags --- cloud-sql/sql-server/sqlalchemy/Dockerfile | 4 ---- cloud-sql/sql-server/sqlalchemy/main.py | 27 +++++++++++----------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/cloud-sql/sql-server/sqlalchemy/Dockerfile b/cloud-sql/sql-server/sqlalchemy/Dockerfile index f6a540486e5..a9012ce96a8 100644 --- a/cloud-sql/sql-server/sqlalchemy/Dockerfile +++ b/cloud-sql/sql-server/sqlalchemy/Dockerfile @@ -51,10 +51,6 @@ ENV APP_HOME /app WORKDIR $APP_HOME COPY . ./ -ENV DB_USER sqlserver -ENV DB_PASS password -ENV DB_NAME votes -ENV PROD true # Run the web service on container startup. Here we use the gunicorn # webserver, with one worker process and 8 threads. diff --git a/cloud-sql/sql-server/sqlalchemy/main.py b/cloud-sql/sql-server/sqlalchemy/main.py index 66147703732..9b721b22dd7 100644 --- a/cloud-sql/sql-server/sqlalchemy/main.py +++ b/cloud-sql/sql-server/sqlalchemy/main.py @@ -27,19 +27,18 @@ logger = logging.getLogger() +# [START cloud_sql_server_python_sqlalchemy_create] # Remember - storing secrets in plaintext is potentially unsafe. Consider using # something like https://cloud.google.com/kms/ to help keep secrets secret. db_user = os.environ.get("DB_USER") db_pass = os.environ.get("DB_PASS") db_name = os.environ.get("DB_NAME") -#If this is running in the docker container when deployed to GAE flex, use "172.17.0.1" +# If this is running in the docker container when deployed to GAE flex, use "172.17.0.1" host = "172.17.0.1" if os.environ.get("PROD") else "127.0.0.1" -# [START cloud_sql_postgres_sqlalchemy_create] # The SQLAlchemy engine will help manage interactions, including automatically # managing a pool of connections to your database - db = sqlalchemy.create_engine( # Equivalent URL: # mssql+pyodbc://:@/:/?driver=ODBC+Driver+17+for+SQL+Server @@ -54,34 +53,34 @@ ), # ... Specify additional properties here. # [START_EXCLUDE] - # [START cloud_sql_postgres_sqlalchemy_limit] + # [START cloud_sql_server_python_sqlalchemy_limit] # Pool size is the maximum number of permanent connections to keep. pool_size=5, # Temporarily exceeds the set pool_size if no connections are available. max_overflow=2, # The total number of concurrent connections for your application will be # a total of pool_size and max_overflow. - # [END cloud_sql_postgres_sqlalchemy_limit] - # [START cloud_sql_postgres_sqlalchemy_backoff] + # [END cloud_sql_server_python_sqlalchemy_limit] + # [START cloud_sql_server_python_sqlalchemy_backoff] # SQLAlchemy automatically uses delays between failed connection attempts, # but provides no arguments for configuration. - # [END cloud_sql_postgres_sqlalchemy_backoff] - # [START cloud_sql_postgres_sqlalchemy_timeout] + # [END cloud_sql_server_python_sqlalchemy_backoff] + # [START cloud_sql_server_python_sqlalchemy_timeout] # 'pool_timeout' is the maximum number of seconds to wait when retrieving a # new connection from the pool. After the specified amount of time, an # exception will be thrown. pool_timeout=30, # 30 seconds - # [END cloud_sql_postgres_sqlalchemy_timeout] - # [START cloud_sql_postgres_sqlalchemy_lifetime] + # [END cloud_sql_server_python_sqlalchemy_limit] + # [START cloud_sql_server_python_sqlalchemy_lifetime] # 'pool_recycle' is the maximum number of seconds a connection can persist. # Connections that live longer than the specified amount of time will be # reestablished pool_recycle=1800, # 30 minutes - # [END cloud_sql_postgres_sqlalchemy_lifetime] + # [END cloud_sql_server_python_sqlalchemy_lifetime] echo=True # debug # [END_EXCLUDE] ) -# [END cloud_sql_postgres_sqlalchemy_create] +# [END cloud_sql_server_python_sqlalchemy_create] @app.before_first_request @@ -136,7 +135,7 @@ def save_vote(): logger.warning(team) return Response(response="Invalid team specified.", status=400) - # [START cloud_sql_postgres_sqlalchemy_connection] + # [START cloud_sql_server_python_sqlalchemy_connection] # Preparing a statement before hand can help protect against injections. stmt = sqlalchemy.text( "INSERT INTO votes (time_cast, candidate)" " VALUES (:time_cast, :candidate)" @@ -157,7 +156,7 @@ def save_vote(): "application logs for more details.", ) # [END_EXCLUDE] - # [END cloud_sql_postgres_sqlalchemy_connection] + # [END cloud_sql_server_python_sqlalchemy_connection] return Response( status=200, From bb1f7b675836c72afcdd7fa5dc47f4355cf480eb Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Tue, 24 Mar 2020 14:46:06 -0700 Subject: [PATCH 05/16] fixed linting errors --- cloud-sql/sql-server/sqlalchemy/main.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/cloud-sql/sql-server/sqlalchemy/main.py b/cloud-sql/sql-server/sqlalchemy/main.py index 9b721b22dd7..1bb461d5897 100644 --- a/cloud-sql/sql-server/sqlalchemy/main.py +++ b/cloud-sql/sql-server/sqlalchemy/main.py @@ -18,10 +18,9 @@ from flask import Flask, render_template, request, Response import sqlalchemy -from sqlalchemy import MetaData from sqlalchemy import Table from sqlalchemy import Column -from sqlalchemy import Integer, String, DateTime +from sqlalchemy import DateTime, Integer, String app = Flask(__name__) @@ -34,7 +33,8 @@ db_pass = os.environ.get("DB_PASS") db_name = os.environ.get("DB_NAME") -# If this is running in the docker container when deployed to GAE flex, use "172.17.0.1" +# If this is running in the docker container when deployed to GAE flex, +# use "172.17.0.1" host = "172.17.0.1" if os.environ.get("PROD") else "127.0.0.1" # The SQLAlchemy engine will help manage interactions, including automatically @@ -88,7 +88,7 @@ def create_tables(): # Create tables (if they don't already exist) if not db.has_table("votes"): metadata = sqlalchemy.MetaData(db) - table = Table( + Table( "votes", metadata, Column("vote_id", Integer, primary_key=True, nullable=False), @@ -104,7 +104,8 @@ def index(): with db.connect() as conn: # Execute the query and fetch all results recent_votes = conn.execute( - "SELECT TOP(5) candidate, time_cast FROM votes ORDER BY time_cast DESC" + "SELECT TOP(5) candidate, time_cast FROM votes " + "ORDER BY time_cast DESC" ).fetchall() # Convert the results into a list of dicts representing votes for row in recent_votes: @@ -120,9 +121,8 @@ def index(): space_result = conn.execute(stmt, candidate="SPACES").fetchone() space_count = space_result[0] - return render_template( - "index.html", recent_votes=votes, tab_count=tab_count, space_count=space_count - ) + return render_template("index.html", recent_votes=votes, + tab_count=tab_count, space_count=space_count) @app.route("/", methods=["POST"]) @@ -138,7 +138,8 @@ def save_vote(): # [START cloud_sql_server_python_sqlalchemy_connection] # Preparing a statement before hand can help protect against injections. stmt = sqlalchemy.text( - "INSERT INTO votes (time_cast, candidate)" " VALUES (:time_cast, :candidate)" + "INSERT INTO votes (time_cast, candidate)" + " VALUES (:time_cast, :candidate)" ) try: # Using a with statement ensures that the connection is always released @@ -160,7 +161,8 @@ def save_vote(): return Response( status=200, - response="Vote successfully cast for '{}' at time {}!".format(team, time_cast), + response="Vote successfully cast for '{}' at time {}!".format( + team, time_cast), ) From df96f8cbb36352ce307135bbc8bd5fec05bd9a0b Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Tue, 24 Mar 2020 19:26:05 -0700 Subject: [PATCH 06/16] Update copyright year Co-Authored-By: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> --- cloud-sql/sql-server/sqlalchemy/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-sql/sql-server/sqlalchemy/Dockerfile b/cloud-sql/sql-server/sqlalchemy/Dockerfile index a9012ce96a8..0ca818e34a0 100644 --- a/cloud-sql/sql-server/sqlalchemy/Dockerfile +++ b/cloud-sql/sql-server/sqlalchemy/Dockerfile @@ -1,4 +1,4 @@ -# Copyright 2019 Google, LLC. +# 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. From 8366e83dcaacc5097b4baf9ed0cb2c21d481ff86 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Mon, 30 Mar 2020 14:58:48 -0700 Subject: [PATCH 07/16] Update cloud-sql/sql-server/sqlalchemy/app.yaml Co-Authored-By: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> --- cloud-sql/sql-server/sqlalchemy/app.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud-sql/sql-server/sqlalchemy/app.yaml b/cloud-sql/sql-server/sqlalchemy/app.yaml index 24366231380..a9269dea96a 100644 --- a/cloud-sql/sql-server/sqlalchemy/app.yaml +++ b/cloud-sql/sql-server/sqlalchemy/app.yaml @@ -1,4 +1,4 @@ -# Copyright 2018 Google LLC +# 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. @@ -24,4 +24,4 @@ env_variables: PROD: true beta_settings: - cloud_sql_instances: ::=tcp:1433 \ No newline at end of file + cloud_sql_instances: ::=tcp:1433 From ff9018c35e563f7e64412e46dab6b79995954f6d Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Mon, 30 Mar 2020 14:59:14 -0700 Subject: [PATCH 08/16] Update cloud-sql/sql-server/sqlalchemy/templates/index.html Co-Authored-By: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> --- cloud-sql/sql-server/sqlalchemy/templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-sql/sql-server/sqlalchemy/templates/index.html b/cloud-sql/sql-server/sqlalchemy/templates/index.html index e390afaa5e2..1b3e8b26ae3 100644 --- a/cloud-sql/sql-server/sqlalchemy/templates/index.html +++ b/cloud-sql/sql-server/sqlalchemy/templates/index.html @@ -1,5 +1,5 @@