Open sleberknight opened 1 year ago
I changed this to an investigation because it might be caused by the way the example application is implemented.
It explicitly adds an environment variable StringSubstitutor
in the initialize
of the Application
class before adding the ConsulBundle
, which then adds the Consul substitutor.
So, it might be that whenever there is a default value in a template variable, it uses that when it fails to find a value and doesn't consult any other substitutors. This needs more investigation to determine the exact behavior.
I performed additional investigation using seven different configuration scenarios, and two application runtime configurations.
The two application configurations are:
Application
class only registers a ConsulSubstitutor
(indirectly when adding the ConsulBundle
)Application
class registers an EnvironmentVariableSubstitutor
and then registers a ConsulSubstitutor
(indirectly when adding the ConsulBundle
)The seven configuration scenarios are:
Configuration
class (i.e. a field in the Configuration
class that has a default value)Configuration
class, literal value in YAML config fileConfiguration
class, template value in YAML config, value Consul KV storeConfiguration
class, template value in YAML config with default, value Consul KV storeConfiguration
class, template value in YAML config with defaultConfiguration
class, template value in YAML config with default, value in environment variableConfiguration
class, template value in YAML config with no default, value in environment variableA value in the Configuration
class looks like:
private String template = "Hello there, %s!";
private String defaultName = "Stranger";
The literal value in YAML looks like:
template: Hello there, %s!
defaultValue: Stranger
A template value in YAML looks like:
template: ${helloworld/template}
defaultName: ${helloworld/defaultName}
And, template value in YAML with default value looks like:
template: ${helloworld/template:-Hello, %s!}
defaultName: ${helloworld/defaultName:-Stranger}
The table below shows the results of attempting each of the seven configuration scenarios for the two application configurations.
Scenario | CS | ECS | Final value |
---|---|---|---|
1 | ✔️ | ✔️ | Value of field in Configuration object |
2 | ✔️ | ✔️ | Literal value in YAML |
3 | ✔️ | ✔️ | Value in Consul KV |
4 | ✔️ | ✔️ | Value in Consul KV |
5 | ✔️ | ✔️ | Default value in YAML |
6 | ❌ | ❌ | Default value in YAML (expected to be value of environment variable) |
7 | ✔️ | ✔️ | Value of environment variable |
As is shown in the table, only one scenario fails, which occurs when there is a field in the Configuration
class with a default value, and there is a template value in the YAML config with a default value, and there is an environment variable, but there is no value in Consul KV. The final value is from the default value in the YAML config.
I am fairly sure this occurs because the ConsulSubstitutor
attempts a Consul KV lookup, fails, sees that there is a default value from the YAML config, and just uses that instead of attempting to see if there is an environment variable before falling back to the default value in the YAML. This makes a certain amount of sense, but I would logically expect a "chain" of look-ups to try the first one, then the second one, and so on. For example, you might expect this to behave like:
flowchart LR
ckv(Consul KV) --> ev(Environment Value) --> dvy(Default value in YAML) --> dvcc(Default value in Configuration class)
or maybe you would want to check for the environment variable first and the Consul KV value second:
flowchart LR
ev(Environment Value) --> ckv(Consul KV) --> dvy(Default value in YAML) --> dvcc(Default value in Configuration class)
This kind of chained fallback behavior can be implemented, but not without creating additional code and changing the existing ConsulSubstitutor
. In fact, the chained fallback can be implemented as a StringLookup
e.g.
public class StringLookupChain implements StringLookup {
private final List<StringLookup> lookups;
public StringLookupChain(List<StringLookup> lookups) {
checkArgument(nonNull(lookups) && !lookups.isEmpty(), "lookups must not be null or empty");
this.lookups = lookups;
}
@Override
public String lookup(String key) {
return lookups.stream()
.map(nextLookup -> nextLookup.lookup(key))
.map(Strings::nullToEmpty)
.filter(str -> !str.isEmpty())
.findFirst()
.orElse(null);
}
}
And then the ConsulSubstitutor
constructor can be changed as follows to set a chained variable resolver:
StringLookup envLookup = System::getenv;
var consulLookup = new ConsulLookup(consul, strict);
// In this implementation, environment variables take precedence over Consul KV values
var lookupChain = new StringLookupChain(List.of(envLookup, consulLookup));
this.setVariableResolver(lookupChain);
Summary
When using the
ConsulSubsitutor
to retrieve configuration values from the Consul KV store, there seems to be a problem when there are default values.I used the
consul-example
application (before removing it from this project, and it's obviously still in git history) to test getting values from Consul's KV store. Here is the YAML config corresponding to thetemplate
anddefaultName
properties in the application'sConfiguration
class:This is a snippet from the
hello-world.yml
file that is in the example application.In this configuration,
helloworld/template
andhelloworld/defaultName
are the keys to look up in Consul, andHello, %s!
andStranger
are the default values when not found in Consul.What should happen:
When the application starts, it should find the values for
helloworld/template
andhelloworld/defaultName
in Consul's KV store, and they should be stored in the application'sConfiguration
object at runtime.What actually happens
The default values are in the
Configuration
object, not the values from Consul.This only happens when there are default values specified in the YAML. If the configuration is:
then the values stored in Consul (I used
Hola %s
forhelloworld/template
andCowgirl
forhelloworld/defaultName
) are stored in theConfiguration
object at runtime.Steps to reproduce:
helloworld/template
andhelloworld/defaultName
in Consul's KV store (e.g.Hola, %s
andCowgirl
)GET
request to the/hello-world
endpointThe output will be:
Hello, Stranger!
instead of the expectedHola, Cowgirl
.Notes
I searched through the issues in the original dropwizard-consul project and found this issue: working with default values failing. So, I was not the first person to find this problem. That issue was reported on Jan 2, 2018 and was automatically closed as stale by a GitHub action.