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 Environment Variable DynamicConfigProvider #11377

Conversation

bananaaggle
Copy link
Contributor

@bananaaggle bananaaggle commented Jun 22, 2021

As #9351 described, DynamicConfigProvider should replace PasswordProvider. PasswordProvider has an implementation named EnvironmentVariablePasswordProvider, which allow users to read config from environment variable. This PR provides a same implementation for DynamicConfigProvider. There is an example for its usage:

"DynamicConfigProvider": {
  "type": "environment",
  "variables":
         {
            "userNameVariable": "MY_USER_NAME_VAR",
            "passwordVariable": "MY_PASSWORD_VAR"
         }
}

This PR has:

  • been self-reviewed.
  • added documentation for new or modified features or behaviors.
  • added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
  • added or updated version, license, or notice information in licenses.yaml
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold for code coverage is met.
  • added integration tests.
  • been tested in a test Druid cluster.

Copy link
Contributor

@jihoonson jihoonson left a comment

Choose a reason for hiding this comment

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

@bananaaggle thanks for the PR. I left some comments.

@JsonProperty("variables") Map<String, String> config
)
{
this.variables = ImmutableMap.copyOf(config);
Copy link
Contributor

Choose a reason for hiding this comment

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

config can be null if there is something wrong in the configuration. This will throw NPE in that case. It would be better to tell what is wrong than NPE with no message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

import java.util.Map;
import java.util.Objects;

public class EnvironmentVariableDynamicConfigProvider implements DynamicConfigProvider
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public class EnvironmentVariableDynamicConfigProvider implements DynamicConfigProvider
public class EnvironmentVariableDynamicConfigProvider implements DynamicConfigProvider<String>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

}

