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

Allowing setting config vars from environment #409

Merged
merged 6 commits into from
Oct 13, 2022
Merged

Conversation

vsoch
Copy link
Member

@vsoch vsoch commented Oct 7, 2022

This is an idea to address the same issue brought up in #408 - we want to be able to easily set variables in settings from the environment. This currently just includes values in config.py (and currently can be extended). I decided to explicitly list the names and types to have better control (e.g., instead of iterating over locals().

Signed-off-by: vsoch [email protected]

@vsoch vsoch force-pushed the consolidate-config branch from bdc7994 to 404f138 Compare October 7, 2022 13:45
Copy link

@cpeel cpeel left a comment

Choose a reason for hiding this comment

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

Overall I like this approach! The challenge with doing it here at the end of config.py rather than __init__.py is that we can't override other variables set in other settings/* files, like those in tasks.py (which we would want to do in our k8s scenario as we move redis elsewhere).

I also fear that we now need to keep the lists up-to-date when a new variable is added. We could add some CI that checks to see if any variable is in the file that isn't in one of the other lists.

shub/settings/config.py Outdated Show resolved Hide resolved
shub/settings/config.py Outdated Show resolved Hide resolved
shub/settings/config.py Outdated Show resolved Hide resolved
shub/settings/config.py Outdated Show resolved Hide resolved
shub/settings/config.py Outdated Show resolved Hide resolved
shub/settings/config.py Outdated Show resolved Hide resolved
shub/settings/config.py Outdated Show resolved Hide resolved
shub/settings/config.py Outdated Show resolved Hide resolved
@vsoch
Copy link
Member Author

vsoch commented Oct 7, 2022

@cpeel totally agree - let me rework this a bit (probably after work) to see if I can make it a neater solution. I don't like the redundancy either!

@vsoch
Copy link
Member Author

vsoch commented Oct 7, 2022

