Skip to content
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

Provide samples for using Python Connector with Django #437

Open
jackwotherspoon opened this issue Aug 25, 2022 · 14 comments
Open

Provide samples for using Python Connector with Django #437

jackwotherspoon opened this issue Aug 25, 2022 · 14 comments
Assignees
Labels
priority: p1 Important issue which blocks shipping the next release. Will be fixed prior to next release. type: cleanup An internal cleanup or hygiene concern. type: docs Improvement to the documentation for an API.

Comments

@jackwotherspoon
Copy link
Collaborator

Django is one of the most widely used Python web frameworks. We should look at providing samples on how to connect to a Cloud SQL database using the Python Connector and Django.

What makes this a more difficult feat is that the default Django backend for Postgres is psycopg2 and for MySQL is mysqlclient or mysql-connector-python, which are not currently supported with the Python Connector.

@jackwotherspoon jackwotherspoon added priority: p2 Moderately-important priority. Fix may not be included in next release. type: docs Improvement to the documentation for an API. labels Aug 25, 2022
@jackwotherspoon jackwotherspoon self-assigned this Aug 25, 2022
@hadim
Copy link

hadim commented Nov 15, 2022

Any ETA on this?

@jackwotherspoon
Copy link
Collaborator Author

@hadim I can't really provide a precise ETA as of right now, it most likely won't be until early 2023. I have begun playing around with usage of the Python Connector and Django as is currently. However, if development work is needed to add support within Django for the Python Connector I won't be able to get around to it until the new year.

If anyone here has had experience in leveraging the Python Connector with Django I'm more than happy to hear about their feedback/experience to update the Python Connector documentation accordingly with a code sample.

Will ping this thread with any updates moving forward.

@enocom enocom added the type: cleanup An internal cleanup or hygiene concern. label Nov 18, 2022
@jackwotherspoon jackwotherspoon added priority: p1 Important issue which blocks shipping the next release. Will be fixed prior to next release. and removed priority: p2 Moderately-important priority. Fix may not be included in next release. labels Dec 7, 2022
@stiangrim
Copy link

Any updates on this?

@jackwotherspoon
Copy link
Collaborator Author

Hi @stiangrim, I've begun trying to find elegant workarounds to using the Cloud SQL Python Connector with Django as I hope to avoid having to write Django backend(s) to support the Python Connector.

In the meantime if you or anyone else has had any luck feel free to post your finding here! I'm sure others would benefit greatly 😃

@danielroseman
Copy link
Contributor

I don't know about doing it without writing a Django backend, but it turned out to be super easy to write one that supports the connector. In my case, since I was only targeting MySQL, I put this into a cloudsql/base.py file in my project:

from django.db.backends.mysql import base
from google.cloud.sql.connector import Connector


class DatabaseWrapper(base.DatabaseWrapper):
    def get_new_connection(self, conn_params):
        return Connector().connect(**conn_params)

and then with a DATABASES setting like this:

DATABASES = {
  "default": {
    "ENGINE": "cloudsql",
    "USER": "...",
    "PASSWORD": "...",
    "NAME": "...",
    "OPTIONS": {
      "driver": "pymysql",
      "instance_connection_string": "project:region:instance"
    }
}
import pymysql
pymysql.install_as_MySQLdb()

it Just Worked. (The install_as_MySQLdb is needed as Django doesn't support PyMySQL out of the box; if the connector starts supporting mysqlclient, as proposed elsewhere, we can switch to that and this can go away.)

Happy to contribute this as a PR to the docs.

@jackwotherspoon
Copy link
Collaborator Author

@danielroseman That is great news! 👏

Would be greatly appreciated if you could put up a PR adding this to the frameworks section of our README, we'd love to showcase this solution for others 😄

danielroseman added a commit to danielroseman/cloud-sql-python-connector that referenced this issue Jul 28, 2023
Refs GoogleCloudPlatform#437 

Note, I realised while writing this that it won't work with Postgres as there's no equivalent of the `install_as_mysqldb` call; I included a note explaining this.
danielroseman added a commit to danielroseman/cloud-sql-python-connector that referenced this issue Jul 29, 2023
@enocom
Copy link
Member

enocom commented Aug 10, 2023

I don't know about doing it without writing a Django backend, but it turned out to be super easy to write one that supports the connector.

Writing three separate custom backends for each db engine is probably the way to go. Incidentally, this mirrors what we do in Go with three separate database/sql hooks. Similar effort and similar result.

@punjabdhaputar
Copy link

Is there any support for Postgres + Django using the cloud-sql-connector?

@enocom
Copy link
Member

enocom commented Feb 6, 2024

You'd have to write a custom backend using pg8000 (or asyncpg) like above. I haven't tried this myself, but in theory it's possible.

@rcleveng
Copy link

rcleveng commented Mar 6, 2024

Here's one way to do it.

https://github.com/rcleveng/django_gcp_iam_auth

Instructions on how to install with pip on the README.md
Just update the DATABASES entry to replace the ENGINE for your connection.

DATABASES["default"]["ENGINE"]  = "django_gcp_iam_auth.postgresql"

@enocom
Copy link
Member

enocom commented Mar 6, 2024

Thanks, @rcleveng -- that's a nice approach.

In effect it's a variation of #214 (comment).

It gets you IAM authentication without the Connector. Meanwhile, we're working on improving Django support in the near term.

@enocom
Copy link
Member

enocom commented Mar 7, 2024

Adding the code sample from the link above for posterity:

import copy
from django.db.backends.postgresql import base

try:
    import google.auth
    import google.auth.exceptions
    from google.auth.transport import requests
    from google.auth.credentials import Credentials, Scoped, TokenState

except google.auth.exceptions.DefaultCredentialsError:
    pass

CLOUDSQL_IAM_LOGIN_SCOPE = ["https://www.googleapis.com/auth/sqlservice.login"]

class DatabaseWrapper(base.DatabaseWrapper):
    def get_connection_params(self):
        params = super().get_connection_params()
        # need to remove this otherwise we'll get errors like
        #   'invalid dsn: invalid connection option "gcp_iam_auth"'
        if params.pop("gcp_iam_auth", None):
            self._credentials, _ = google.auth.default(scopes=CLOUDSQL_IAM_LOGIN_SCOPE)
            if not self._credentials.token_state == TokenState.FRESH:
                self._credentials.refresh(requests.Request())
            params.setdefault("port", 5432)
            # TODO - should we add in a resource restriction for the DB instance?
            # https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#auth_downscoping_token_broker-python
            # Set password to newly fetched access token
            params["password"] = self._credentials.token

        return params

@JoshTanke
Copy link

Adding the code sample from the link above for posterity:

import copy
from django.db.backends.postgresql import base

try:
    import google.auth
    import google.auth.exceptions
    from google.auth.transport import requests
    from google.auth.credentials import Credentials, Scoped, TokenState

except google.auth.exceptions.DefaultCredentialsError:
    pass

CLOUDSQL_IAM_LOGIN_SCOPE = ["https://www.googleapis.com/auth/sqlservice.login"]

class DatabaseWrapper(base.DatabaseWrapper):
    def get_connection_params(self):
        params = super().get_connection_params()
        # need to remove this otherwise we'll get errors like
        #   'invalid dsn: invalid connection option "gcp_iam_auth"'
        if params.pop("gcp_iam_auth", None):
            self._credentials, _ = google.auth.default(scopes=CLOUDSQL_IAM_LOGIN_SCOPE)
            if not self._credentials.token_state == TokenState.FRESH:
                self._credentials.refresh(requests.Request())
            params.setdefault("port", 5432)
            # TODO - should we add in a resource restriction for the DB instance?
            # https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#auth_downscoping_token_broker-python
            # Set password to newly fetched access token
            params["password"] = self._credentials.token

        return params

@enocom Your comments on this thread and others has been really helpful! I'm wondering if you might have any ideas on this related issue I'm hitting. I've been trying to get the IAM auth approach to work with Django on my local machine but the connection keeps timing out. AFAICT my psycopg2.connect database url matches the format in this other thead: dbname={db_name} client_encoding=UTF8 user={sa}@{project}.iam host={public_ip} port=5432 password={token}. I have public + private IPs enabled and also have "Require trusted client certificates" turned on in the SSL settings. My best guess is the SSL setting might be the issue - do I need to create an SSL client certificate and include it in my DatabaseWrapper somehow, or does the IAM auth flow do that for me somehow?

@jackwotherspoon
Copy link
Collaborator Author

@JoshTanke Thanks for the comment on this issue. Do you mind creating a new issue on the repo with the error you are seeing? That way we can help debug your problem while keeping this thread clean 😄

I have public + private IPs enabled and also have "Require trusted client certificates" turned on in the SSL settings. My best guess is the SSL setting might be the issue - do I need to create an SSL client certificate and include it in my DatabaseWrapper somehow, or does the IAM auth flow do that for me somehow?

Once you open a new issue with your stacktrace I'll get a better grasp of what is going on. However, yes if you have "Require trusted client certificates" turned on and are not creating a database connection using SSL with client certs (mTLS). Which by the sample above and database URL you provided you are not then I would expect it to error. You would need to download your certificates and add their path to your psycopg2 database url (this SO post does a good job explaining the psycopg2 config)

Depending on your use-case and especially for Private IP connections you may be fine with turning the setting to "Allow only SSL connections". This would allow you to update your database url with sslmode=require to:

bname={db_name} client_encoding=UTF8 user={sa}@{project}.iam host={public_ip} port=5432 password={token} sslmode=require

However, this still isn't ideal for Public IP connections which we are working on by adding support for psycopg2 to the Cloud SQL Python Connector soon (will be worked on this quarter) which will then also unblock Django for postgres and give users a way of using Django + Cloud SQL for "Require trusted client certificates" without having to manage certificates yourself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority: p1 Important issue which blocks shipping the next release. Will be fixed prior to next release. type: cleanup An internal cleanup or hygiene concern. type: docs Improvement to the documentation for an API.
Projects
None yet
Development

No branches or pull requests

8 participants