@Override
public Map getConfig()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public Map getConfig()
public Map<String> getConfig()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@Override
public int hashCode()
{
return variables != null ? variables.hashCode() : 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

variables doesn't seem to be able to null.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

Comment on lines 43 to 49
try {
this.variables = ImmutableMap.copyOf(config);
}
catch (NullPointerException e) {
log.error(e, "Can not parse config by EnvironmentVariableDynamicConfigProvider! Please check your config file.");
throw e;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to propagate the error to users so that they don't have to look up logs when this happens.

Suggested change
try {
this.variables = ImmutableMap.copyOf(config);
}
catch (NullPointerException e) {
log.error(e, "Can not parse config by EnvironmentVariableDynamicConfigProvider! Please check your config file.");
throw e;
}
this.variables = ImmutableMap.copyOf(Preconditions.checkNotNull(config, "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.

fixed

Copy link
Member

@clintropolis clintropolis left a comment

Choose a reason for hiding this comment

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

👍


private static final Logger log = new Logger(EnvironmentVariableDynamicConfigProvider.class);

private ImmutableMap<String, String> variables;
Copy link
Member

Choose a reason for hiding this comment

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

nit: this can be final

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

@jihoonson
Copy link
Contributor

@bananaaggle thanks for the quick response. The code change LGTM, but there are 2 things missing here, one is documentation and another is tests. For documentation, I think you can add some in docs/operations/dynamic-config-provider.md. For tests, you can add some unit tests in EnvironmentVariableDynamicConfigProviderTest that should run with a proper set of environment variables. To let Travis CI run those new tests successfully, you will need to add those environment variables in .travis.yaml too. I can help you with adding these, let me know if you have questions.

@bananaaggle
Copy link
Contributor Author

@bananaaggle thanks for the quick response. The code change LGTM, but there are 2 things missing here, one is documentation and another is tests. For documentation, I think you can add some in docs/operations/dynamic-config-provider.md. For tests, you can add some unit tests in EnvironmentVariableDynamicConfigProviderTest that should run with a proper set of environment variables. To let Travis CI run those new tests successfully, you will need to add those environment variables in .travis.yaml too. I can help you with adding these, let me know if you have questions.

Document added. I'll add more tests in week.

@bananaaggle
Copy link
Contributor Author

@bananaaggle thanks for the quick response. The code change LGTM, but there are 2 things missing here, one is documentation and another is tests. For documentation, I think you can add some in docs/operations/dynamic-config-provider.md. For tests, you can add some unit tests in EnvironmentVariableDynamicConfigProviderTest that should run with a proper set of environment variables. To let Travis CI run those new tests successfully, you will need to add those environment variables in .travis.yaml too. I can help you with adding these, let me know if you have questions.

Hi, @jihoonson. I add unit test for getConfig(). To set environment variable, I find a method named setEnv(Map<String, String> newenv) in stackoverflow which can change jvm environment variable by reflection. Do you think it is a proper solution? Is necessary to add environment variables in .travis.yaml? If add environment variables in .travis.yaml, this unit test will work in travis, but users can not pass it without travis. What unit tests you think is necessary to be added in next step?

Copy link
Contributor

@jihoonson jihoonson left a comment

Choose a reason for hiding this comment

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

@bananaaggle thank you for adding a documentation! I left some comments on it.

For unit tests, I was thinking of some tests that should run with a set of certain environment variables. So, for Travis, those variables should be set in .travis.yaml. To run those tests locally, those variables should be set and passed properly to the process that runs the tests. I thought this would be the easiest way to add tests, but if you think there is a better way, I'm OK with it as long as those tests are not flaky.


`EnvironmentVariableDynamicConfigProvider` can be used to replace `EnvironmentVariablePasswordProvider`. This class allow users to avoid exposing passwords or other secret information in the runtime.properties file. You can set environment variables in the following example:
```json
druid.metadata.storage.connector.dynamicConfigProvider={"type": "environment","variables":{"user": "MY_USER_NAME_VAR","password": "MY_PASSWORD_VAR"}
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't support dynamicConfig for metadata yet.

Suggested change
druid.metadata.storage.connector.dynamicConfigProvider={"type": "environment","variables":{"user": "MY_USER_NAME_VAR","password": "MY_PASSWORD_VAR"}
druid.some.config.dynamicConfigProvider={"type": "environment","variables":{"secret1": "SECRET1_VAR","secret2": "SECRET2_VAR"}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. In #11389, I make a design to replace PasswordProvider. I think it can work with metadata and other classes which use PasswordProvider.

Copy link
Contributor

Choose a reason for hiding this comment

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

Cool, I will take a look at that PR too. Thanks!

@@ -31,3 +31,16 @@ Users can create custom extension of the `DynamicConfigProvider` interface that

For more information, see [Adding a new DynamicConfigProvider implementation](../development/modules.md#adding-a-new-dynamicconfigprovider-implementation).

## EnvironmentVariableDynamicConfigProvider

`EnvironmentVariableDynamicConfigProvider` can be used to replace `EnvironmentVariablePasswordProvider`. This class allow users to avoid exposing passwords or other secret information in the runtime.properties file. You can set environment variables in the following example:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
`EnvironmentVariableDynamicConfigProvider` can be used to replace `EnvironmentVariablePasswordProvider`. This class allow users to avoid exposing passwords or other secret information in the runtime.properties file. You can set environment variables in the following example:
`EnvironmentVariableDynamicConfigProvider` can be used to avoid exposing credentials or other secret information in the configuration files using environment variables. An example to use this configProvider is:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Contributor

@jihoonson jihoonson left a comment

Choose a reason for hiding this comment

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

Hey @bananaaggle, thank you for adding unit tests. I have some concerns about the new code added. Please see my comments.

theEnvironmentField.setAccessible(true);
Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null);
env.putAll(newenv);
Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment");
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this theCaseInsensitiveEnvironment variable? I don't see it in java.lang.ProcessEnvironment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

Comment on lines 77 to 91
catch (NoSuchFieldException e) {
Class[] classes = Collections.class.getDeclaredClasses();
Map<String, String> env = System.getenv();
for (Class cl : classes) {
if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) {
Field field = cl.getDeclaredField("m");
field.setAccessible(true);
Object obj = field.get(env);
Map<String, String> map = (Map<String, String>) obj;
map.clear();
map.putAll(newenv);
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you elaborate more on what this catch clause is doing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

Assert.assertEquals("123", ((EnvironmentVariableDynamicConfigProvider) provider).getConfig().get("password"));
}

protected static void setEnv(Map<String, String> newenv) throws Exception
Copy link
Contributor

Choose a reason for hiding this comment

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

This method seems to have a couple of issues.

  1. This method seems to be copied from the code snippet in https://stackoverflow.com/a/7201825/4127682. If this is true, we cannot use it directly because it violates the ASF license policy. You can get some hint from the stack overflow, but should not copy from it.
  2. As noted in the stack overflow link, the environment variables should be reset because they are shared by all tests run by the same JVM process.
  3. It would be nice to add some comments to help others understand the code. I left some comments for this.

Copy link
Contributor Author

@bananaaggle bananaaggle Jul 12, 2021

Choose a reason for hiding this comment

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

I've changed this unit test. Separated setting and getting environment variables. I use a map to record which environment variables are changed and add a method to recover those system environment variables after test. Add comment for getENVMap().

@bananaaggle
Copy link
Contributor Author

Hi @jihoonson! I've changed this unit test but CI tests failed. It seems caused by travis error, can you help me rerun it? And is there something in code needed to change? Thanks!

@jihoonson
Copy link
Contributor

@bananaaggle sorry, I forgot about this PR. I restarted the timed-out tests. Will finish my review today.

Copy link
Contributor

@jihoonson jihoonson left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks @bananaaggle!

@jihoonson jihoonson merged commit 23d7d71 into apache:master Aug 5, 2021
@bananaaggle
Copy link
Contributor Author

LGTM. Thanks @bananaaggle!

Thank you very much for review! In 11389, I make a design to replace PasswordProvider, but I'm not sure this design is proper or not. Can you help review it? If design is proper, I will involve more classes which use PasswordProvider in that PR. I also comment about it in 9351.

@clintropolis clintropolis added this to the 0.22.0 milestone Aug 12, 2021
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.

None yet

3 participants