Skip to content

Commit 4631b16

Browse files
Add support for environment config (#2660)
Add environment config
1 parent 5629f0c commit 4631b16

File tree

12 files changed

+1846
-1
lines changed

12 files changed

+1846
-1
lines changed

gradle/jacoco.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies {
1313
jacocoAggregation project(':temporal-spring-boot-starter')
1414
jacocoAggregation project(':temporal-test-server')
1515
jacocoAggregation project(':temporal-testing')
16+
jacocoAggregation project(':temporal-envconfig')
1617
}
1718

1819
def jacocoExclusions = [

settings.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ include 'temporal-kotlin'
99
include 'temporal-spring-boot-autoconfigure'
1010
include 'temporal-spring-boot-starter'
1111
include 'temporal-remote-data-encoder'
12-
include 'temporal-shaded'
12+
include 'temporal-shaded'
13+
include 'temporal-envconfig'

temporal-bom/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ dependencies {
1616
api project(':temporal-spring-boot-starter')
1717
api project(':temporal-test-server')
1818
api project(':temporal-testing')
19+
api project(':temporal-envconfig')
1920
}
2021
}

temporal-envconfig/build.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
description = '''Temporal Java SDK Environment Config Module'''
2+
3+
dependencies {
4+
// this module shouldn't carry temporal-sdk with it, especially for situations when users may be using a shaded artifact
5+
compileOnly project(':temporal-sdk')
6+
7+
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-toml:${jacksonVersion}"
8+
testImplementation project(":temporal-testing")
9+
testImplementation "junit:junit:${junitVersion}"
10+
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
11+
12+
testRuntimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}"
13+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package io.temporal.envconfig;
2+
3+
import com.fasterxml.jackson.databind.ObjectReader;
4+
import com.fasterxml.jackson.databind.ObjectWriter;
5+
import com.fasterxml.jackson.dataformat.toml.TomlMapper;
6+
import io.temporal.common.Experimental;
7+
import java.io.*;
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
import java.util.Objects;
11+
12+
/** ClientConfig represents a client config file. */
13+
@Experimental
14+
public class ClientConfig {
15+
/** Creates a new builder to build a {@link ClientConfig}. */
16+
public static Builder newBuilder() {
17+
return new Builder();
18+
}
19+
20+
/**
21+
* Creates a new builder to build a {@link ClientConfig} based on an existing config.
22+
*
23+
* @param profile the existing profile to base the builder on
24+
* @return a new Builder instance
25+
*/
26+
public static Builder newBuilder(ClientConfig profile) {
27+
return new Builder(profile);
28+
}
29+
30+
/** Returns a default instance of {@link ClientConfig} with all fields unset. */
31+
public static ClientConfig getDefaultInstance() {
32+
return new ClientConfig.Builder().build();
33+
}
34+
35+
/** Get the default config file path: $HOME/.config/temporal/temporal.toml */
36+
private static String getDefaultConfigFilePath() {
37+
String userDir = System.getProperty("user.home");
38+
if (userDir == null || userDir.isEmpty()) {
39+
throw new RuntimeException("failed getting user home directory");
40+
}
41+
return userDir + "/.config/temporal/temporal.toml";
42+
}
43+
44+
/**
45+
* Load all client profiles from given sources.
46+
*
47+
* <p>This does not apply environment variable overrides to the profiles, it only uses an
48+
* environment variable to find the default config file path (TEMPORAL_CONFIG_FILE). To get a
49+
* single profile with environment variables applied, use {@link ClientConfigProfile#load}.
50+
*/
51+
public static ClientConfig load() throws IOException {
52+
return load(LoadClientConfigOptions.newBuilder().build());
53+
}
54+
55+
/**
56+
* Load all client profiles from given sources.
57+
*
58+
* <p>This does not apply environment variable overrides to the profiles, it only uses an
59+
* environment variable to find the default config file path (TEMPORAL_CONFIG_FILE). To get a
60+
* single profile with environment variables applied, use {@link ClientConfigProfile#load}.
61+
*
62+
* @param options options to control loading the config
63+
* @throws IOException if the config file cannot be read or parsed
64+
*/
65+
public static ClientConfig load(LoadClientConfigOptions options) throws IOException {
66+
ObjectReader reader = new TomlMapper().readerFor(ClientConfigToml.TomlClientConfig.class);
67+
if (options.isStrictConfigFile()) {
68+
reader =
69+
reader.withFeatures(
70+
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
71+
} else {
72+
reader =
73+
reader.withoutFeatures(
74+
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
75+
}
76+
if (options.getConfigFileData() != null && options.getConfigFileData().length > 0) {
77+
if (options.getConfigFilePath() != null && !options.getConfigFilePath().isEmpty()) {
78+
throw new IllegalArgumentException(
79+
"Cannot have both ConfigFileData and ConfigFilePath set");
80+
}
81+
ClientConfigToml.TomlClientConfig result = reader.readValue(options.getConfigFileData());
82+
return new ClientConfig(ClientConfigToml.getClientProfiles(result));
83+
} else {
84+
// Get file name which is either set value, env var, or default path
85+
String file = options.getConfigFilePath();
86+
if (file == null || file.isEmpty()) {
87+
Map<String, String> env = options.getEnvOverrides();
88+
if (env == null) {
89+
env = System.getenv();
90+
}
91+
// Unlike env vars for the config values, empty and unset env var
92+
// for config file path are both treated as unset
93+
file = env.get("TEMPORAL_CONFIG_FILE");
94+
}
95+
if (file == null || file.isEmpty()) {
96+
file = getDefaultConfigFilePath();
97+
}
98+
ClientConfigToml.TomlClientConfig result = reader.readValue(new File(file));
99+
return new ClientConfig(ClientConfigToml.getClientProfiles(result));
100+
}
101+
}
102+
103+
/**
104+
* Load client config from given TOML data.
105+
*
106+
* @param tomlData TOML data to parse
107+
* @return the parsed client config
108+
* @throws IOException if the TOML data cannot be parsed
109+
*/
110+
public static ClientConfig fromToml(byte[] tomlData) throws IOException {
111+
return fromToml(tomlData, ClientConfigFromTomlOptions.getDefaultInstance());
112+
}
113+
114+
/**
115+
* Load client config from given TOML data.
116+
*
117+
* @param tomlData TOML data to parse
118+
* @param options options to control parsing the TOML data
119+
* @return the parsed client config
120+
* @throws IOException if the TOML data cannot be parsed
121+
*/
122+
public static ClientConfig fromToml(byte[] tomlData, ClientConfigFromTomlOptions options)
123+
throws IOException {
124+
ObjectReader reader = new TomlMapper().readerFor(ClientConfigToml.TomlClientConfig.class);
125+
if (options.isStrictConfigFile()) {
126+
reader =
127+
reader.withFeatures(
128+
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
129+
} else {
130+
reader =
131+
reader.withoutFeatures(
132+
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
133+
}
134+
ClientConfigToml.TomlClientConfig result = reader.readValue(tomlData);
135+
return new ClientConfig(ClientConfigToml.getClientProfiles(result));
136+
}
137+
138+
/**
139+
* Convert the client config to TOML data. Encoding is UTF-8.
140+
*
141+
* @param config the client config to convert
142+
* @return the TOML data as bytes
143+
* @apiNote The output will not be identical to the input if the config was loaded from a file
144+
* because comments and formatting are not preserved.
145+
*/
146+
public static byte[] toTomlAsBytes(ClientConfig config) throws IOException {
147+
ObjectWriter writer = new TomlMapper().writerFor(ClientConfigToml.TomlClientConfig.class);
148+
return writer.writeValueAsBytes(
149+
new ClientConfigToml.TomlClientConfig(
150+
ClientConfigToml.fromClientProfiles(config.getProfiles())));
151+
}
152+
153+
private ClientConfig(Map<String, ClientConfigProfile> profiles) {
154+
this.profiles = profiles;
155+
}
156+
157+
private final Map<String, ClientConfigProfile> profiles;
158+
159+
/** All profiles loaded from the config file, may be empty but never null. */
160+
public Map<String, ClientConfigProfile> getProfiles() {
161+
return new HashMap<>(profiles);
162+
}
163+
164+
@Override
165+
public boolean equals(Object o) {
166+
if (o == null || getClass() != o.getClass()) return false;
167+
ClientConfig that = (ClientConfig) o;
168+
return Objects.equals(profiles, that.profiles);
169+
}
170+
171+
@Override
172+
public int hashCode() {
173+
return Objects.hashCode(profiles);
174+
}
175+
176+
@Override
177+
public String toString() {
178+
return "ClientConfig{" + "profiles=" + profiles + '}';
179+
}
180+
181+
public static final class Builder {
182+
private final Map<String, ClientConfigProfile> profiles;
183+
184+
public Builder(ClientConfig config) {
185+
this.profiles = config.getProfiles();
186+
}
187+
188+
public Builder() {
189+
this.profiles = new HashMap<>();
190+
}
191+
192+
public Builder putProfile(String name, ClientConfigProfile profile) {
193+
profiles.put(name, profile);
194+
return this;
195+
}
196+
197+
public ClientConfig build() {
198+
return new ClientConfig(profiles);
199+
}
200+
}
201+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.temporal.envconfig;
2+
3+
/**
4+
* Options for parsing a client config toml file in {@link ClientConfig#fromToml(byte[],
5+
* ClientConfigFromTomlOptions)}
6+
*/
7+
public class ClientConfigFromTomlOptions {
8+
/** Create a builder for {@link ClientConfigFromTomlOptions}. */
9+
public static Builder newBuilder() {
10+
return new ClientConfigFromTomlOptions.Builder();
11+
}
12+
13+
/** Create a builder from an existing {@link ClientConfigFromTomlOptions}. */
14+
public static Builder newBuilder(ClientConfigFromTomlOptions options) {
15+
return new Builder(options);
16+
}
17+
18+
/** Returns a default instance of {@link ClientConfigFromTomlOptions} with all fields unset. */
19+
public static ClientConfigFromTomlOptions getDefaultInstance() {
20+
return new ClientConfigFromTomlOptions.Builder().build();
21+
}
22+
23+
private final Boolean strictConfigFile;
24+
25+
private ClientConfigFromTomlOptions(Boolean strictConfigFile) {
26+
this.strictConfigFile = strictConfigFile;
27+
}
28+
29+
public Boolean isStrictConfigFile() {
30+
return strictConfigFile;
31+
}
32+
33+
public static class Builder {
34+
private Boolean strictConfigFile = false;
35+
36+
private Builder() {}
37+
38+
private Builder(ClientConfigFromTomlOptions options) {
39+
this.strictConfigFile = options.strictConfigFile;
40+
}
41+
42+
/**
43+
* When true, the parser will fail if the config file contains unknown fields. Default is false.
44+
*/
45+
public Builder setStrictConfigFile(Boolean strictConfigFile) {
46+
this.strictConfigFile = strictConfigFile;
47+
return this;
48+
}
49+
50+
public ClientConfigFromTomlOptions build() {
51+
return new ClientConfigFromTomlOptions(strictConfigFile);
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)