helidon
helidon copied to clipboard
[4.x] Dynamic config source
Environment Details
- Helidon Version: 4.0.6
- Helidon SE
Problem Description
Provide a way to create a config source with values wrapped by suppliers, where the suppliers are invoked every time the value is requested.
Use case
An Helidon example demonstrates how to use a client to connect to a remote service, however the test will mock the remote service using the current server.
We can use config to configure the URI to use for the remote service, the main config hard-codes the URI to use.
I.e. application.yaml:
server:
port: 8080
host: 0.0.0.0
app:
client-uri: http://localhost:8080
We use Config.global()
in the HttpService
to initialize the WebClient
instance lazily:
class MyService implements HttpService {
private final LazyValue<WebClient> clientSupplier = LazyValue.create(() -> WebClient.builder()
.baseUri(Config.global().get("app.client-uri").asString().get())
.build());
@Override
public void routing(HttpRules rules) {
rules.get((req, res) -> {
WebClient client = clientSupplier.get();
res.send(client.get().requestEntity(String.class));
});
}
}
However the test uses a dynamically allocated port, and the routing is initialized before the server is started (meaning that the port is not yet known).
@ServerTest
public class MyServiceTest {
static String serverUri;
@BeforeAll
static void beforeAll(URI uri) {
serverUri = uri.toString();
}
@SetUpRoute
static void routing(HttpRouting.Builder routing) {
Config.global(Config.create(
// wrap the value of serverUri with a supplier
DynamicConfigSources.create(Map.of("app.client-uri", () -> serverUri)),
ConfigSources.classpath("application.yaml"),
ConfigSources.classpath("application-test.yaml")
));
routing.register(new MyService());
}
}
See a simplistic implementation of DynamicConfigSources
:
public class DynamicConfigSources {
public static ConfigSource create(Map<String, Supplier<String>> map) {
NodeContent content = NodeContent.builder()
.node(new DynamicContentNode(map))
.build();
return (NodeConfigSource) () -> Optional.of(content);
}
private static final class DynamicObjectNode extends AbstractMap<String, ConfigNode>
implements ConfigNode.ObjectNode {
private final Map<String, Supplier<String>> delegate;
private DynamicObjectNode(Map<String, Supplier<String>> delegate) {
this.delegate = delegate;
}
@Override
public Optional<String> value() {
return Optional.of(toString());
}
@Override
public Set<Entry<String, ConfigNode>> entrySet() {
return delegate.entrySet().stream()
.map(e -> Map.entry(e.getKey(), (ConfigNode) new ValueNodeImpl(null) {
@Override
public Optional<String> value() {
return Optional.ofNullable(e.getValue().get());
}
@Override
public String get() {
String s = e.getValue().get();
return s != null ? s : "?";
}
}))
.collect(Collectors.toSet());
}
}
}
Note that the code above only works because ValueNodeImpl
is public and non-final. This allows us to create an implementation that is backed by Supplier<String>
instead of String
. Also the code above does not handle list values.
In a previous discussion of dynamic config sources we came up these characteristics (which go beyond what is requested in this issue):
- The set of keys is unbounded and not known in advance
- Lookups are expensive
- Caching of values and control of caching policies is important
- Defining the key space (with key patterns or prefixes) is important to guard against spurious lookups and to define the range of keys supported by the config source
- Values in the config source may change at any time via external mechanisms
Note that the secrets-config-source also dabbles in this area.
Related issue: #7745