Skip to content

Conversation

mdgilene
Copy link
Contributor

@mdgilene mdgilene commented Aug 12, 2020

Change Summary

Added ability for BaseSettings to load SecretStr and SecretBytes from 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

  • Unit tests for the changes exist
  • Tests pass on CI and coverage remains at 100%
  • Documentation reflects the changes where applicable
  • changes/<pull request or issue id>-<github username>.md file added describing change
    (see changes/README.md for details)

@codecov
Copy link

codecov bot commented Aug 12, 2020

Codecov Report

Merging #1820 into master will increase coverage by 0.00%.
The diff coverage is 100.00%.

@@           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           
Impacted Files Coverage Δ
pydantic/env_settings.py 100.00% <100.00%> (ø)
pydantic/utils.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5288920...f1c17ca. Read the comment docs.

@mdgilene mdgilene force-pushed the settings-from-secret-files branch from 4d5192a to c9543ec Compare August 12, 2020 02:17
@mdgilene mdgilene marked this pull request as ready for review August 12, 2020 17:56
Copy link

@FichteFoll FichteFoll left a 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.

@mdgilene mdgilene force-pushed the settings-from-secret-files branch from 708e380 to 8ae4fec Compare August 14, 2020 21:18
Copy link
Member

@samuelcolvin samuelcolvin left a 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.

from .typing import display_as_type
from .utils import deep_update, sequence_like

env_file_sentinel = str(object())
secrets_dir_sentinel = str(object())
Copy link
Member

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.

Copy link
Contributor Author

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():
Copy link
Member

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()?

Copy link
Member

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?

Copy link
Contributor Author

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.

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,
Copy link
Member

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.

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 '
Copy link
Member

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.

if not secrets_path.is_dir():
return d

secret_paths = {
Copy link
Member

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


value: Optional[str] = None
for env_name in field.field_info.extra['env_names']:
path = secret_paths.get(env_name, None)
Copy link
Member

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

"""
Build secrets suitable for passing to the Model.
"""
d: Dict[str, Optional[str]] = {}
Copy link
Member

Choose a reason for hiding this comment

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

more descriptive name please.

Copy link
Contributor Author

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]:
Copy link
Member

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.
Copy link
Member

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.

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 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.

@mdgilene mdgilene requested a review from samuelcolvin October 9, 2020 15:36
@samuelcolvin
Copy link
Member

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 SecretStr and SecretBytes. This is because:

  • it would make it more like other variable substitutions from arguments, environment variables and dot-env files, and thus less confusing
  • I often want field types to be something like HttpUrl or even int, restricting to SecretStr and SecretBytes would prevent that

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.

@FichteFoll
Copy link

FichteFoll commented Oct 25, 2020

I think these values from secrets files should apply to all fields, not just SecretStr and SecretBytes.

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.)

@mdgilene
Copy link
Contributor Author

mdgilene commented Oct 25, 2020

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.

@samuelcolvin
Copy link
Member

okay, thanks. I'll try and do it.

Settings()


@pytest.mark.skipif(sys.platform.startswith('win'), reason='windows paths break regex')
Copy link

@FichteFoll FichteFoll Oct 25, 2020

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?

Copy link
Member

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()

Choose a reason for hiding this comment

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

Is this documented?

Copy link
Member

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

Related #1804

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants