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

Add a config.strategy to enable overwrite of properties from env vars #620

Merged
merged 13 commits into from
Apr 3, 2019

Conversation

andreaTP
Copy link
Contributor

This PR propose to introduce an alternative config.strategy to enable the overwrite of configurations entries with environment variables even if a substitution is not explicitly provided in the configuration files.

Rationale:

We want to be able to dynamically override potentially any configuration option with environment variables.
The current env variable substitution mechanism is verbose and have to be explicitly defined for every configuration ahead of time.
more on the topic is detailed here: https://github.com/codacy/env2props#rationale

Unfortunately the approach taken in env2props has a big drawback, getting the bash string substitution correct is not trivial and is error-prone.

Here I'm proposing to have an alternative implementation of the config.strategy that allow to obtain the very same behavior.

Notes:

the mangling of env vars is needed because in a default bash environment:

$ export A.B=1
bash: export: `A.B=1': not a valid identifier
$ export A-B=1
bash: export: `A-B=1': not a valid identifier

@havocp
Copy link
Collaborator

havocp commented Mar 1, 2019

Useful context from gitter chat is that we're wanting to allow setting a JVM property flag one time, and then a DevOps or Ops person could add new overrides via environment variables at any time, without editing the config file itself. This is useful in cases like Docker or Heroku or Kubernetes which are often configured via env var.

Possible requirements:

  • this should be opt-in somehow (shouldn't start importing all env vars as overrides) for back compat reasons
  • should be able to override any config value no matter what's in the config file

I feel like the "strategy" notion is a little non-orthogonal (it potentially bundles together multiple behaviors), thinking it could be easier to think about this as a feature which is on or off.

One way it could look is:

  • by default, it stays how it is now
  • if you set -Dconfig.override_with_env_vars_prefix=CONFIG_FORCE_ then any env vars starting with CONFIG_FORCE_ are added to ConfigFactory.defaultOverrides

Possible questions:

  • is it really needed to have -Dconfig.override_with_env_vars_prefix defaulting to null, or could we simply always look at CONFIG_FORCE_ and figure nobody would have env vars starting with that right now
  • instead of a configurable prefix we could have a boolean and it's always the same prefix

The escaping situation is a little bit tough; config (and JSON) allow keys to be about anything, but many characters are awkward in an env variable. The PR so far invents a novel escaping mechanism to allow ., -, and _, which would need to be documented / isn't obvious / doesn't cover all characters such as spaces... I'm not loving any of the solutions to this that I can think of.

An option we've discussed in the past is an env var whose value would be parsed as HOCON and made into overrides, e.g. on #427 we mentioned it; an advantage to this is that values can also be unset and that any arbitrary key is possible. See #582

The downside to CONFIG_OVERRIDES="{ foo: null }" is that it's pretty annoying if you just want to set one key and don't want to figure out how to escape it etc.

It's possible we should do a combined approach? Like all CONFIG_FORCE_* env vars get added to defaultOverrides(), and also the value of CONFIG_OVERRIDES= is parsed as a document and also added to defaultOverrides() ? and then we don't support any fancy escaping or hard stuff with single CONFIG_FORCE, it only does simple cases, and the CONFIG_OVERRIDES mechanism is there for hard cases.

(minor detail if we did go that road maybe we should choose between FORCE and OVERRIDES for both, like CONFIG_OVERRIDE_* and CONFIG_OVERRIDES, instead of calling one force and one override.)

with the prefixes in the CONFIG_ namespace it kinda seems OK to me to enable this by default without a JVM flag...

@andreaTP
Copy link
Contributor Author

andreaTP commented Mar 1, 2019

I like your proposal and I will re-work this PR next week to follow.
On the trigger I personally prefer the option of having a simple boolean to enable it and fix the prefix to CONFIG_FORCE_, this looks the simpler and safer.

On the invents a novel escaping you are completely right but I'm short on alternatives, it is currently documented here: https://github.com/lightbend/config/pull/620/files#diff-23287583c575f1193e540c940d690b6aR11 but I'm open to other options as I don't love it as well, to note that this limitation is imposed by usability in bash and terminals in general since A.B or A-B are totally valid environment variables. This is a common issue (e.g. moby/moby#16585 (comment))

I personally don't love the CONFIG_OVERRIDES approach since writing json, hocon etc. in env variables is extremely annoying, un-setting keys looks to be the only valid use case, and looks pretty niche. Still I would prefer to have a single approach to the problem instead of 2 overlapping features.

with the prefixes in the CONFIG_ namespace it kinda seems OK to me to enable this by default without a JVM flag...

this is an assumption actually, debugging an issue caused by this will be incredibly hard and counter-intuitive, I will start with a flag and in case we remove it if there is enough community endorsement.

@andreaTP
Copy link
Contributor Author

andreaTP commented Mar 4, 2019

@havocp please have a look at the updated implementation.

@dwijnand
Copy link
Member

dwijnand commented Mar 6, 2019

👨‍🎨 Could the prefix be HOCON_ instead?

(I still believe we should rebrand this library as "hocon" - #514)

@eed3si9n
Copy link
Contributor

eed3si9n commented Mar 6, 2019

I am not sure about the implementation details but the idea of ops person being able to override things via environment variables seems consistent with Twelve-Factor App (https://12factor.net/config), Kubernetes, etc ethos.

@andreaTP
Copy link
Contributor Author

andreaTP commented Mar 7, 2019

Thinking twice at it, having a prefix like HOCON_FORCE_ or HOCON_OVERRIDE_ we can most likely get rid of the JVM flag and enable it by default.

@dwijnand
Copy link
Member

dwijnand commented Mar 7, 2019

(Still in 👨‍🎨-mode) I'd rather avoid the extra FORCE/OVERRIDE.

For example, I'd love if I could specify with an environment variable akka discovery's method with just:

export HOCON_AKKA_DISCOVERY_METHOD=akka-dns

@havocp
Copy link
Collaborator

havocp commented Mar 7, 2019

If there’s nothing after HOCON then we can’t use that namespace for any other purpose (env vars that affect the library itself for example). Maybe that’s OK.

It feels a bit odd to me to use this namespace here and config everywhere else. These env vars technically have nothing to do with HOCON, in fact if we stripped the library down to only json and properties and env var support, this feature would still work.

@andreaTP
Copy link
Contributor Author

andreaTP commented Mar 7, 2019

also your points are valid @havocp , to not stall the PR on this can we make it configurable via java properties and have a longish default like JAVA_CONFIG_OVERWRITE_ ?

@dwijnand
Copy link
Member

dwijnand commented Mar 7, 2019

if we stripped the library down to only json and properties and env var support, this feature would still work

(Sure, but I identify this library as the hocon library. 😄)

Copy link
Collaborator

@havocp havocp left a comment

Choose a reason for hiding this comment

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

Might be helpful to add a note to the README or other docs where config.* props are already mentioned, about this new -Dconfig.override_with_env_vars

Overall I feel like this is good. I like the CONFIG_FORCE_ naming I guess, though I don't like the overly-generic name of the entire library, I don't know that we should address that piecemeal in this context. :-)

I'm fine with leaving this as opt-in behavior, at least to start. We could always make it the default down the road, I suppose.

@@ -383,7 +384,11 @@ public static Config defaultReference(ClassLoader loader) {
* @return the default override configuration
*/
public static Config defaultOverrides() {
return systemProperties();
if (!getOverrideWithEnv()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Think it's easier to read if you reverse the if/else to avoid the !

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

private static Boolean getOverrideWithEnv() {
String overrideWithEnv = System.getProperties().getProperty(OVERRIDE_WITH_ENV_PROPERTY_NAME);

return Boolean.parseBoolean(overrideWithEnv);
Copy link
Collaborator

Choose a reason for hiding this comment

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

if the value of the property is not a boolean this will throw an ugly and likely-mysterious exception right? might want to catch it and say something like "the value of OVERRIDE_WITH_ENV_PROPERTY_NAME must be a boolean not '%s'"

Not sure if we already have this bug when looking at any other env var values

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've deliberately choose to use Boolean.parseBoolean because it doesn't throw exceptions.
Basically it always return false unless the string is (case-insensitive) true.

* This method can return a global immutable singleton, so it's preferred
* over parsing system properties yourself.
* <p>
* {@link #defaultOverrides} will include the system system environment variables as
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* {@link #defaultOverrides} will include the system system environment variables as
* {@link #defaultOverrides} will include the system environment variables as

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tnx!

@havocp
Copy link
Collaborator

havocp commented Mar 7, 2019

Oh and thank you for working on this!

@andreaTP
Copy link
Contributor Author

andreaTP commented Mar 8, 2019

@havocp I followed your review and added a little section to the Readme feel free to suggest and re-wording and/or ask to move that section around.
Ready for next round!

@andreaTP
Copy link
Contributor Author

@havocp ping?

@andreaTP
Copy link
Contributor Author

For anyone else interested on the topic, I published Env4Config:
https://github.com/codacy/env4config
Based on the first implementation of this PR.
Until this PR find it's way to upstream it's a road unblocker.

as a side note (maybe for @dwijnand ) I found really misleading the fact that Play doesn't always load configuration in a standard way:
https://github.com/playframework/playframework/blob/ab6697f494a000ba8d19768e9d04eed3bb8cdbf7/core/play/src/main/scala/play/api/Configuration.scala#L73
The workaround there is to use config.url that fortunately is not re-mapped in the Play codebase, not sure if I should open an issue there or something ...

@dwijnand
Copy link
Member

@andreaTP #619 which also about Play's difference in config loading.

Copy link
Collaborator

@havocp havocp left a comment

Choose a reason for hiding this comment

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

I noticed one more thing that invalidateCaches should cover the new cached thing, but otherwise 💯
lgtm

README.md Outdated
i.e. The environment variable `CONFIG_FORCE_a_b__c___d` set the
configuration key `a.b-c_d`

Rationale the name mangling:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd probably leave this section out, if people really care they can look in the code or commit messages (maybe it's worth moving it to one of those places)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved to a comment in the code

}

private static class EnvVariablesOverridesHolder {
static volatile AbstractConfigObject envVariables = loadEnvVariablesOverrides();
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess we need to make ConfigFactory.invalidateCaches clear this

public static void invalidateCaches() {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@andreaTP
Copy link
Contributor Author

ready for next round :-)

@andreaTP
Copy link
Contributor Author

Need to re-trigger the CI

@andreaTP andreaTP closed this Mar 21, 2019
@andreaTP andreaTP reopened this Mar 21, 2019
@andreaTP
Copy link
Contributor Author

Not sure why, but Travis was failing to download sbt-launcher 1.2.7, bumping it to 1.2.8 seems to solve the issue.

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
Copy link
Contributor

Choose a reason for hiding this comment

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

Shell we update/fix the 'Puppy Crawl' comment as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN"
Copy link
Contributor

Choose a reason for hiding this comment

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

And here still revers to Puppy Crawl as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -360,6 +361,71 @@ public static void reloadEnvVariablesConfig() {
EnvVariablesHolder.envVariables = loadEnvVariables();
}

private static AbstractConfigObject loadEnvVariablesOverrides() {
Map<String, String> env = new HashMap(System.getenv());
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't mean to bike shed -- especially as I've seen you've written tests against this. I just though I'd add a suggestion which could (1) perhaps make the different override cases more easily scannable/readable and (2) make the test cases easier:

Instead of conflating the 'System.getenv()' call within the same method which does the override logic, you might consider having something like:

static String envVariableAsProperty(envVariable : String, envVarOverridePrefix : String) : String = {
... your code from line 377 here
}

which you can then just test via:

assertEquals(envVariableAsProperty("PREFIX_A_B__C", "PREFIX_"), "a.b-c")
assertEquals(envVariableAsProperty("PREFIX_A_B___C", "PREFIX_"), "a.b_c")

etc

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it makes totally sense, thanks for pushing me out from my laziness :-)

Copy link
Contributor

@aaronp aaronp left a comment

Choose a reason for hiding this comment

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

Great work - thanks for (1) fixing the puppy crawl issue and (2) adding support for this feature, which obviates the need for my similar, but not as good implementation of the same thing in 'args4c'!

@andreaTP
Copy link
Contributor Author

thanks for the review @aaronp ! ready for next round! :-)

Copy link
Contributor

@aaronp aaronp left a comment

Choose a reason for hiding this comment

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

Great work!

@havocp havocp merged commit e24ff57 into lightbend:master Apr 3, 2019
@havocp
Copy link
Collaborator

havocp commented Apr 3, 2019

Thanks! Sorry I'm so slow on the reviews.

@TimMoore
Copy link

TimMoore commented Apr 4, 2019

@havocp I just wanted to say thank you for continuing to review and merge pull requests at all!

@andreaTP
Copy link
Contributor Author

andreaTP commented Apr 4, 2019

Thanks @havocp ! any ETA on the next release?

@mkurz
Copy link

mkurz commented May 2, 2020

@andreaTP Seems like there is an issue with this feature, please see this bug report: playframework/playframework#10206

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.

8 participants