Skip to content

GCP Cloud Run/Function only: GoogleSecretManagerSettingsSource tries to read unrelated secrets even if all values are set in .env or env vars #710

@zaphod72

Description

@zaphod72

I am using the example code at https://docs.pydantic.dev/latest/concepts/pydantic_settings/#basic-usage


Note: this does NOT fail if I do not use a nested model. I.e.:

class Settings(BaseSettings):
    password: str
    user: str

    model_config = SettingsConfigDict(env_nested_delimiter='__')

    @classmethod
    def settings_customise_sources(
...

or if I remove the GoogleSecretManagerSettingsSource
or if I run outside of the GCP.


When I deploy this into Google Cloud Run or Google Cloud Functions it fails trying to read an unrelated secret.

Repro:

  1. Delete database__user and database__password secrets from your GCP.
  2. Create some unrelated secret and ensure that the Cloud Run/Function service account does NOT have read permissions on that secret.
  3. Create a Cloud Function with Python 3.12
    Set variables on the container:
DATABASE__PASSWORD: db_pwd
DATABASE__USER: db_user

Files:
.env:

DATABASE__USER=dotenv_db_user
DATABASE__PASSWORD=dotenv_db_password

requirements.txt:

functions-framework==3.*
pydantic-settings[gcp-secret-manager]==2.12.*
pydantic

main.py:

import functions_framework
from pydantic import BaseModel
from pydantic_settings import (
    BaseSettings,
    GoogleSecretManagerSettingsSource,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
)

class Database(BaseModel):
    password: str
    user: str


class Settings(BaseSettings):
    database: Database

    model_config = SettingsConfigDict(env_nested_delimiter='__')

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        # Create the GCP Secret Manager settings source
        gcp_settings = GoogleSecretManagerSettingsSource(
            settings_cls,
            # If not provided, will use google.auth.default()
            # to get credentials from the environemnt
            # credentials=your_credentials,
            # If not provided, will use google.auth.default()
            # to get project_id from the environemnt
            project_id='<your-project-id>',   # Yes, I did replace this with my project id
        )

        return (
            init_settings,
            env_settings,
            dotenv_settings,
            file_secret_settings,
            gcp_settings,
        )

@functions_framework.http
def test(request):
    settings = Settings()
    return settings.model_dump_json(indent=2)
  1. Deploy your Cloud Function.
  2. curl the CF URL.
  3. Check the logs for the error:
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
    rv = self.handle_user_exception(e)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/functions_framework/execution_id.py", line 157, in wrapper
    result = view_function(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/functions_framework/__init__.py", line 142, in view_func
    return function(request._get_current_object())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/functions_framework/__init__.py", line 121, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/main.py", line 50, in test
    settings = Settings()
               ^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/pydantic_settings/main.py", line 195, in __init__
    **__pydantic_self__._settings_build_values(
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/pydantic_settings/main.py", line 438, in _settings_build_values
    source_state = source()
                   ^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/pydantic_settings/sources/base.py", line 510, in __call__
    field_value = self.prepare_field_value(field_name, field, field_value, value_is_complex)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/pydantic_settings/sources/providers/env.py", line 114, in prepare_field_value
    env_val_built = self.explode_env_vars(field_name, field, self.env_vars)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/pydantic_settings/sources/providers/env.py", line 226, in explode_env_vars
    for env_name, env_val in env_vars.items():
                             ^^^^^^^^^^^^^^^^
  File "<frozen _collections_abc>", line 894, in __iter__
  File "/layers/google.python.pip/pip/lib/python3.12/site-packages/pydantic_settings/sources/providers/gcp.py", line 78, in __getitem__
    raise KeyError(key)
KeyError: '<one of your secrets>'

I also tried with just the .env and no env vars, and just the env vars and no .env

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions