diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle
index 7a738f17ef..43bf9075a8 100644
--- a/gradle/jacoco.gradle
+++ b/gradle/jacoco.gradle
@@ -13,6 +13,7 @@ dependencies {
jacocoAggregation project(':temporal-spring-boot-starter')
jacocoAggregation project(':temporal-test-server')
jacocoAggregation project(':temporal-testing')
+ jacocoAggregation project(':temporal-envconfig')
}
def jacocoExclusions = [
diff --git a/settings.gradle b/settings.gradle
index cca3ef0b82..cecb189451 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -9,4 +9,5 @@ include 'temporal-kotlin'
include 'temporal-spring-boot-autoconfigure'
include 'temporal-spring-boot-starter'
include 'temporal-remote-data-encoder'
-include 'temporal-shaded'
\ No newline at end of file
+include 'temporal-shaded'
+include 'temporal-envconfig'
\ No newline at end of file
diff --git a/temporal-bom/build.gradle b/temporal-bom/build.gradle
index bc4d5b0500..8f5a8971d2 100644
--- a/temporal-bom/build.gradle
+++ b/temporal-bom/build.gradle
@@ -16,5 +16,6 @@ dependencies {
api project(':temporal-spring-boot-starter')
api project(':temporal-test-server')
api project(':temporal-testing')
+ api project(':temporal-envconfig')
}
}
diff --git a/temporal-envconfig/build.gradle b/temporal-envconfig/build.gradle
new file mode 100644
index 0000000000..2adfdcfc2d
--- /dev/null
+++ b/temporal-envconfig/build.gradle
@@ -0,0 +1,13 @@
+description = '''Temporal Java SDK Environment Config Module'''
+
+dependencies {
+ // this module shouldn't carry temporal-sdk with it, especially for situations when users may be using a shaded artifact
+ compileOnly project(':temporal-sdk')
+
+ implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-toml:${jacksonVersion}"
+ testImplementation project(":temporal-testing")
+ testImplementation "junit:junit:${junitVersion}"
+ testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+
+ testRuntimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}"
+}
\ No newline at end of file
diff --git a/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfig.java b/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfig.java
new file mode 100644
index 0000000000..c39fe86612
--- /dev/null
+++ b/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfig.java
@@ -0,0 +1,201 @@
+package io.temporal.envconfig;
+
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.dataformat.toml.TomlMapper;
+import io.temporal.common.Experimental;
+import java.io.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/** ClientConfig represents a client config file. */
+@Experimental
+public class ClientConfig {
+ /** Creates a new builder to build a {@link ClientConfig}. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /**
+ * Creates a new builder to build a {@link ClientConfig} based on an existing config.
+ *
+ * @param profile the existing profile to base the builder on
+ * @return a new Builder instance
+ */
+ public static Builder newBuilder(ClientConfig profile) {
+ return new Builder(profile);
+ }
+
+ /** Returns a default instance of {@link ClientConfig} with all fields unset. */
+ public static ClientConfig getDefaultInstance() {
+ return new ClientConfig.Builder().build();
+ }
+
+ /** Get the default config file path: $HOME/.config/temporal/temporal.toml */
+ private static String getDefaultConfigFilePath() {
+ String userDir = System.getProperty("user.home");
+ if (userDir == null || userDir.isEmpty()) {
+ throw new RuntimeException("failed getting user home directory");
+ }
+ return userDir + "/.config/temporal/temporal.toml";
+ }
+
+ /**
+ * Load all client profiles from given sources.
+ *
+ *
This does not apply environment variable overrides to the profiles, it only uses an
+ * environment variable to find the default config file path (TEMPORAL_CONFIG_FILE). To get a
+ * single profile with environment variables applied, use {@link ClientConfigProfile#load}.
+ */
+ public static ClientConfig load() throws IOException {
+ return load(LoadClientConfigOptions.newBuilder().build());
+ }
+
+ /**
+ * Load all client profiles from given sources.
+ *
+ *
This does not apply environment variable overrides to the profiles, it only uses an
+ * environment variable to find the default config file path (TEMPORAL_CONFIG_FILE). To get a
+ * single profile with environment variables applied, use {@link ClientConfigProfile#load}.
+ *
+ * @param options options to control loading the config
+ * @throws IOException if the config file cannot be read or parsed
+ */
+ public static ClientConfig load(LoadClientConfigOptions options) throws IOException {
+ ObjectReader reader = new TomlMapper().readerFor(ClientConfigToml.TomlClientConfig.class);
+ if (options.isStrictConfigFile()) {
+ reader =
+ reader.withFeatures(
+ com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ } else {
+ reader =
+ reader.withoutFeatures(
+ com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ }
+ if (options.getConfigFileData() != null && options.getConfigFileData().length > 0) {
+ if (options.getConfigFilePath() != null && !options.getConfigFilePath().isEmpty()) {
+ throw new IllegalArgumentException(
+ "Cannot have both ConfigFileData and ConfigFilePath set");
+ }
+ ClientConfigToml.TomlClientConfig result = reader.readValue(options.getConfigFileData());
+ return new ClientConfig(ClientConfigToml.getClientProfiles(result));
+ } else {
+ // Get file name which is either set value, env var, or default path
+ String file = options.getConfigFilePath();
+ if (file == null || file.isEmpty()) {
+ Map env = options.getEnvOverrides();
+ if (env == null) {
+ env = System.getenv();
+ }
+ // Unlike env vars for the config values, empty and unset env var
+ // for config file path are both treated as unset
+ file = env.get("TEMPORAL_CONFIG_FILE");
+ }
+ if (file == null || file.isEmpty()) {
+ file = getDefaultConfigFilePath();
+ }
+ ClientConfigToml.TomlClientConfig result = reader.readValue(new File(file));
+ return new ClientConfig(ClientConfigToml.getClientProfiles(result));
+ }
+ }
+
+ /**
+ * Load client config from given TOML data.
+ *
+ * @param tomlData TOML data to parse
+ * @return the parsed client config
+ * @throws IOException if the TOML data cannot be parsed
+ */
+ public static ClientConfig fromToml(byte[] tomlData) throws IOException {
+ return fromToml(tomlData, ClientConfigFromTomlOptions.getDefaultInstance());
+ }
+
+ /**
+ * Load client config from given TOML data.
+ *
+ * @param tomlData TOML data to parse
+ * @param options options to control parsing the TOML data
+ * @return the parsed client config
+ * @throws IOException if the TOML data cannot be parsed
+ */
+ public static ClientConfig fromToml(byte[] tomlData, ClientConfigFromTomlOptions options)
+ throws IOException {
+ ObjectReader reader = new TomlMapper().readerFor(ClientConfigToml.TomlClientConfig.class);
+ if (options.isStrictConfigFile()) {
+ reader =
+ reader.withFeatures(
+ com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ } else {
+ reader =
+ reader.withoutFeatures(
+ com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ }
+ ClientConfigToml.TomlClientConfig result = reader.readValue(tomlData);
+ return new ClientConfig(ClientConfigToml.getClientProfiles(result));
+ }
+
+ /**
+ * Convert the client config to TOML data. Encoding is UTF-8.
+ *
+ * @param config the client config to convert
+ * @return the TOML data as bytes
+ * @apiNote The output will not be identical to the input if the config was loaded from a file
+ * because comments and formatting are not preserved.
+ */
+ public static byte[] toTomlAsBytes(ClientConfig config) throws IOException {
+ ObjectWriter writer = new TomlMapper().writerFor(ClientConfigToml.TomlClientConfig.class);
+ return writer.writeValueAsBytes(
+ new ClientConfigToml.TomlClientConfig(
+ ClientConfigToml.fromClientProfiles(config.getProfiles())));
+ }
+
+ private ClientConfig(Map profiles) {
+ this.profiles = profiles;
+ }
+
+ private final Map profiles;
+
+ /** All profiles loaded from the config file, may be empty but never null. */
+ public Map getProfiles() {
+ return new HashMap<>(profiles);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ ClientConfig that = (ClientConfig) o;
+ return Objects.equals(profiles, that.profiles);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(profiles);
+ }
+
+ @Override
+ public String toString() {
+ return "ClientConfig{" + "profiles=" + profiles + '}';
+ }
+
+ public static final class Builder {
+ private final Map profiles;
+
+ public Builder(ClientConfig config) {
+ this.profiles = config.getProfiles();
+ }
+
+ public Builder() {
+ this.profiles = new HashMap<>();
+ }
+
+ public Builder putProfile(String name, ClientConfigProfile profile) {
+ profiles.put(name, profile);
+ return this;
+ }
+
+ public ClientConfig build() {
+ return new ClientConfig(profiles);
+ }
+ }
+}
diff --git a/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigFromTomlOptions.java b/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigFromTomlOptions.java
new file mode 100644
index 0000000000..fb833e47a6
--- /dev/null
+++ b/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigFromTomlOptions.java
@@ -0,0 +1,54 @@
+package io.temporal.envconfig;
+
+/**
+ * Options for parsing a client config toml file in {@link ClientConfig#fromToml(byte[],
+ * ClientConfigFromTomlOptions)}
+ */
+public class ClientConfigFromTomlOptions {
+ /** Create a builder for {@link ClientConfigFromTomlOptions}. */
+ public static Builder newBuilder() {
+ return new ClientConfigFromTomlOptions.Builder();
+ }
+
+ /** Create a builder from an existing {@link ClientConfigFromTomlOptions}. */
+ public static Builder newBuilder(ClientConfigFromTomlOptions options) {
+ return new Builder(options);
+ }
+
+ /** Returns a default instance of {@link ClientConfigFromTomlOptions} with all fields unset. */
+ public static ClientConfigFromTomlOptions getDefaultInstance() {
+ return new ClientConfigFromTomlOptions.Builder().build();
+ }
+
+ private final Boolean strictConfigFile;
+
+ private ClientConfigFromTomlOptions(Boolean strictConfigFile) {
+ this.strictConfigFile = strictConfigFile;
+ }
+
+ public Boolean isStrictConfigFile() {
+ return strictConfigFile;
+ }
+
+ public static class Builder {
+ private Boolean strictConfigFile = false;
+
+ private Builder() {}
+
+ private Builder(ClientConfigFromTomlOptions options) {
+ this.strictConfigFile = options.strictConfigFile;
+ }
+
+ /**
+ * When true, the parser will fail if the config file contains unknown fields. Default is false.
+ */
+ public Builder setStrictConfigFile(Boolean strictConfigFile) {
+ this.strictConfigFile = strictConfigFile;
+ return this;
+ }
+
+ public ClientConfigFromTomlOptions build() {
+ return new ClientConfigFromTomlOptions(strictConfigFile);
+ }
+ }
+}
diff --git a/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigProfile.java b/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigProfile.java
new file mode 100644
index 0000000000..b051602791
--- /dev/null
+++ b/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigProfile.java
@@ -0,0 +1,483 @@
+package io.temporal.envconfig;
+
+import io.grpc.Metadata;
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
+import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.common.Experimental;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.Objects;
+
+/** ClientConfigProfile is profile-level configuration for a client. */
+@Experimental
+public class ClientConfigProfile {
+ /** Creates a new builder to build a {@link ClientConfigProfile}. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /**
+ * Creates a new builder to build a {@link ClientConfigProfile} based on an existing profile.
+ *
+ * @param profile the existing profile to base the builder on
+ * @return a new Builder instance
+ */
+ public static Builder newBuilder(ClientConfigProfile profile) {
+ return new Builder(profile);
+ }
+
+ /** Returns a default instance of {@link ClientConfigProfile} with all fields unset. */
+ public static ClientConfigProfile getDefaultInstance() {
+ return new Builder().build();
+ }
+
+ /**
+ * Load a client profile from given sources, applying environment variable overrides.
+ *
+ * This is a convenience method for {@link #load(LoadClientConfigProfileOptions)} with default
+ * options.
+ *
+ * @throws IOException if the config file cannot be read or parsed
+ */
+ public static ClientConfigProfile load() throws IOException {
+ return load(LoadClientConfigProfileOptions.newBuilder().build());
+ }
+
+ /**
+ * Load a client profile from given sources, applying environment variable overrides.
+ *
+ * @param options options to control loading the profile
+ * @throws IOException if the config file cannot be read or parsed
+ */
+ public static ClientConfigProfile load(LoadClientConfigProfileOptions options)
+ throws IOException {
+ if (options.isDisableFile() && options.isDisableEnv()) {
+ throw new IllegalArgumentException("Cannot have both DisableFile and DisableEnv set to true");
+ }
+ Map env = System.getenv();
+ if (options.getEnvOverrides() != null) {
+ env = options.getEnvOverrides();
+ }
+ ClientConfigProfile clientConfigProfile = ClientConfigProfile.getDefaultInstance();
+ // If file is enabled, load it and find just the profile
+ if (!options.isDisableFile()) {
+ ClientConfig conf =
+ ClientConfig.load(
+ LoadClientConfigOptions.newBuilder()
+ .setConfigFilePath(options.getConfigFilePath())
+ .setConfigFileData(options.getConfigFileData())
+ .setStrictConfigFile(options.isConfigFileStrict())
+ .setEnvOverrides(options.getEnvOverrides())
+ .build());
+ String profile = options.getConfigFileProfile();
+ boolean profileUnset = false;
+ if (profile == null || profile.isEmpty()) {
+ profile = env.get("TEMPORAL_PROFILE");
+ }
+ if (profile == null || profile.isEmpty()) {
+ profile = "default";
+ profileUnset = true;
+ }
+ ClientConfigProfile fileClientConfigProfile = conf.getProfiles().get(profile);
+ if (fileClientConfigProfile != null) {
+ clientConfigProfile = fileClientConfigProfile;
+ } else if (!profileUnset) {
+ throw new IllegalArgumentException("Unable to find profile " + profile + " in config data");
+ }
+ }
+ // If env is enabled, apply it on top of an empty profile
+ if (!options.isDisableEnv()) {
+ clientConfigProfile.applyEnvOverrides(options.getEnvOverrides());
+ }
+ return clientConfigProfile;
+ }
+
+ private String namespace;
+ private String address;
+ private String apiKey;
+ private Metadata metadata;
+ private ClientConfigTLS tls;
+
+ private ClientConfigProfile(
+ String namespace, String address, String apiKey, Metadata metadata, ClientConfigTLS tls) {
+ this.namespace = namespace;
+ this.address = address;
+ this.apiKey = apiKey;
+ this.metadata = metadata;
+ this.tls = tls;
+ }
+
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ /**
+ * Converts this profile to WorkflowServiceStubsOptions. Note that not all fields are converted,
+ * only those relevant to service stubs.
+ */
+ public WorkflowServiceStubsOptions toWorkflowServiceStubsOptions() {
+ WorkflowServiceStubsOptions.Builder builder = WorkflowServiceStubsOptions.newBuilder();
+ if (this.address != null && !this.address.isEmpty()) {
+ builder.setTarget(this.address);
+ }
+ if (this.apiKey != null && !this.apiKey.isEmpty()) {
+ builder.addApiKey(() -> this.apiKey);
+ }
+ if (this.metadata != null) {
+ builder.addGrpcMetadataProvider(() -> this.metadata);
+ }
+ if (this.tls != null && (this.tls.isDisabled() == null || !this.tls.isDisabled())) {
+ InputStream clientCertStream = null;
+ InputStream keyFileStream = null;
+ InputStream trustCertCollectionInputStream = null;
+ try {
+ if (this.tls.getClientCertPath() != null && !this.tls.getClientCertPath().isEmpty()) {
+ clientCertStream = Files.newInputStream(Paths.get(this.tls.getClientCertPath()));
+ } else if (this.tls.getClientCertData() != null
+ && this.tls.getClientCertData().length > 0) {
+ clientCertStream = new ByteArrayInputStream(this.tls.getClientCertData());
+ }
+
+ if (this.tls.getClientKeyPath() != null && !this.tls.getClientKeyPath().isEmpty()) {
+ keyFileStream = Files.newInputStream(Paths.get(this.tls.getClientKeyPath()));
+ } else if (this.tls.getClientKeyData() != null && this.tls.getClientKeyData().length > 0) {
+ keyFileStream = new ByteArrayInputStream(this.tls.getClientKeyData());
+ }
+
+ if (this.tls.getServerCACertPath() != null && !this.tls.getServerCACertPath().isEmpty()) {
+ trustCertCollectionInputStream =
+ Files.newInputStream(Paths.get(this.tls.getServerCACertPath()));
+ } else if (this.tls.getServerCACertData() != null
+ && this.tls.getServerCACertData().length > 0) {
+ trustCertCollectionInputStream = new ByteArrayInputStream(this.tls.getServerCACertData());
+ }
+
+ SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient();
+ if (trustCertCollectionInputStream != null) {
+ sslContextBuilder.trustManager(trustCertCollectionInputStream);
+ } else if (Boolean.TRUE.equals(this.tls.isDisableHostVerification())) {
+ sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
+ }
+ sslContextBuilder.keyManager(clientCertStream, keyFileStream);
+ builder.setSslContext(sslContextBuilder.build());
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to create SSL context", e);
+ } finally {
+ if (clientCertStream != null) {
+ try {
+ clientCertStream.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ if (keyFileStream != null) {
+ try {
+ keyFileStream.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ if (trustCertCollectionInputStream != null) {
+ try {
+ trustCertCollectionInputStream.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+ if (this.tls.getServerName() != null && !this.tls.getServerName().isEmpty()) {
+ builder.setChannelInitializer(c -> c.overrideAuthority(this.tls.getServerName()));
+ }
+ } else if (this.apiKey != null && !this.apiKey.isEmpty()) {
+ if (this.getTls() == null
+ || this.tls.isDisabled() == null
+ || (this.tls.isDisabled() != null && !this.tls.isDisabled())) {
+ // If API key is set, TLS is required, so enable it with defaults
+ builder.setEnableHttps(true);
+ }
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Converts this profile to WorkflowClientOptions. Note that not all fields are converted, only
+ * those relevant to client options.
+ */
+ public WorkflowClientOptions toWorkflowClientOptions() {
+ WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder();
+ if (this.namespace != null && !this.namespace.isEmpty()) {
+ builder.setNamespace(this.namespace);
+ }
+
+ return builder.build();
+ }
+
+ private void applyEnvOverrides(Map overrideEnvVars) {
+ Map env = System.getenv();
+ if (overrideEnvVars != null) {
+ env = overrideEnvVars;
+ }
+ if (env.containsKey("TEMPORAL_ADDRESS")) {
+ this.address = env.get("TEMPORAL_ADDRESS");
+ }
+ if (env.containsKey("TEMPORAL_NAMESPACE")) {
+ this.namespace = env.get("TEMPORAL_NAMESPACE");
+ }
+ if (env.containsKey("TEMPORAL_API_KEY")) {
+ this.apiKey = env.get("TEMPORAL_API_KEY");
+ }
+ // TLS settings
+ ClientConfigTLS.Builder tlsBuilder = this.tls != null ? this.tls.toBuilder() : null;
+ if (env.containsKey("TEMPORAL_TLS")) {
+ String s = env.get("TEMPORAL_TLS");
+ if (s != null && !s.isEmpty()) {
+ boolean v = Boolean.parseBoolean(s);
+ if (tlsBuilder == null) {
+ tlsBuilder = ClientConfigTLS.newBuilder();
+ }
+ tlsBuilder.setDisabled(!v);
+ }
+ }
+ if (env.containsKey("TEMPORAL_TLS_CLIENT_CERT_PATH")) {
+ String s = env.get("TEMPORAL_TLS_CLIENT_CERT_PATH");
+ if (s != null && !s.isEmpty()) {
+ if (tlsBuilder == null) {
+ tlsBuilder = ClientConfigTLS.newBuilder();
+ }
+ tlsBuilder.setClientCertPath(s);
+ }
+ }
+ if (env.containsKey("TEMPORAL_TLS_CLIENT_CERT_DATA")) {
+ String s = env.get("TEMPORAL_TLS_CLIENT_CERT_DATA");
+ if (s != null && !s.isEmpty()) {
+ if (tlsBuilder == null) {
+ tlsBuilder = ClientConfigTLS.newBuilder();
+ }
+ tlsBuilder.setClientCertData(s.getBytes());
+ }
+ }
+ if (env.containsKey("TEMPORAL_TLS_CLIENT_KEY_PATH")) {
+ String s = env.get("TEMPORAL_TLS_CLIENT_KEY_PATH");
+ if (s != null && !s.isEmpty()) {
+ if (tlsBuilder == null) {
+ tlsBuilder = ClientConfigTLS.newBuilder();
+ }
+ tlsBuilder.setClientKeyPath(s);
+ }
+ }
+ if (env.containsKey("TEMPORAL_TLS_CLIENT_KEY_DATA")) {
+ String s = env.get("TEMPORAL_TLS_CLIENT_KEY_DATA");
+ if (s != null && !s.isEmpty()) {
+ if (tlsBuilder == null) {
+ tlsBuilder = ClientConfigTLS.newBuilder();
+ }
+ tlsBuilder.setClientKeyData(s.getBytes());
+ }
+ }
+ if (env.containsKey("TEMPORAL_TLS_SERVER_CA_CERT_PATH")) {
+ String s = env.get("TEMPORAL_TLS_SERVER_CA_CERT_PATH");
+ if (s != null && !s.isEmpty()) {
+ if (tlsBuilder == null) {
+ tlsBuilder = ClientConfigTLS.newBuilder();
+ }
+ tlsBuilder.setServerCACertPath(s);
+ }
+ }
+ if (env.containsKey("TEMPORAL_TLS_SERVER_CA_CERT_DATA")) {
+ String s = env.get("TEMPORAL_TLS_SERVER_CA_CERT_DATA");
+ if (s != null && !s.isEmpty()) {
+ if (tlsBuilder == null) {
+ tlsBuilder = ClientConfigTLS.newBuilder();
+ }
+ tlsBuilder.setServerCACertData(s.getBytes());
+ }
+ }
+ if (env.containsKey("TEMPORAL_TLS_SERVER_NAME")) {
+ String s = env.get("TEMPORAL_TLS_SERVER_NAME");
+ if (tlsBuilder == null) {
+ tlsBuilder = ClientConfigTLS.newBuilder();
+ }
+ tlsBuilder.setServerName(s);
+ }
+ if (env.containsKey("TEMPORAL_TLS_DISABLE_HOST_VERIFICATION")) {
+ String s = env.get("TEMPORAL_TLS_DISABLE_HOST_VERIFICATION");
+ if (s != null && !s.isEmpty()) {
+ boolean v = Boolean.parseBoolean(s);
+ if (tlsBuilder == null) {
+ tlsBuilder = ClientConfigTLS.newBuilder();
+ }
+ tlsBuilder.setDisableHostVerification(v);
+ }
+ }
+ // Apply the TLS changes if any
+ if (tlsBuilder != null) {
+ this.tls = tlsBuilder.build();
+ }
+ // GRPC meta requires crawling the envs to find
+ for (Map.Entry entry : env.entrySet()) {
+ String v = entry.getValue();
+ if (entry.getKey().startsWith("TEMPORAL_GRPC_META_")) {
+ String key = entry.getKey().substring("TEMPORAL_GRPC_META_".length());
+ key = key.replace('_', '-').toLowerCase();
+ if (this.metadata == null) {
+ this.metadata = new Metadata();
+ }
+ Metadata.Key metaKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
+ // Empty env vars are not the same as unset. Unset will leave the
+ // meta key unchanged, but empty removes it.
+ if (v.isEmpty()) {
+ this.metadata.removeAll(metaKey);
+ } else {
+ this.metadata.put(metaKey, v);
+ }
+ }
+ }
+ }
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ public Metadata getMetadata() {
+ return metadata;
+ }
+
+ public ClientConfigTLS getTls() {
+ return tls;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ ClientConfigProfile profile = (ClientConfigProfile) o;
+ // Compare metadata properly
+ if (this.metadata == null) {
+ if (profile.metadata != null && profile.metadata.keys() != null) {
+ return false;
+ }
+ } else if (profile.metadata == null) {
+ if (this.metadata.keys() != null) {
+ return false;
+ }
+ } else {
+ // Both non-null, compare keys and values
+ if (!Objects.equals(this.metadata.keys(), profile.metadata.keys())) {
+ return false;
+ }
+ for (String key : this.metadata.keys()) {
+ if (!Objects.equals(
+ this.metadata.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)),
+ profile.metadata.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)))) {
+ return false;
+ }
+ }
+ }
+ return Objects.equals(namespace, profile.namespace)
+ && Objects.equals(address, profile.address)
+ && Objects.equals(apiKey, profile.apiKey)
+ && Objects.equals(tls, profile.tls);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(namespace, address, apiKey, metadata, tls);
+ }
+
+ @Override
+ public String toString() {
+ return "ClientConfigProfile{"
+ + "namespace='"
+ + namespace
+ + '\''
+ + ", address='"
+ + address
+ + '\''
+ + ", apiKey='"
+ + apiKey
+ + '\''
+ + ", metadata="
+ + metadata
+ + ", tls="
+ + tls
+ + '}';
+ }
+
+ public static final class Builder {
+ private String namespace;
+ private String address;
+ private String apiKey;
+ private Metadata metadata;
+ private ClientConfigTLS tls;
+
+ private Builder() {}
+
+ private Builder(ClientConfigProfile profile) {
+ this.namespace = profile.namespace;
+ this.address = profile.address;
+ this.apiKey = profile.apiKey;
+ this.metadata = profile.metadata;
+ this.tls = profile.tls;
+ }
+
+ /**
+ * Sets the namespace for the client. This is optional; if not set, the default namespace will
+ * be used.
+ */
+ public Builder setNamespace(String namespace) {
+ this.namespace = namespace;
+ return this;
+ }
+
+ /**
+ * Sets the address of the Temporal service endpoint. This is optional; if not set, the default
+ * address will be used.
+ */
+ public Builder setAddress(String address) {
+ this.address = address;
+ return this;
+ }
+
+ /** Sets the API key for the client. This is optional; if not set, no API key will be used. */
+ public Builder setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ return this;
+ }
+
+ /**
+ * Sets the gRPC metadata to be sent with each request. This is optional; if not set, no
+ * metadata will be sent.
+ */
+ public Builder setMetadata(Metadata metadata) {
+ this.metadata = metadata;
+ return this;
+ }
+
+ /**
+ * Sets the TLS configuration for the client. This is optional; if not set, no TLS will be used.
+ */
+ public Builder setTls(ClientConfigTLS tls) {
+ this.tls = tls;
+ return this;
+ }
+
+ public ClientConfigProfile build() {
+ return new ClientConfigProfile(namespace, address, apiKey, metadata, tls);
+ }
+ }
+}
diff --git a/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigTLS.java b/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigTLS.java
new file mode 100644
index 0000000000..5f7f1954f7
--- /dev/null
+++ b/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigTLS.java
@@ -0,0 +1,248 @@
+package io.temporal.envconfig;
+
+import io.temporal.common.Experimental;
+import java.util.Arrays;
+import java.util.Objects;
+
+/** TLS configuration for a client. */
+@Experimental
+public class ClientConfigTLS {
+ /** Create a builder for {@link ClientConfigTLS}. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /** Create a builder from an existing {@link ClientConfigTLS}. */
+ public static Builder newBuilder(ClientConfigTLS config) {
+ return new Builder(config);
+ }
+
+ /** Returns a default instance of {@link ClientConfigTLS} with all fields unset. */
+ public static ClientConfigTLS getDefaultInstance() {
+ return new Builder().build();
+ }
+
+ private final Boolean disabled;
+ private final String clientCertPath;
+ private final byte[] clientCertData;
+ private final String clientKeyPath;
+ private final byte[] clientKeyData;
+ private final String serverCACertPath;
+ private final byte[] serverCACertData;
+ private final String serverName;
+ private final Boolean disableHostVerification;
+
+ private ClientConfigTLS(
+ Boolean disabled,
+ String clientCertPath,
+ byte[] clientCertData,
+ String clientKeyPath,
+ byte[] clientKeyData,
+ String serverCACertPath,
+ byte[] serverCACertData,
+ String serverName,
+ Boolean disableHostVerification) {
+ this.disabled = disabled;
+ this.clientCertPath = clientCertPath;
+ this.clientCertData = clientCertData;
+ this.clientKeyPath = clientKeyPath;
+ this.clientKeyData = clientKeyData;
+ this.serverCACertPath = serverCACertPath;
+ this.serverCACertData = serverCACertData;
+ this.serverName = serverName;
+ this.disableHostVerification = disableHostVerification;
+ }
+
+ public Boolean isDisabled() {
+ return disabled;
+ }
+
+ public String getClientCertPath() {
+ return clientCertPath;
+ }
+
+ public byte[] getClientCertData() {
+ return clientCertData;
+ }
+
+ public String getClientKeyPath() {
+ return clientKeyPath;
+ }
+
+ public byte[] getClientKeyData() {
+ return clientKeyData;
+ }
+
+ public String getServerCACertPath() {
+ return serverCACertPath;
+ }
+
+ public byte[] getServerCACertData() {
+ return serverCACertData;
+ }
+
+ public String getServerName() {
+ return serverName;
+ }
+
+ public Boolean isDisableHostVerification() {
+ return disableHostVerification;
+ }
+
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ ClientConfigTLS that = (ClientConfigTLS) o;
+ return Objects.equals(disabled, that.disabled)
+ && Objects.equals(clientCertPath, that.clientCertPath)
+ && Objects.deepEquals(clientCertData, that.clientCertData)
+ && Objects.equals(clientKeyPath, that.clientKeyPath)
+ && Objects.deepEquals(clientKeyData, that.clientKeyData)
+ && Objects.equals(serverCACertPath, that.serverCACertPath)
+ && Objects.deepEquals(serverCACertData, that.serverCACertData)
+ && Objects.equals(serverName, that.serverName)
+ && Objects.equals(disableHostVerification, that.disableHostVerification);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ disabled,
+ clientCertPath,
+ Arrays.hashCode(clientCertData),
+ clientKeyPath,
+ Arrays.hashCode(clientKeyData),
+ serverCACertPath,
+ Arrays.hashCode(serverCACertData),
+ serverName,
+ disableHostVerification);
+ }
+
+ @Override
+ public String toString() {
+ return "ClientConfigTLS{"
+ + "disabled="
+ + disabled
+ + ", clientCertPath='"
+ + clientCertPath
+ + '\''
+ + ", clientCertData="
+ + Arrays.toString(clientCertData)
+ + ", clientKeyPath='"
+ + clientKeyPath
+ + '\''
+ + ", clientKeyData="
+ + Arrays.toString(clientKeyData)
+ + ", serverCACertPath='"
+ + serverCACertPath
+ + '\''
+ + ", serverCACertData="
+ + Arrays.toString(serverCACertData)
+ + ", serverName='"
+ + serverName
+ + '\''
+ + ", disableHostVerification="
+ + disableHostVerification
+ + '}';
+ }
+
+ public static class Builder {
+ private String clientCertPath;
+ private byte[] clientCertData;
+ private String clientKeyPath;
+ private byte[] clientKeyData;
+ private String serverCACertPath;
+ private byte[] serverCACertData;
+ private Boolean disabled;
+ private String serverName;
+ private Boolean disableHostVerification;
+
+ private Builder() {}
+
+ private Builder(ClientConfigTLS clientConfigTLS) {
+ this.disabled = clientConfigTLS.disabled;
+ this.serverName = clientConfigTLS.serverName;
+ this.clientCertPath = clientConfigTLS.clientCertPath;
+ this.clientCertData = clientConfigTLS.clientCertData;
+ this.clientKeyPath = clientConfigTLS.clientKeyPath;
+ this.clientKeyData = clientConfigTLS.clientKeyData;
+ this.serverCACertPath = clientConfigTLS.serverCACertPath;
+ this.serverCACertData = clientConfigTLS.serverCACertData;
+ this.disableHostVerification = clientConfigTLS.disableHostVerification;
+ }
+
+ /** Disable TLS. Default: false. */
+ public Builder setDisabled(Boolean disabled) {
+ this.disabled = disabled;
+ return this;
+ }
+
+ /**
+ * Server name for TLS verification. If not set, the hostname from the target endpoint will be
+ * used.
+ */
+ public Builder setServerName(String serverName) {
+ this.serverName = serverName;
+ return this;
+ }
+
+ /** Path to client mTLS certificate. Mutually exclusive with ClientCertData. */
+ public Builder setClientCertPath(String clientCertPath) {
+ this.clientCertPath = clientCertPath;
+ return this;
+ }
+
+ /** PEM bytes for client mTLS certificate. Mutually exclusive with ClientCertPath. */
+ public Builder setClientCertData(byte[] bytes) {
+ this.clientCertData = bytes;
+ return this;
+ }
+
+ /** Path to client mTLS key. Mutually exclusive with ClientKeyData. */
+ public Builder setClientKeyPath(String clientKeyPath) {
+ this.clientKeyPath = clientKeyPath;
+ return this;
+ }
+
+ /** PEM bytes for client mTLS key. Mutually exclusive with ClientKeyPath. */
+ public Builder setClientKeyData(byte[] clientKeyData) {
+ this.clientKeyData = clientKeyData;
+ return this;
+ }
+
+ /** Path to server CA cert override. Mutually exclusive with ServerCACertData. */
+ public Builder setServerCACertPath(String serverCACertPath) {
+ this.serverCACertPath = serverCACertPath;
+ return this;
+ }
+
+ /** PEM bytes for server CA cert override. Mutually exclusive with ServerCACertPath. */
+ public Builder setServerCACertData(byte[] serverCACertData) {
+ this.serverCACertData = serverCACertData;
+ return this;
+ }
+
+ /** Disable server host verification. Default: false */
+ public Builder setDisableHostVerification(Boolean disableHostVerification) {
+ this.disableHostVerification = disableHostVerification;
+ return this;
+ }
+
+ public ClientConfigTLS build() {
+ return new ClientConfigTLS(
+ disabled,
+ clientCertPath,
+ clientCertData,
+ clientKeyPath,
+ clientKeyData,
+ serverCACertPath,
+ serverCACertData,
+ serverName,
+ disableHostVerification);
+ }
+ }
+}
diff --git a/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigToml.java b/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigToml.java
new file mode 100644
index 0000000000..d4ead9baed
--- /dev/null
+++ b/temporal-envconfig/src/main/java/io/temporal/envconfig/ClientConfigToml.java
@@ -0,0 +1,230 @@
+package io.temporal.envconfig;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.grpc.Metadata;
+import io.temporal.common.Experimental;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Classes for parsing the client config toml file.
+ *
+ * These are package private, use {@link ClientConfig} and {@link ClientConfigProfile} to load
+ * and work with client configs.
+ */
+@Experimental
+class ClientConfigToml {
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ static class TomlClientConfig {
+ @JsonProperty("profile")
+ public Map profiles;
+
+ protected TomlClientConfig() {}
+
+ TomlClientConfig(Map profiles) {
+ this.profiles = profiles;
+ }
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ static class TomlClientConfigProfile {
+ @JsonProperty("address")
+ public String address;
+
+ @JsonProperty("namespace")
+ public String namespace;
+
+ @JsonProperty("api_key")
+ public String apiKey;
+
+ @JsonProperty("tls")
+ public TomlClientConfigTLS tls;
+
+ @JsonProperty("grpc_meta")
+ public Map grpcMeta;
+
+ protected TomlClientConfigProfile() {}
+
+ TomlClientConfigProfile(
+ String address,
+ String namespace,
+ String apiKey,
+ TomlClientConfigTLS tls,
+ Map grpcMeta) {
+ this.address = address;
+ this.namespace = namespace;
+ this.apiKey = apiKey;
+ this.tls = tls;
+ this.grpcMeta = grpcMeta;
+ }
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ static class TomlClientConfigTLS {
+ @JsonProperty("disabled")
+ public Boolean disabled;
+
+ @JsonProperty("client_cert_path")
+ public String clientCertPath;
+
+ @JsonProperty("client_cert_data")
+ public String clientCertData;
+
+ @JsonProperty("client_key_path")
+ public String clientKeyPath;
+
+ @JsonProperty("client_key_data")
+ public String clientKeyData;
+
+ @JsonProperty("server_ca_cert_path")
+ public String serverCACertPath;
+
+ @JsonProperty("server_ca_cert_data")
+ public String serverCACertData;
+
+ @JsonProperty("server_name")
+ public String serverName;
+
+ @JsonProperty("disable_host_verification")
+ public Boolean disableHostVerification;
+
+ protected TomlClientConfigTLS() {}
+
+ TomlClientConfigTLS(
+ Boolean disabled,
+ String clientCertPath,
+ String clientCertData,
+ String clientKeyPath,
+ String clientKeyData,
+ String serverCACertPath,
+ String serverCACertData,
+ String serverName,
+ Boolean disableHostVerification) {
+ this.disabled = disabled;
+ this.clientCertPath = clientCertPath;
+ this.clientCertData = clientCertData;
+ this.clientKeyPath = clientKeyPath;
+ this.clientKeyData = clientKeyData;
+ this.serverCACertPath = serverCACertPath;
+ this.serverCACertData = serverCACertData;
+ this.serverName = serverName;
+ this.disableHostVerification = disableHostVerification;
+ }
+ }
+
+ @Nullable
+ static ClientConfigTLS getClientConfigTLS(ClientConfigToml.TomlClientConfigProfile tomlProfile) {
+ ClientConfigTLS tls = null;
+ if (tomlProfile.tls != null) {
+ tls =
+ ClientConfigTLS.newBuilder()
+ .setClientCertData(
+ tomlProfile.tls.clientCertData != null
+ ? tomlProfile.tls.clientCertData.getBytes(StandardCharsets.UTF_8)
+ : null)
+ .setClientCertPath(tomlProfile.tls.clientCertPath)
+ .setClientKeyData(
+ tomlProfile.tls.clientKeyData != null
+ ? tomlProfile.tls.clientKeyData.getBytes(StandardCharsets.UTF_8)
+ : null)
+ .setClientKeyPath(tomlProfile.tls.clientKeyPath)
+ .setServerCACertData(
+ tomlProfile.tls.serverCACertData != null
+ ? tomlProfile.tls.serverCACertData.getBytes(StandardCharsets.UTF_8)
+ : null)
+ .setServerCACertPath(tomlProfile.tls.serverCACertPath)
+ .setDisabled(tomlProfile.tls.disabled)
+ .setServerName(tomlProfile.tls.serverName)
+ .setDisableHostVerification(tomlProfile.tls.disableHostVerification)
+ .build();
+ }
+ return tls;
+ }
+
+ private static String normalizeGrpcMetaKey(String key) {
+ return key.toLowerCase().replace('_', '-');
+ }
+
+ static Map getClientProfiles(
+ ClientConfigToml.TomlClientConfig clientConfig) {
+ Map profiles = new HashMap<>(clientConfig.profiles.size());
+ for (Map.Entry entry :
+ clientConfig.profiles.entrySet()) {
+ String profileName = entry.getKey();
+ ClientConfigToml.TomlClientConfigProfile tomlProfile = entry.getValue();
+ ClientConfigTLS tls = getClientConfigTLS(tomlProfile);
+ Metadata metadata = null;
+ if (tomlProfile.grpcMeta != null) {
+ for (Map.Entry metaEntry : tomlProfile.grpcMeta.entrySet()) {
+ if (metadata == null) {
+ metadata = new Metadata();
+ }
+ Metadata.Key key =
+ Metadata.Key.of(
+ normalizeGrpcMetaKey(metaEntry.getKey()), Metadata.ASCII_STRING_MARSHALLER);
+ metadata.put(key, metaEntry.getValue());
+ }
+ }
+ ClientConfigProfile profile =
+ ClientConfigProfile.newBuilder()
+ .setAddress(tomlProfile.address)
+ .setNamespace(tomlProfile.namespace)
+ .setApiKey(tomlProfile.apiKey)
+ .setMetadata(metadata)
+ .setTls(tls)
+ .build();
+ profiles.put(profileName, profile);
+ }
+ return profiles;
+ }
+
+ public static Map fromClientProfiles(
+ Map profiles) {
+ Map tomlProfiles = new HashMap<>(profiles.size());
+ for (Map.Entry entry : profiles.entrySet()) {
+ String profileName = entry.getKey();
+ ClientConfigProfile profile = entry.getValue();
+ TomlClientConfigTLS tls = null;
+ if (profile.getTls() != null) {
+ tls =
+ new TomlClientConfigTLS(
+ profile.getTls().isDisabled(),
+ profile.getTls().getClientCertPath(),
+ profile.getTls().getClientCertData() != null
+ ? new String(profile.getTls().getClientCertData(), StandardCharsets.UTF_8)
+ : null,
+ profile.getTls().getClientKeyPath(),
+ profile.getTls().getClientKeyData() != null
+ ? new String(profile.getTls().getClientKeyData(), StandardCharsets.UTF_8)
+ : null,
+ profile.getTls().getServerCACertPath(),
+ profile.getTls().getServerCACertData() != null
+ ? new String(profile.getTls().getServerCACertData(), StandardCharsets.UTF_8)
+ : null,
+ profile.getTls().getServerName(),
+ profile.getTls().isDisableHostVerification());
+ }
+ Map grpcMeta = null;
+ if (profile.getMetadata() != null) {
+ grpcMeta = new HashMap<>();
+ for (String key : profile.getMetadata().keys()) {
+ Metadata.Key metaKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
+ Iterable values = profile.getMetadata().getAll(metaKey);
+ if (values != null) {
+ // Join multiple values with comma
+ String joinedValues = String.join(",", values);
+ grpcMeta.put(key, joinedValues);
+ }
+ }
+ }
+ TomlClientConfigProfile tomlProfile =
+ new TomlClientConfigProfile(
+ profile.getAddress(), profile.getNamespace(), profile.getApiKey(), tls, grpcMeta);
+ tomlProfiles.put(profileName, tomlProfile);
+ }
+ return tomlProfiles;
+ }
+}
diff --git a/temporal-envconfig/src/main/java/io/temporal/envconfig/LoadClientConfigOptions.java b/temporal-envconfig/src/main/java/io/temporal/envconfig/LoadClientConfigOptions.java
new file mode 100644
index 0000000000..c7271ef864
--- /dev/null
+++ b/temporal-envconfig/src/main/java/io/temporal/envconfig/LoadClientConfigOptions.java
@@ -0,0 +1,111 @@
+package io.temporal.envconfig;
+
+import io.temporal.common.Experimental;
+import java.util.Map;
+
+/** Options for loading a client config via {@link ClientConfig#load(LoadClientConfigOptions)} */
+@Experimental
+public class LoadClientConfigOptions {
+ /** Create a builder for {@link LoadClientConfigOptions}. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /** Create a builder from an existing {@link LoadClientConfigOptions}. */
+ public static Builder newBuilder(LoadClientConfigOptions options) {
+ return new Builder(options);
+ }
+
+ /** Returns a default instance of {@link LoadClientConfigOptions} with all fields unset. */
+ public static LoadClientConfigOptions getDefaultInstance() {
+ return new Builder().build();
+ }
+
+ private final String configFilePath;
+ private final byte[] configFileData;
+ private final boolean strictConfigFile;
+ private final Map envOverrides;
+
+ private LoadClientConfigOptions(
+ String configFilePath,
+ byte[] configFileData,
+ boolean strictConfigFile,
+ Map envOverrides) {
+ this.configFilePath = configFilePath;
+ this.configFileData = configFileData;
+ this.strictConfigFile = strictConfigFile;
+ this.envOverrides = envOverrides;
+ }
+
+ public String getConfigFilePath() {
+ return configFilePath;
+ }
+
+ public byte[] getConfigFileData() {
+ return configFileData;
+ }
+
+ public boolean isStrictConfigFile() {
+ return strictConfigFile;
+ }
+
+ public Map getEnvOverrides() {
+ return envOverrides;
+ }
+
+ public static class Builder {
+ private String configFilePath;
+ private byte[] configFileData;
+ private boolean strictConfigFile;
+ private Map envOverrides;
+
+ private Builder() {}
+
+ private Builder(LoadClientConfigOptions options) {
+ this.configFilePath = options.configFilePath;
+ this.configFileData = options.configFileData;
+ this.strictConfigFile = options.strictConfigFile;
+ this.envOverrides = options.envOverrides;
+ }
+
+ /** If true, will error if there are unrecognized keys. Defaults to false. */
+ public Builder setStrictConfigFile(boolean configFileStrict) {
+ this.strictConfigFile = configFileStrict;
+ return this;
+ }
+
+ /**
+ * Set environment variable overrides. If set, these will be used instead of the actual
+ * environment variables. If not set, the actual environment variables will be used.
+ */
+ public Builder setEnvOverrides(Map envOverrides) {
+ this.envOverrides = envOverrides;
+ return this;
+ }
+
+ /**
+ * TOML data to load for config. If set, this overrides any file loading. Cannot be set if
+ * ConfigFilePath is set. Ignored if DisableFile is true.
+ */
+ public Builder setConfigFileData(byte[] bytes) {
+ this.configFileData = bytes;
+ return this;
+ }
+
+ /**
+ * Override the file path to use to load the TOML file for config. Defaults to
+ * TEMPORAL_CONFIG_FILE environment variable or if that is unset/empty, defaults to
+ * [os.UserConfigDir]/temporal/temporal.toml. If ConfigFileData is set, this cannot be set and
+ * no file loading from disk occurs. Ignored if DisableFile is true.
+ */
+ public Builder setConfigFilePath(String configFilePath) {
+ this.configFilePath = configFilePath;
+ return this;
+ }
+
+ public LoadClientConfigOptions build() {
+ return new LoadClientConfigOptions(
+ configFilePath, configFileData, strictConfigFile, envOverrides);
+ }
+ }
+}
diff --git a/temporal-envconfig/src/main/java/io/temporal/envconfig/LoadClientConfigProfileOptions.java b/temporal-envconfig/src/main/java/io/temporal/envconfig/LoadClientConfigProfileOptions.java
new file mode 100644
index 0000000000..4b0d345004
--- /dev/null
+++ b/temporal-envconfig/src/main/java/io/temporal/envconfig/LoadClientConfigProfileOptions.java
@@ -0,0 +1,177 @@
+package io.temporal.envconfig;
+
+import io.temporal.common.Experimental;
+import java.util.Map;
+
+/**
+ * Options for loading a client config profile via {@link
+ * ClientConfigProfile#load(LoadClientConfigProfileOptions)}
+ */
+@Experimental
+public class LoadClientConfigProfileOptions {
+ /** Create a builder for {@link LoadClientConfigProfileOptions}. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /** Create a builder from an existing {@link LoadClientConfigProfileOptions}. */
+ public static Builder newBuilder(LoadClientConfigProfileOptions options) {
+ return new Builder(options);
+ }
+
+ /** Returns a default instance of {@link LoadClientConfigProfileOptions} with all fields unset. */
+ public static LoadClientConfigProfileOptions getDefaultInstance() {
+ return new Builder().build();
+ }
+
+ private final String configFileProfile;
+ private final String configFilePath;
+ private final byte[] configFileData;
+ private final boolean configFileStrict;
+ private final boolean disableFile;
+ private final boolean disableEnv;
+ private final Map envOverrides;
+
+ private LoadClientConfigProfileOptions(
+ String configFileProfile,
+ String configFilePath,
+ byte[] configFileData,
+ boolean configFileStrict,
+ boolean disableFile,
+ boolean disableEnv,
+ Map envOverrides) {
+ this.configFileProfile = configFileProfile;
+ this.configFilePath = configFilePath;
+ this.configFileData = configFileData;
+ this.configFileStrict = configFileStrict;
+ this.disableFile = disableFile;
+ this.disableEnv = disableEnv;
+ this.envOverrides = envOverrides;
+ }
+
+ public String getConfigFileProfile() {
+ return configFileProfile;
+ }
+
+ public byte[] getConfigFileData() {
+ return configFileData;
+ }
+
+ public boolean isConfigFileStrict() {
+ return configFileStrict;
+ }
+
+ public boolean isDisableFile() {
+ return disableFile;
+ }
+
+ public boolean isDisableEnv() {
+ return disableEnv;
+ }
+
+ public Map getEnvOverrides() {
+ return envOverrides;
+ }
+
+ public String getConfigFilePath() {
+ return configFilePath;
+ }
+
+ public static class Builder {
+ private String configFileProfile;
+ private String configFilePath;
+ private byte[] configFileData;
+ private boolean configFileStrict;
+ private boolean disableFile;
+ private boolean disableEnv;
+ private Map envOverrides;
+
+ private Builder() {}
+
+ private Builder(LoadClientConfigProfileOptions options) {
+ this.configFileProfile = options.configFileProfile;
+ this.configFilePath = options.configFilePath;
+ this.configFileData = options.configFileData;
+ this.configFileStrict = options.configFileStrict;
+ this.disableFile = options.disableFile;
+ this.disableEnv = options.disableEnv;
+ this.envOverrides = options.envOverrides;
+ }
+
+ /** If true, will error if there are unrecognized keys. Defaults to false. */
+ public Builder setConfigFileStrict(boolean configFileStrict) {
+ this.configFileStrict = configFileStrict;
+ return this;
+ }
+
+ /**
+ * If true, will not do any TOML loading from file or data. This and DisableEnv cannot both be
+ * true. Defaults to false.
+ */
+ public Builder setDisableFile(boolean disableFile) {
+ this.disableFile = disableFile;
+ return this;
+ }
+
+ /**
+ * If true, will not apply environment variables on top of file config for the client options,
+ * but TEMPORAL_CONFIG_FILE and TEMPORAL_PROFILE environment variables may still by used to
+ * populate defaults in this options structure. Defaults to false.
+ */
+ public Builder setDisableEnv(boolean disableEnv) {
+ this.disableEnv = disableEnv;
+ return this;
+ }
+
+ /**
+ * Override the file path to use to load the TOML file for config. Defaults to
+ * TEMPORAL_CONFIG_FILE environment variable or if that is unset/empty, defaults to
+ * [os.UserConfigDir]/temporal/temporal.toml. If ConfigFileData is set, this cannot be set and
+ * no file loading from disk occurs. Ignored if DisableFile is true.
+ */
+ public Builder setConfigFilePath(String configFilePath) {
+ this.configFilePath = configFilePath;
+ return this;
+ }
+
+ /**
+ * TOML data to load for config. If set, this overrides any file loading. Cannot be set if
+ * ConfigFilePath is set. Ignored if DisableFile is true.
+ */
+ public Builder setConfigFileData(byte[] bytes) {
+ this.configFileData = bytes;
+ return this;
+ }
+
+ /**
+ * Set specific profile to use after file is loaded. Defaults to TEMPORAL_PROFILE environment
+ * variable or if that is unset/empty, defaults to "default". If either this or the environment
+ * variable are set, load will fail if the profile isn't present in the config. Ignored if
+ * DisableFile is true.
+ */
+ public Builder setConfigFileProfile(String foo) {
+ this.configFileProfile = foo;
+ return this;
+ }
+
+ /**
+ * Set environment variable overrides. If set, these will be used instead of the actual
+ * environment variables. If not set, the actual environment variables will be used.
+ */
+ public Builder setEnvOverrides(Map envOverrides) {
+ this.envOverrides = envOverrides;
+ return this;
+ }
+
+ public LoadClientConfigProfileOptions build() {
+ return new LoadClientConfigProfileOptions(
+ configFileProfile,
+ configFilePath,
+ configFileData,
+ configFileStrict,
+ disableFile,
+ disableEnv,
+ envOverrides);
+ }
+ }
+}
diff --git a/temporal-envconfig/src/test/java/io/temporal/envconfig/ClientConfigProfileTest.java b/temporal-envconfig/src/test/java/io/temporal/envconfig/ClientConfigProfileTest.java
new file mode 100644
index 0000000000..fcb1ced662
--- /dev/null
+++ b/temporal-envconfig/src/test/java/io/temporal/envconfig/ClientConfigProfileTest.java
@@ -0,0 +1,325 @@
+package io.temporal.envconfig;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+import io.grpc.Metadata;
+import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import java.io.*;
+import java.util.Collections;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ClientConfigProfileTest {
+ @Test
+ public void loadProfileApplyEnv() throws IOException {
+ String toml =
+ "[profile.foo]\n"
+ + "address = \"my-address\"\n"
+ + "namespace = \"my-namespace\"\n"
+ + "api_key = \"my-api-key\"\n"
+ + "grpc_meta = { some-heAder1 = \"some-value1\", some-header2 = \"some-value2\", some_heaDer3 = \"some-value3\" }\n"
+ + "some_future_key = \"some future value not handled\"\n"
+ + "\n"
+ + "[profile.foo.tls]\n"
+ + "disabled = true\n"
+ + "client_cert_path = \"my-client-cert-path\"\n"
+ + "client_cert_data = \"my-client-cert-data\"\n"
+ + "client_key_path = \"my-client-key-path\"\n"
+ + "client_key_data = \"my-client-key-data\"\n"
+ + "server_ca_cert_path = \"my-server-ca-cert-path\"\n"
+ + "server_ca_cert_data = \"my-server-ca-cert-data\"\n"
+ + "server_name = \"my-server-name\"\n"
+ + "disable_host_verification = true";
+ ClientConfigProfile profile =
+ ClientConfigProfile.load(
+ LoadClientConfigProfileOptions.newBuilder()
+ .setConfigFileData(toml.getBytes())
+ .setConfigFileProfile("foo")
+ .setEnvOverrides(Collections.emptyMap())
+ .build());
+ Assert.assertEquals("my-address", profile.getAddress());
+ Assert.assertEquals("my-namespace", profile.getNamespace());
+ Assert.assertEquals("my-api-key", profile.getApiKey());
+ Assert.assertEquals(3, profile.getMetadata().keys().size());
+ Assert.assertNotNull(profile.getTls());
+ Assert.assertEquals(
+ "some-value1",
+ profile
+ .getMetadata()
+ .get(Metadata.Key.of("some-header1", Metadata.ASCII_STRING_MARSHALLER)));
+ Assert.assertEquals(
+ "some-value2",
+ profile
+ .getMetadata()
+ .get(Metadata.Key.of("some-header2", Metadata.ASCII_STRING_MARSHALLER)));
+ Assert.assertEquals(
+ "some-value3",
+ profile
+ .getMetadata()
+ .get(Metadata.Key.of("some-header3", Metadata.ASCII_STRING_MARSHALLER)));
+ // With env
+ profile =
+ ClientConfigProfile.load(
+ LoadClientConfigProfileOptions.newBuilder()
+ .setConfigFileData(toml.getBytes())
+ .setEnvOverrides(
+ new ImmutableMap.Builder()
+ .put("TEMPORAL_PROFILE", "foo")
+ .put("TEMPORAL_ADDRESS", "my-address-new")
+ .put("TEMPORAL_NAMESPACE", "my-namespace-new")
+ .put("TEMPORAL_API_KEY", "my-api-key-new")
+ .put("TEMPORAL_TLS", "false")
+ .put("TEMPORAL_TLS_CLIENT_CERT_PATH", "my-client-cert-path-new")
+ .put("TEMPORAL_TLS_CLIENT_CERT_DATA", "my-client-cert-data-new")
+ .put("TEMPORAL_TLS_CLIENT_KEY_PATH", "my-client-key-path-new")
+ .put("TEMPORAL_TLS_CLIENT_KEY_DATA", "my-client-key-data-new")
+ .put("TEMPORAL_TLS_SERVER_CA_CERT_PATH", "my-server-ca-cert-path-new")
+ .put("TEMPORAL_TLS_SERVER_CA_CERT_DATA", "my-server-ca-cert-data-new")
+ .put("TEMPORAL_TLS_SERVER_NAME", "my-server-name-new")
+ .put("TEMPORAL_TLS_DISABLE_HOST_VERIFICATION", "false")
+ .put("TEMPORAL_GRPC_META_SOME_HEADER2", "some-value2-new")
+ .put("TEMPORAL_GRPC_META_SOME_HEADER3", "")
+ .put("TEMPORAL_GRPC_META_SOME_HEADER4", "some-value4-new")
+ .build())
+ .build());
+ Assert.assertEquals("my-address-new", profile.getAddress());
+ Assert.assertEquals("my-namespace-new", profile.getNamespace());
+ Assert.assertEquals("my-api-key-new", profile.getApiKey());
+ Assert.assertEquals(3, profile.getMetadata().keys().size());
+ Assert.assertNotNull(profile.getTls());
+ Assert.assertEquals(
+ "some-value1",
+ profile
+ .getMetadata()
+ .get(Metadata.Key.of("some-header1", Metadata.ASCII_STRING_MARSHALLER)));
+ Assert.assertEquals(
+ "some-value2-new",
+ profile
+ .getMetadata()
+ .get(Metadata.Key.of("some-header2", Metadata.ASCII_STRING_MARSHALLER)));
+ Assert.assertNull(
+ profile
+ .getMetadata()
+ .get(Metadata.Key.of("some-header3", Metadata.ASCII_STRING_MARSHALLER)));
+ Assert.assertEquals(
+ "some-value4-new",
+ profile
+ .getMetadata()
+ .get(Metadata.Key.of("some-header4", Metadata.ASCII_STRING_MARSHALLER)));
+ }
+
+ @Test
+ public void loadClientConfigProfileFile() throws IOException {
+ // Put some data in temp file and set the env var to use that file
+ File temp = File.createTempFile("envConfigTest", "");
+ temp.deleteOnExit();
+ Files.asCharSink(temp, Charsets.UTF_8)
+ .write(
+ "[profile.default]\n" + "address = \"my-address\"\n" + "namespace = \"my-namespace\"");
+ // Explicitly set
+ ClientConfigProfile profile =
+ ClientConfigProfile.load(
+ LoadClientConfigProfileOptions.newBuilder()
+ .setConfigFilePath(temp.getAbsolutePath())
+ .setEnvOverrides(Collections.emptyMap())
+ .build());
+ Assert.assertEquals("my-address", profile.getAddress());
+ Assert.assertEquals("my-namespace", profile.getNamespace());
+ // From env
+ profile =
+ ClientConfigProfile.load(
+ LoadClientConfigProfileOptions.newBuilder()
+ .setEnvOverrides(
+ Collections.singletonMap("TEMPORAL_CONFIG_FILE", temp.getAbsolutePath()))
+ .build());
+ Assert.assertEquals("my-address", profile.getAddress());
+ Assert.assertEquals("my-namespace", profile.getNamespace());
+ }
+
+ @Test
+ public void loadClientOptionsAPIKeyTLS() throws IOException {
+ // Since API key is present, TLS defaults to present
+ ClientConfigProfile profile =
+ ClientConfigProfile.load(
+ LoadClientConfigProfileOptions.newBuilder()
+ .setConfigFileData(("[profile.default]\n" + "api_key = \"my-api-key\"").getBytes())
+ .setEnvOverrides(Collections.emptyMap())
+ .build());
+ WorkflowServiceStubsOptions stubsOptions = profile.toWorkflowServiceStubsOptions();
+ Assert.assertEquals(1, stubsOptions.getGrpcMetadataProviders().size());
+ Assert.assertTrue(stubsOptions.getEnableHttps());
+
+ // But when API key is not present, neither should TLS be
+ profile =
+ ClientConfigProfile.load(
+ LoadClientConfigProfileOptions.newBuilder()
+ .setConfigFileData(("[profile.default]\n" + "address = \"whatever\"").getBytes())
+ .setEnvOverrides(Collections.emptyMap())
+ .build());
+ stubsOptions = profile.toWorkflowServiceStubsOptions();
+ Assert.assertNull(stubsOptions.getGrpcMetadataProviders());
+ Assert.assertFalse(stubsOptions.getEnableHttps());
+ }
+
+ @Test
+ public void loadClientOptionsTLS() throws IOException {
+ String clientCertData =
+ "-----BEGIN CERTIFICATE-----\n"
+ + "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\n"
+ + "-----END CERTIFICATE-----";
+
+ String clientKeyData =
+ "-----BEGIN PRIVATE KEY-----\n"
+ + "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\n"
+ + "YymQE1T0jkQc7f+tsDiNuDn91NTlCeQ=\n"
+ + "-----END PRIVATE KEY-----\n";
+
+ // Since API key is present, TLS defaults to present
+ ClientConfigProfile profile =
+ ClientConfigProfile.load(
+ LoadClientConfigProfileOptions.newBuilder()
+ .setConfigFileData(
+ ("[profile.default]\n"
+ + "[profile.default.tls]\n"
+ + "client_cert_data = \"\"\""
+ + clientCertData
+ + "\"\"\"\n"
+ + "client_key_data = \"\"\""
+ + clientKeyData
+ + "\"\"\"")
+ .getBytes())
+ .setEnvOverrides(Collections.emptyMap())
+ .build());
+ WorkflowServiceStubsOptions stubsOptions = profile.toWorkflowServiceStubsOptions();
+ Assert.assertNotNull(stubsOptions.getSslContext());
+ SslContext context = stubsOptions.getSslContext();
+ Assert.assertTrue(context.isClient());
+ }
+
+ @Test
+ public void parseToml() throws IOException {
+ String toml =
+ "[profile.foo]\n"
+ + "address = \"my-address\"\n"
+ + "namespace = \"my-namespace\"\n"
+ + "api_key = \"my-api-key\"\n"
+ + "grpc_meta = { some-heAder1 = \"some-value1\", some-header2 = \"some-value2\", some_heaDer3 = \"some-value3\" }\n"
+ + "some_future_key = \"some future value not handled\"\n"
+ + "\n"
+ + "[profile.foo.tls]\n"
+ + "disabled = true\n"
+ + "client_cert_path = \"my-client-cert-path\"\n"
+ + "client_cert_data = \"my-client-cert-data\"\n"
+ + "client_key_path = \"my-client-key-path\"\n"
+ + "client_key_data = \"my-client-key-data\"\n"
+ + "server_ca_cert_path = \"my-server-ca-cert-path\"\n"
+ + "server_ca_cert_data = \"my-server-ca-cert-data\"\n"
+ + "server_name = \"my-server-name\"\n"
+ + "disable_host_verification = true";
+ ClientConfig clientConfig = ClientConfig.fromToml(toml.getBytes());
+ Assert.assertEquals(1, clientConfig.getProfiles().size());
+ ClientConfigProfile profile = clientConfig.getProfiles().get("foo");
+ Assert.assertNotNull(profile);
+ Assert.assertEquals("my-address", profile.getAddress());
+ Assert.assertEquals("my-namespace", profile.getNamespace());
+ Assert.assertEquals("my-api-key", profile.getApiKey());
+ Assert.assertEquals(3, profile.getMetadata().keys().size());
+ Assert.assertNotNull(profile.getTls());
+ Assert.assertEquals(
+ "some-value1",
+ profile
+ .getMetadata()
+ .get(Metadata.Key.of("some-header1", Metadata.ASCII_STRING_MARSHALLER)));
+ Assert.assertEquals(
+ "some-value2",
+ profile
+ .getMetadata()
+ .get(Metadata.Key.of("some-header2", Metadata.ASCII_STRING_MARSHALLER)));
+ Assert.assertEquals(
+ "some-value3",
+ profile
+ .getMetadata()
+ .get(Metadata.Key.of("some-header3", Metadata.ASCII_STRING_MARSHALLER)));
+ String generatedTomlString = new String(ClientConfig.toTomlAsBytes(clientConfig));
+ ClientConfig clientConfig2 = ClientConfig.fromToml(generatedTomlString.getBytes());
+ Assert.assertEquals(clientConfig, clientConfig2);
+ }
+}