-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Add ability to read secrets from files #1820
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
Add ability to read secrets from files #1820
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1820 +/- ##
=======================================
Coverage 99.90% 99.90%
=======================================
Files 21 21
Lines 4051 4078 +27
Branches 807 817 +10
=======================================
+ Hits 4047 4074 +27
Misses 3 3
Partials 1 1
Continue to review full report at Codecov.
|
4d5192a to
c9543ec
Compare
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.
Looking good from my side. Just a few things, notably the SecretStr documentation issue.
708e380 to
8ae4fec
Compare
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 looking great, a few things to change.
pydantic/env_settings.py
Outdated
| from .typing import display_as_type | ||
| from .utils import deep_update, sequence_like | ||
|
|
||
| env_file_sentinel = str(object()) | ||
| secrets_dir_sentinel = str(object()) |
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 doesn't seem to be used.
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.
Yep, leftover from copying the environment code. Will remove
|
|
||
| secrets_path = Path(secrets_dir) | ||
|
|
||
| if not secrets_path.is_dir(): |
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.
surely to avoid a lot of confusion we should raise an error if secrets_path.is_file()?
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.
perhaps we should also raise an error/warning if the directory is missing? i'm not so sure on that.
Perhaps only if it's provided via an argument, not config?
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.
Good point, I think that since the default value for secrets_dir is None, that providing any value pointing to a directory that is missing can be an error.
pydantic/env_settings.py
Outdated
| warnings.warn( | ||
| f'found secret file matching field "{field.name}" but the field was not ' | ||
| 'of type "SecretStr" or "SecretBytes". This secret file was not loaded.', | ||
| Warning, |
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.
perhaps we should remove this and use UserWarning so the warning is printed? (From memory only UserWarning is printed out by default which would be useful I imagine.
pydantic/env_settings.py
Outdated
| for field in self.__fields__.values(): | ||
| if field.type_ not in (SecretStr, SecretBytes): | ||
| warnings.warn( | ||
| f'found secret file matching field "{field.name}" but the field was not ' |
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.
wait, this is in the wrong place I think. No file has been found yet, we're just iterating through fields.
pydantic/env_settings.py
Outdated
| if not secrets_path.is_dir(): | ||
| return d | ||
|
|
||
| secret_paths = { |
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.
remove this see comment below
pydantic/env_settings.py
Outdated
|
|
||
| value: Optional[str] = None | ||
| for env_name in field.field_info.extra['env_names']: | ||
| path = secret_paths.get(env_name, None) |
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.
Better to remove the `` dict above and just do
path = secrets_path / env_name
if path.is_file() and secret type: add to the main dict
elif path.is_dir() and secret type: warning that it's a directory
elif path.is_file() and not secret type: warning that the file exist but type is not secret
pydantic/env_settings.py
Outdated
| """ | ||
| Build secrets suitable for passing to the Model. | ||
| """ | ||
| d: Dict[str, Optional[str]] = {} |
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.
more descriptive name please.
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.
Ok, this is the same name used by the environment loading code so may want to fix that at another time as well.
| @@ -122,13 +122,14 @@ def in_ipython() -> bool: | |||
| KeyType = TypeVar('KeyType') | |||
|
|
|||
|
|
|||
| def deep_update(mapping: Dict[KeyType, Any], updating_mapping: Dict[KeyType, Any]) -> Dict[KeyType, Any]: | |||
| def deep_update(mapping: Dict[KeyType, Any], *updating_mappings: Dict[KeyType, Any]) -> Dict[KeyType, Any]: | |||
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.
👍 looks good
| **a dotenv file and environment variables will always take priority over values loaded from the secrets directory**. | ||
|
|
||
| Passing a file path via the `_secrets_dir` keyword argument on instantiation (method 2) will override | ||
| the value (if any) set on the `Config` class. |
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.
Does this work with docker secrets and AWS Parameter Store, if so, please can we add a section to the docs saying that and giving an example of how you'd use them.
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 don't have any experience with AWS so I can't speak to it. However, Docker secrets as far as I'm aware simply mount the secrets as volumes to /run/secrets so yes this should handle that use case.
|
I've made a few tweaks to the docs, this looks great and I'm ready to merge it except one thing. I think these values from secrets files should apply to all fields, not just
Sorry, I should have thought about this before, but it didn't occur to me until now. @mdgilene and @FichteFoll what do you think? Unless you have strong objections, I can either wait for you to make the change, or make it myself. |
I definitely agree with your points there, as they reflect my own concerns regarding gating this behind special classes. While the explicit declaration of loading from secrets makes it easier to see what values are different at a glance, it also limits the functionality compared to loading values from environment variable, where nothing needs to be configured. (I'm also sorry for not raising this before despite thinking about it, as it didn't affect my particular use case.) |
|
I don't have a strong preference one way or the other. So opening it up to all types works for me. I don't actually have much time at the moment, so if you are able to make the change that would be awesome. Otherwise it might take a few days till I get around to it. |
|
okay, thanks. I'll try and do it. |
| Settings() | ||
|
|
||
|
|
||
| @pytest.mark.skipif(sys.platform.startswith('win'), reason='windows paths break regex') |
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.
You could .replace('/', r'[\\/]').
Or alternatively just use os.sep instead of a / literal?
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 tried escaping, too boring without a Windows install.
| """ | ||
| secrets: Dict[str, Optional[str]] = {} | ||
|
|
||
| secrets_dir = _secrets_dir or self.__config__.secrets_dir | ||
| if secrets_dir is None: | ||
| return secrets | ||
|
|
||
| secrets_path = Path(secrets_dir) | ||
| secrets_path = Path(secrets_dir).expanduser() |
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.
Is this documented?
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.
No, but it's pretty obvious
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.
Related #1804
Change Summary
Added ability for
BaseSettingsto loadSecretStrandSecretBytesfrom files. I opted to drop the ability to load complex types from secrets as there is no existing secret type for complex objects.Related issue number
#1279
Checklist
changes/<pull request or issue id>-<github username>.mdfile added describing change(see changes/README.md for details)