@cpeel I've been considering this for some time - but it might bet time to migrate the settings folder into a single settings.py to make this more clear, generally. What do you think? I'm happy to take the first shot (and I'll refactor this PR along with it!)

@cpeel
Copy link

cpeel commented Oct 7, 2022

I'm not sure refactoring it into a single settings.py is required. The way you've done the importing via __init__.py is nice and it lets them all be "local" to settings.py but logically grouped. If you moved the overrides into __init__.py after the imports rather than in config.py that would ensure that the overrides apply to any file.

One way to decrease the redundancy in this approach might be to iterate over all env settings and pull out all of them with SREGISTRY_ prefixes. If there is a matching local variable, set it. If it is prefixed by 'ENABLEorDISABLE` treat it as a boolean. Otherwise try to cast it as an int, otherwise treat it as a string. This has the potential to be more error prone -- and I would need to review all of the existing settings to see if this would work -- but it's extensible and removes the redundancy. We'd still need to special-case the lists in the code, but those are actually pretty few. An idea anyway.

Another possible approach would be to have the settings in a JSON file that is specified via env variable (SREGISTRY_CONFIG_FILE). The JSON would allow the user to be explicit about the type and take care of things like lists, dicts, etc. So the override code would open and iterate over the JSON and set local settings to their JSON values. In k8s we'd just create the JSON file in the manifest and pass in the env variable to pick it up. The more I type the more I like that idea.

@vsoch
Copy link
Member Author

vsoch commented Oct 7, 2022

Another possible approach would be to have the settings in a JSON file that is specified via env variable (SREGISTRY_CONFIG_FILE). The JSON would allow the user to be explicit about the type and take care of things like lists, dicts, etc. So the override code would open and iterate over the JSON and set local settings to their JSON values. In k8s we'd just create the JSON file in the manifest and pass in the env variable to pick it up. The more I type the more I like that idea.

I also like the idea of having this as an option, although not required. And I'm also leaning towards making everything very flexible - e.g., "provide the value directly in the file, via the config file, or environment - up to you!"

Is JSON preferred to YAML? I think (for configs like this) my preference would be YAML, but maybe my config-ometer sense is off. 😆

@vsoch
Copy link
Member Author

vsoch commented Oct 7, 2022

okay will have a PR update for you this evening!

@cpeel
Copy link

cpeel commented Oct 7, 2022

Is JSON preferred to YAML? I think (for configs like this) my preference would be YAML, but maybe my config-ometer sense is off.

YAML is probably better (says my professional half -- my personal half thinks YAML is rubbish 😁 ).

@vsoch vsoch force-pushed the consolidate-config branch from 9b25c8c to 78e0452 Compare October 7, 2022 23:13
@vsoch
Copy link
Member Author

vsoch commented Oct 7, 2022

YAML is probably better (says my professional half -- my personal half thinks YAML is rubbish grin ).

Haha, I totally feel that!

So I just pushed a refactor that has a single settings.py (to give more confidence we find everything in one place) and it should work to define variables from:

  • defaults in that file (e.g., a local or custom build)
  • a settings.yaml in the root OR a custom path (e.g., still local or cloud / Kubernetes)
  • from the environment (could also work in either case!)
  • still a secrets.py alongside the settings.py (I realize I enabled SECRET_KEY and we might need to move this import higher up to ensure it doesn't get overridden!)

9b25c8c

I decided to still have tight control over variables and placed them into groups based on type, as oppose to a strategy that looks for any SREGISTRY_ prefix in the environment and then tries to guess. It's an improvement on my first shot because we remove the redundancy. If we want to expose more customization (e.g., in the various dicts in settings.py) that should be fairly easy to do.

The one change I'm still thinking about doing is not having any "special prefix" for secrets as I do now (e.g., SREGISTRY_SECRET_. I was thinking that might be useful for the developer - so they can see based on the name that something should be treated more carefully. I'm thinking maybe it makes sense to try out this branch for your use case, and we can tweak / change as you see issues arise?

Copy link

@cpeel cpeel left a comment

Choose a reason for hiding this comment

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

The one change I'm still thinking about doing is not having any "special prefix" for secrets as I do now (e.g., SREGISTRY_SECRET_).

Yes, I was thinking that too after reviewing the code. I don't think prefixing SECRET makes the developer any more (or less) aware of the importance of that and think it would be simpler overall if we didn't do the special prefixing. And as a counter-example we're not prefixing that to the postgres DB password but that's arguably also important.


While I haven't yet tested this, the concept is solid and I think this will work for what we need to do.

One thing I am wary of is that this will break backwards compatibility for users who upgrade and in the past may have been replacing CONFIG.py with their settings. This also renames some configuration variables. I would classify this is an "API-breaking change" and would rev this as a major point release, rather than a fix update, per semver

shub/settings.py Outdated Show resolved Hide resolved
shub/settings.py Show resolved Hide resolved
shub/settings.py Show resolved Hide resolved
shub/settings.py Outdated Show resolved Hide resolved
shub/settings.py Outdated

# STORAGE

MINIO_ROOT_USER = os.environ.get("MINIO_ROOT_USER")
Copy link

Choose a reason for hiding this comment

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

While we're here, why not allow this to be set via the config as well? To keep backwards compatibility we could do:

MINIO_ROOT_USER = os.environ.get("MINIO_ROOT_USER", cfg.MINIO_ROOT_USER)

and make the default for MINIO_ROOT_USER in the STRING_DEFAULTS to be None.

Copy link
Member Author

Choose a reason for hiding this comment

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

We could definitely do that, although for the actual Minio server it is expecting MINIO_ROOT_USER explicitly. I think the use case that you have in mind is having deployed your Minio separately (and so you could use SREGISTRY_MINIO_ROOT_USER without an issue? I'm definitely happy to add that!

Copy link
Member Author

@vsoch vsoch Oct 9, 2022

Choose a reason for hiding this comment

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

I also realize that I need to do a check if a value is set before setting to locals, e.g., for the case that the cfg MINIO_ROOT_USER (from SREGISTRY_MINIO_ROOT_USER) is None, we don't want it to override MINIO_ROOT_USER

Copy link

Choose a reason for hiding this comment

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

Correct - I need to decouple the two. Eventually I need this to work against a genuine S3 bucket rather than a minio server, but I haven't yet gotten to testing that / making changes to support it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh nice! That would be amazing to have (and based on the shared protocol hopefully not too hard to support). I can definitely help when the time comes.

shub/settings.py Outdated Show resolved Hide resolved
requirements.txt Outdated Show resolved Hide resolved
shub/settings.py Outdated Show resolved Hide resolved
@vsoch vsoch force-pushed the consolidate-config branch 2 times, most recently from 38d1c21 to 854b7ab Compare October 9, 2022 22:46
@vsoch
Copy link
Member Author

vsoch commented Oct 9, 2022

okay (barring no issues with building) another round of changes! 854b7ab

The only detail we have to be careful about now is that values that are expected to be in settings (e.g., GOOGLE_ANALYTICS that can still be set to undefined are provided as empty string "" and not None - Nones are assumed to be unset and won't be added to the settings locals, whereas an empty string will.

Copy link

@cpeel cpeel 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 💯 . Some small PR feedback. Still need to do some testing on it.

docs/_docs/install/settings.md Show resolved Hide resolved
shub/settings.py Outdated Show resolved Hide resolved
shub/settings.py Show resolved Hide resolved

Order of preference or variables honored is:

1. secrets.py
Copy link

Choose a reason for hiding this comment

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

The way it is coded now, the order is:

  1. secrets.py
  2. settings.yaml
  3. the environment
  4. defaults directly in settings.py

Which I think is the right order, because we:

  1. set the defaults
  2. override any values from the env
  3. override any values from settings.yaml
  4. only override values already set in the file from secrets.yaml.

Copy link
Member Author

Choose a reason for hiding this comment

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

Environment (starting on line 226) actually comes before settings.yaml (275) , however we could switch this around so the settings.yaml is first. I was thinking that the secrets.py should take first preference for backwards compatibility, and then environment should always override everything else (e.g., a config file might already exist but the user can easily set the envar to override it) and then the defaults in settings.py are last. What do you think?

Copy link

Choose a reason for hiding this comment

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

Yes, sorry, I read the code wrong. What you have here in settings.md matches the behavior in settings.py.

It might be better if the env overrides the settings.yaml file, but I'm not going to lose any sleep over what you have here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I do agree environment should not be over-ridden - the challenge here is that we don't have a good way to determine if a variable was set in the environment (or is still a default). We can't just check if a DEFAULT[key] is not None, because this won't work for booleans, or generally any variable with a default that isn't None. It's a bit of a catch 22 because in order to set anything from settings.yaml, we need to have all the variables in one lookup, DEFAULTS, but we can't set that one variable until we've parsed the different types from the environment! So I think (for now at least) this approach is a reasonable start. If you have a good idea for an implementation though I'd definitely be down to try it!

shub/settings.py Outdated
return envar_list


# Try reading in from secrets first (no issue if not found)
Copy link

Choose a reason for hiding this comment

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

I would move this to the end of the file so that the load order maps to the conceptual "define and then override" that we're doing:

  1. defaults
  2. environment
  3. settings.yaml
  4. secrets.py

The secrets currently take precedence because of this code on line 664:

    # Don't set if the value is empty, or it's been set previously
    if value is None or key in locals() and locals()[key] is not None:

I think the order you have is correct and moving the secrets imports to line 648 makes it very clear that the file trumps everything else.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah ok, so you agree about environment being honored first? I think my point of confusion is that I'm loading in this order:

  1. secrets.py
  2. environment
  3. settings.yaml
  4. defaults

and the implementation does that by using that order, but never setting something that is already defined.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree - we can put secrets at the bottom, although I'll want to double check / think about if any of the previous settings (e.g., putting a value into a data structure) would prevent that.

Copy link
Member Author

Choose a reason for hiding this comment

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

okay I think this will work - with the assumption that secrets.py will work only for values you could previously put there (e.g., the new values for the various API settings that I'm adding newly will not work) and to over-ride entire datastructures (e.g., the database) you need to define the entire thing (and it's not exposed just via attributes of that).

the user can now define variables directly in settings.py,
via an external secrets.py or settings.yaml, or in
the environment with SREGISTRY_ as a prefix.

Signed-off-by: vsoch <[email protected]>
@vsoch vsoch force-pushed the consolidate-config branch from 854b7ab to 4a51afb Compare October 10, 2022 00:49
@vsoch vsoch force-pushed the consolidate-config branch from 061a6d2 to ea1a148 Compare October 10, 2022 01:25
@vsoch
Copy link
Member Author

vsoch commented Oct 10, 2022

Note to self: don't look back at old code, because it undoubtedly will look terrible and you'll want to change everything to make it look nicer! 😭 (true story, just now...) 😆

Thanks for the back and forth today @cpeel it was fun!

@cpeel
Copy link

cpeel commented Oct 10, 2022

Note to self: don't look back at old code, because it undoubtedly will look terrible and you'll want to change everything to make it look nicer! 😭 (true story, just now...) 😆

lol - I've been doing software development for 22 years and this only gets worse! On the plus side this means that we're becoming better software developers!

Thanks for the back and forth today @cpeel it was fun!

Same! Appreciate all of your work in this PR! I will give this a test run ASAP. Tomorrow is busy for me so might be Tuesday.

Copy link

@cpeel cpeel 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 working great!

Found two small minor comments in my testing but this looks good to go on my end.


# If we don't have a secret key, no go
if "SECRET_KEY" not in locals():
sys.exit("SECRET_KEY is required but not set. Set SREGISTRY_SECRET_KEY.")
Copy link

Choose a reason for hiding this comment

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

Setting SECRET_KEY in the YAML file works fantastically, but setting SREGISTRY_SECRET_KEY env var doesn't work because we don't define it in the STRING_DEFAULTS. It's probably easiest to keep the functionality the way it is now and just update the message to Set SECRET_KEY in secrets.py or a YAML config file.

Copy link
Member Author

Choose a reason for hiding this comment

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

Any reason to not allow this in the environment? I am thinking we can just add to the STRING_DEFAULTS and be more consistent?

Copy link

Choose a reason for hiding this comment

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

No reason I can think of. Adding it to STRING_DEFAULTS and updating the check for it to be not None makes sense.

shub/settings.py Outdated
"AUTH_LDAP_STAFF_GROUP_FLAGS": None, # "cn=staff,ou=django,ou=groups,dc=example,dc=com",
# Anyone in this group is a superuser for the app
"AUTH_LDAP_SUPERUSER_GROUP_FLAGS": None, # "cn=superuser,ou=django,ou=groups,dc=example,dc=com"
# "cn=sregistry_admin,ou=groups,dc=example,dc=com"
Copy link

Choose a reason for hiding this comment

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

Not sure what this LDAP string is for -- is it a second example for AUTH_LDAP_SUPERUSER_GROUP_FLAGS above?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, just a second example! I had two previously and wanted to keep both of them for the developer user. I'm not great with LDAP so I couldn't tell you the difference, I'd have to ping the plugin contributor to get details.

Copy link

Choose a reason for hiding this comment

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

I would remove this line then as I don't think it provides any additional information for someone who is integrating with LDAP -- the other one is sufficient. (Says someone who worked on LDAP code for 10 years.)

@vsoch
Copy link
Member Author

vsoch commented Oct 12, 2022

@cpeel I'll get these final tweaks in tonight - I don't typically commit to personal projects during the work day!

@vsoch
Copy link
Member Author

vsoch commented Oct 12, 2022

okay final two changes in! b9335b6

Copy link

@cpeel cpeel left a comment

Choose a reason for hiding this comment

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

Ship it!

Signed-off-by: vsoch <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants