use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, path::Path};
use thiserror::Error;
pub const DEFAULT_PROFILE: &str = "default";
pub const DEFAULT_CONFIG_FILE: &str = "temporal.toml";
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Profile '{0}' not found")]
ProfileNotFound(String),
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
#[error("Configuration loading error: {0}")]
LoadError(Box<dyn std::error::Error>),
}
impl From<std::env::VarError> for ConfigError {
fn from(e: std::env::VarError) -> Self {
Self::LoadError(e.into())
}
}
impl From<std::str::Utf8Error> for ConfigError {
fn from(e: std::str::Utf8Error) -> Self {
Self::LoadError(e.into())
}
}
impl From<toml::de::Error> for ConfigError {
fn from(e: toml::de::Error) -> Self {
Self::LoadError(e.into())
}
}
impl From<toml::ser::Error> for ConfigError {
fn from(e: toml::ser::Error) -> Self {
Self::LoadError(e.into())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DataSource {
Path(String),
Data(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ClientConfig {
pub profiles: HashMap<String, ClientConfigProfile>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ClientConfigProfile {
pub address: Option<String>,
pub namespace: Option<String>,
pub api_key: Option<String>,
pub tls: Option<ClientConfigTLS>,
pub codec: Option<ClientConfigCodec>,
pub grpc_meta: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ClientConfigTLS {
pub disabled: Option<bool>,
pub client_cert: Option<DataSource>,
pub client_key: Option<DataSource>,
pub server_ca_cert: Option<DataSource>,
pub server_name: Option<String>,
pub disable_host_verification: bool,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ClientConfigCodec {
pub endpoint: Option<String>,
pub auth: Option<String>,
}
#[derive(Debug, Default)]
pub struct LoadClientConfigOptions {
pub config_source: Option<DataSource>,
pub config_file_strict: bool,
}
#[derive(Debug, Default)]
pub struct LoadClientConfigProfileOptions {
pub config_source: Option<DataSource>,
pub config_file_profile: Option<String>,
pub config_file_strict: bool,
pub disable_file: bool,
pub disable_env: bool,
}
#[derive(Debug, Default)]
pub struct ClientConfigFromTOMLOptions {
pub strict: bool,
}
enum EnvProvider<'a> {
Map(&'a HashMap<String, String>),
System,
}
impl<'a> EnvProvider<'a> {
fn get(&self, key: &str) -> Result<Option<String>, ConfigError> {
match self {
EnvProvider::Map(map) => Ok(map.get(key).cloned()),
EnvProvider::System => match std::env::var(key) {
Ok(v) => Ok(Some(v)),
Err(std::env::VarError::NotPresent) => Ok(None),
Err(e) => Err(e.into()),
},
}
}
fn contains_key(&self, key: &str) -> Result<bool, ConfigError> {
match self {
EnvProvider::Map(map) => Ok(map.contains_key(key)),
EnvProvider::System => Ok(std::env::var(key).is_ok()),
}
}
}
fn read_path_bytes(path: &str) -> Result<Option<Vec<u8>>, ConfigError> {
if !Path::new(path).exists() {
return Ok(None);
}
match fs::read(path) {
Ok(data) => Ok(Some(data)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(ConfigError::LoadError(e.into())),
}
}
pub fn load_client_config(
options: LoadClientConfigOptions,
env_vars: Option<&HashMap<String, String>>,
) -> Result<ClientConfig, ConfigError> {
load_client_config_inner(options, env_vars, get_default_config_file_path())
}
fn load_client_config_inner(
options: LoadClientConfigOptions,
env_vars: Option<&HashMap<String, String>>,
default_config_file_path: Option<String>,
) -> Result<ClientConfig, ConfigError> {
let env_provider = match env_vars {
Some(map) => EnvProvider::Map(map),
None => EnvProvider::System,
};
let toml_data = match options.config_source {
Some(DataSource::Data(d)) => Some(d),
Some(DataSource::Path(p)) => read_path_bytes(&p)?,
None => {
let file_path = if let Some(path) = env_provider
.get("TEMPORAL_CONFIG_FILE")?
.filter(|p| !p.is_empty())
{
Some(path)
} else {
default_config_file_path
};
match file_path {
Some(file_path) => read_path_bytes(&file_path)?,
None => None,
}
}
};
if let Some(data) = toml_data {
ClientConfig::from_toml(
&data,
ClientConfigFromTOMLOptions {
strict: options.config_file_strict,
},
)
} else {
Ok(ClientConfig::default())
}
}
pub fn load_client_config_profile(
options: LoadClientConfigProfileOptions,
env_vars: Option<&HashMap<String, String>>,
) -> Result<ClientConfigProfile, ConfigError> {
if options.disable_file && options.disable_env {
return Err(ConfigError::InvalidConfig(
"Cannot disable both file and environment loading".to_string(),
));
}
let env_provider = if options.disable_env {
None
} else {
Some(match env_vars {
Some(map) => EnvProvider::Map(map),
None => EnvProvider::System,
})
};
let mut profile = if options.disable_file {
ClientConfigProfile::default()
} else {
let config = load_client_config(
LoadClientConfigOptions {
config_source: options.config_source,
config_file_strict: options.config_file_strict,
},
env_vars,
)?;
let (profile_name, profile_unset) = if let Some(p) = options.config_file_profile.as_deref()
{
(p.to_string(), false)
} else {
match env_provider.as_ref() {
Some(provider) => match provider.get("TEMPORAL_PROFILE")? {
Some(p) if !p.is_empty() => (p, false),
_ => (DEFAULT_PROFILE.to_string(), true),
},
None => (DEFAULT_PROFILE.to_string(), true),
}
};
if let Some(prof) = config.profiles.get(&profile_name) {
Ok(prof.clone())
} else if !profile_unset {
Err(ConfigError::ProfileNotFound(profile_name))
} else {
Ok(ClientConfigProfile::default())
}?
};
if !options.disable_env {
profile.load_from_env(env_vars)?;
}
Ok(profile)
}
impl ClientConfig {
pub fn from_toml(
toml_bytes: &[u8],
options: ClientConfigFromTOMLOptions,
) -> Result<Self, ConfigError> {
use strict::StrictTomlClientConfig;
let toml_str = std::str::from_utf8(toml_bytes)?;
let mut conf = ClientConfig::default();
if toml_str.trim().is_empty() {
return Ok(conf);
}
if options.strict {
let toml_conf: StrictTomlClientConfig = toml::from_str(toml_str)?;
toml_conf.apply_to_client_config(&mut conf)?;
} else {
let toml_conf: TomlClientConfig = toml::from_str(toml_str)?;
toml_conf.apply_to_client_config(&mut conf)?;
}
Ok(conf)
}
pub fn to_toml(&self) -> Result<Vec<u8>, ConfigError> {
let mut toml_conf = TomlClientConfig::new();
toml_conf.populate_from_client_config(self);
Ok(toml::to_string_pretty(&toml_conf)?.into_bytes())
}
}
impl ClientConfigProfile {
pub fn load_from_env(
&mut self,
env_vars: Option<&HashMap<String, String>>,
) -> Result<(), ConfigError> {
let env_provider = match env_vars {
Some(map) => EnvProvider::Map(map),
None => EnvProvider::System,
};
if let Some(address) = env_provider.get("TEMPORAL_ADDRESS")? {
self.address = Some(address);
}
if let Some(namespace) = env_provider.get("TEMPORAL_NAMESPACE")? {
self.namespace = Some(namespace);
}
if let Some(api_key) = env_provider.get("TEMPORAL_API_KEY")? {
self.api_key = Some(api_key);
}
self.apply_tls_env_vars(&env_provider)?;
self.apply_codec_env_vars(&env_provider)?;
self.apply_grpc_meta_env_vars(&env_provider)?;
Ok(())
}
fn apply_tls_env_vars(&mut self, env_provider: &EnvProvider) -> Result<(), ConfigError> {
const TLS_ENV_VARS: &[&str] = &[
"TEMPORAL_TLS",
"TEMPORAL_TLS_CLIENT_CERT_PATH",
"TEMPORAL_TLS_CLIENT_CERT_DATA",
"TEMPORAL_TLS_CLIENT_KEY_PATH",
"TEMPORAL_TLS_CLIENT_KEY_DATA",
"TEMPORAL_TLS_SERVER_CA_CERT_PATH",
"TEMPORAL_TLS_SERVER_CA_CERT_DATA",
"TEMPORAL_TLS_SERVER_NAME",
"TEMPORAL_TLS_DISABLE_HOST_VERIFICATION",
];
if self.tls.is_none() && has_any_env_var(env_provider, TLS_ENV_VARS)? {
self.tls = Some(ClientConfigTLS::default());
}
if let Some(ref mut tls) = self.tls {
if let Some(disabled_str) = env_provider.get("TEMPORAL_TLS")?
&& let Some(disabled) = env_var_to_bool(&disabled_str)
{
tls.disabled = Some(!disabled);
}
apply_data_source_env_var(
env_provider,
"cert",
"TEMPORAL_TLS_CLIENT_CERT_PATH",
"TEMPORAL_TLS_CLIENT_CERT_DATA",
&mut tls.client_cert,
)?;
apply_data_source_env_var(
env_provider,
"key",
"TEMPORAL_TLS_CLIENT_KEY_PATH",
"TEMPORAL_TLS_CLIENT_KEY_DATA",
&mut tls.client_key,
)?;
apply_data_source_env_var(
env_provider,
"server CA cert",
"TEMPORAL_TLS_SERVER_CA_CERT_PATH",
"TEMPORAL_TLS_SERVER_CA_CERT_DATA",
&mut tls.server_ca_cert,
)?;
if let Some(v) = env_provider.get("TEMPORAL_TLS_SERVER_NAME")? {
tls.server_name = Some(v);
}
if let Some(v) = env_provider.get("TEMPORAL_TLS_DISABLE_HOST_VERIFICATION")?
&& let Some(b) = env_var_to_bool(&v)
{
tls.disable_host_verification = b;
}
}
Ok(())
}
fn apply_codec_env_vars(&mut self, env_provider: &EnvProvider) -> Result<(), ConfigError> {
const CODEC_ENV_VARS: &[&str] = &["TEMPORAL_CODEC_ENDPOINT", "TEMPORAL_CODEC_AUTH"];
if self.codec.is_none() && has_any_env_var(env_provider, CODEC_ENV_VARS)? {
self.codec = Some(ClientConfigCodec::default());
}
if let Some(ref mut codec) = self.codec {
if let Some(endpoint) = env_provider.get("TEMPORAL_CODEC_ENDPOINT")? {
codec.endpoint = Some(endpoint);
}
if let Some(auth) = env_provider.get("TEMPORAL_CODEC_AUTH")? {
codec.auth = Some(auth);
}
}
Ok(())
}
fn apply_grpc_meta_env_vars(&mut self, env_provider: &EnvProvider) -> Result<(), ConfigError> {
let mut handle_meta_var = |header_name: &str, value: &str| {
let normalized_name = normalize_grpc_meta_key(header_name);
if value.is_empty() {
self.grpc_meta.remove(&normalized_name);
} else {
self.grpc_meta.insert(normalized_name, value.to_string());
}
};
match env_provider {
EnvProvider::Map(map) => {
for (key, value) in map.iter() {
if let Some(header_name) = key.strip_prefix("TEMPORAL_GRPC_META_") {
handle_meta_var(header_name, value);
}
}
}
EnvProvider::System => {
for (key, value) in std::env::vars() {
if let Some(header_name) = key.strip_prefix("TEMPORAL_GRPC_META_") {
handle_meta_var(header_name, &value);
}
}
}
}
Ok(())
}
}
fn has_any_env_var(env_provider: &EnvProvider, keys: &[&str]) -> Result<bool, ConfigError> {
for &key in keys {
if env_provider.contains_key(key)? {
return Ok(true);
}
}
Ok(false)
}
fn apply_data_source_env_var(
env_provider: &EnvProvider,
name: &str,
path_var: &str,
data_var: &str,
dest: &mut Option<DataSource>,
) -> Result<(), ConfigError> {
let path_val = env_provider.get(path_var)?;
let data_val = env_provider.get(data_var)?;
match (path_val, data_val) {
(Some(_), Some(_)) => Err(ConfigError::InvalidConfig(format!(
"Cannot specify both {path_var} and {data_var}"
))),
(Some(path), None) => {
if let Some(DataSource::Data(_)) = dest {
return Err(ConfigError::InvalidConfig(format!(
"Cannot specify {name} path via {path_var} when {name} data is already specified"
)));
}
*dest = Some(DataSource::Path(path));
Ok(())
}
(None, Some(data)) => {
if let Some(DataSource::Path(_)) = dest {
return Err(ConfigError::InvalidConfig(format!(
"Cannot specify {name} data via {data_var} when {name} path is already specified"
)));
}
*dest = Some(DataSource::Data(data.into_bytes()));
Ok(())
}
(None, None) => Ok(()),
}
}
fn env_var_to_bool(s: &str) -> Option<bool> {
match s.to_lowercase().as_str() {
"true" | "1" => Some(true),
"false" | "0" => Some(false),
_ => None,
}
}
fn normalize_grpc_meta_key(key: &str) -> String {
key.to_lowercase().replace('_', "-")
}
fn get_default_config_file_path() -> Option<String> {
dirs::config_dir().map(|config_dir| {
config_dir
.join("temporalio")
.join(DEFAULT_CONFIG_FILE)
.to_string_lossy()
.to_string()
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TomlClientConfig {
#[serde(default, rename = "profile")]
profiles: HashMap<String, TomlClientConfigProfile>,
}
impl TomlClientConfig {
fn new() -> Self {
Self {
profiles: HashMap::new(),
}
}
fn apply_to_client_config(&self, conf: &mut ClientConfig) -> Result<(), ConfigError> {
conf.profiles = HashMap::with_capacity(self.profiles.len());
for (k, v) in &self.profiles {
conf.profiles.insert(k.clone(), v.to_client_config()?);
}
Ok(())
}
fn populate_from_client_config(&mut self, conf: &ClientConfig) {
self.profiles = HashMap::with_capacity(conf.profiles.len());
for (k, v) in &conf.profiles {
let mut prof = TomlClientConfigProfile::new();
prof.populate_from_client_config(v);
self.profiles.insert(k.clone(), prof);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TomlClientConfigProfile {
#[serde(skip_serializing_if = "Option::is_none")]
address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
namespace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
api_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tls: Option<TomlClientConfigTLS>,
#[serde(skip_serializing_if = "Option::is_none")]
codec: Option<TomlClientConfigCodec>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
grpc_meta: HashMap<String, String>,
}
impl TomlClientConfigProfile {
fn new() -> Self {
Self {
address: None,
namespace: None,
api_key: None,
tls: None,
codec: None,
grpc_meta: HashMap::new(),
}
}
fn to_client_config(&self) -> Result<ClientConfigProfile, ConfigError> {
let mut ret = ClientConfigProfile {
address: self.address.clone(),
namespace: self.namespace.clone(),
api_key: self.api_key.clone(),
tls: self
.tls
.as_ref()
.map(|tls| tls.to_client_config())
.transpose()?,
codec: self.codec.as_ref().map(|codec| codec.to_client_config()),
grpc_meta: HashMap::new(),
};
if !self.grpc_meta.is_empty() {
ret.grpc_meta = HashMap::with_capacity(self.grpc_meta.len());
for (k, v) in &self.grpc_meta {
ret.grpc_meta.insert(normalize_grpc_meta_key(k), v.clone());
}
}
Ok(ret)
}
fn populate_from_client_config(&mut self, conf: &ClientConfigProfile) {
self.address = conf.address.clone();
self.namespace = conf.namespace.clone();
self.api_key = conf.api_key.clone();
if let Some(ref tls_conf) = conf.tls {
let mut toml_tls = TomlClientConfigTLS::new();
toml_tls.populate_from_client_config(tls_conf);
self.tls = Some(toml_tls);
} else {
self.tls = None;
}
if let Some(ref codec_conf) = conf.codec {
let mut toml_codec = TomlClientConfigCodec::new();
toml_codec.populate_from_client_config(codec_conf);
self.codec = Some(toml_codec);
} else {
self.codec = None;
}
if !conf.grpc_meta.is_empty() {
self.grpc_meta = HashMap::with_capacity(conf.grpc_meta.len());
for (k, v) in &conf.grpc_meta {
self.grpc_meta.insert(normalize_grpc_meta_key(k), v.clone());
}
} else {
self.grpc_meta.clear();
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TomlClientConfigTLS {
#[serde(default, skip_serializing_if = "Option::is_none")]
disabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
client_cert_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
client_cert_data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
client_key_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
client_key_data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
server_ca_cert_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
server_ca_cert_data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
server_name: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
disable_host_verification: bool,
}
impl TomlClientConfigTLS {
fn new() -> Self {
Self {
disabled: None,
client_cert_path: None,
client_cert_data: None,
client_key_path: None,
client_key_data: None,
server_ca_cert_path: None,
server_ca_cert_data: None,
server_name: None,
disable_host_verification: false,
}
}
fn to_client_config(&self) -> Result<ClientConfigTLS, ConfigError> {
if self.client_cert_path.is_some() && self.client_cert_data.is_some() {
return Err(ConfigError::InvalidConfig(
"Cannot specify both client_cert_path and client_cert_data".to_string(),
));
}
if self.client_key_path.is_some() && self.client_key_data.is_some() {
return Err(ConfigError::InvalidConfig(
"Cannot specify both client_key_path and client_key_data".to_string(),
));
}
if self.server_ca_cert_path.is_some() && self.server_ca_cert_data.is_some() {
return Err(ConfigError::InvalidConfig(
"Cannot specify both server_ca_cert_path and server_ca_cert_data".to_string(),
));
}
let string_to_bytes = |s: &Option<String>| {
s.as_ref().and_then(|val| {
if val.is_empty() {
None
} else {
Some(val.as_bytes().to_vec())
}
})
};
Ok(ClientConfigTLS {
disabled: self.disabled,
client_cert: self
.client_cert_path
.clone()
.map(DataSource::Path)
.or_else(|| string_to_bytes(&self.client_cert_data).map(DataSource::Data)),
client_key: self
.client_key_path
.clone()
.map(DataSource::Path)
.or_else(|| string_to_bytes(&self.client_key_data).map(DataSource::Data)),
server_ca_cert: self
.server_ca_cert_path
.clone()
.map(DataSource::Path)
.or_else(|| string_to_bytes(&self.server_ca_cert_data).map(DataSource::Data)),
server_name: self.server_name.clone(),
disable_host_verification: self.disable_host_verification,
})
}
fn populate_from_client_config(&mut self, conf: &ClientConfigTLS) {
self.disabled = conf.disabled;
if let Some(ref cert_source) = conf.client_cert {
match cert_source {
DataSource::Path(p) => self.client_cert_path = Some(p.clone()),
DataSource::Data(d) => {
self.client_cert_data = Some(String::from_utf8_lossy(d).into_owned())
}
}
}
if let Some(ref key_source) = conf.client_key {
match key_source {
DataSource::Path(p) => self.client_key_path = Some(p.clone()),
DataSource::Data(d) => {
self.client_key_data = Some(String::from_utf8_lossy(d).into_owned())
}
}
}
if let Some(ref ca_source) = conf.server_ca_cert {
match ca_source {
DataSource::Path(p) => self.server_ca_cert_path = Some(p.clone()),
DataSource::Data(d) => {
self.server_ca_cert_data = Some(String::from_utf8_lossy(d).into_owned())
}
}
}
self.server_name = conf.server_name.clone();
self.disable_host_verification = conf.disable_host_verification;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TomlClientConfigCodec {
#[serde(skip_serializing_if = "Option::is_none")]
endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
auth: Option<String>,
}
impl TomlClientConfigCodec {
fn new() -> Self {
Self {
endpoint: None,
auth: None,
}
}
fn to_client_config(&self) -> ClientConfigCodec {
ClientConfigCodec {
endpoint: self.endpoint.clone(),
auth: self.auth.clone(),
}
}
fn populate_from_client_config(&mut self, conf: &ClientConfigCodec) {
self.endpoint = conf.endpoint.clone();
self.auth = conf.auth.clone();
}
}
mod strict {
use super::{
ClientConfig, ClientConfigCodec, ClientConfigProfile, ClientConfigTLS, ConfigError,
DataSource, normalize_grpc_meta_key,
};
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct StrictTomlClientConfig {
#[serde(default, rename = "profile")]
profiles: HashMap<String, StrictTomlClientConfigProfile>,
}
impl StrictTomlClientConfig {
pub(crate) fn apply_to_client_config(
self,
conf: &mut ClientConfig,
) -> Result<(), ConfigError> {
conf.profiles = HashMap::with_capacity(self.profiles.len());
for (k, v) in self.profiles {
conf.profiles.insert(k, v.into_client_config()?);
}
Ok(())
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictTomlClientConfigProfile {
#[serde(default)]
address: Option<String>,
#[serde(default)]
namespace: Option<String>,
#[serde(default)]
api_key: Option<String>,
#[serde(default)]
tls: Option<StrictTomlClientConfigTLS>,
#[serde(default)]
codec: Option<StrictTomlClientConfigCodec>,
#[serde(default)]
grpc_meta: HashMap<String, String>,
}
impl StrictTomlClientConfigProfile {
fn into_client_config(self) -> Result<ClientConfigProfile, ConfigError> {
let mut ret = ClientConfigProfile {
address: self.address,
namespace: self.namespace,
api_key: self.api_key,
tls: self.tls.map(|tls| tls.into_client_config()).transpose()?,
codec: self.codec.map(|codec| codec.into_client_config()),
grpc_meta: HashMap::new(),
};
if !self.grpc_meta.is_empty() {
ret.grpc_meta = HashMap::with_capacity(self.grpc_meta.len());
for (k, v) in self.grpc_meta {
ret.grpc_meta.insert(normalize_grpc_meta_key(&k), v);
}
}
Ok(ret)
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictTomlClientConfigTLS {
#[serde(default)]
disabled: Option<bool>,
#[serde(default)]
client_cert_path: Option<String>,
#[serde(default)]
client_cert_data: Option<String>,
#[serde(default)]
client_key_path: Option<String>,
#[serde(default)]
client_key_data: Option<String>,
#[serde(default)]
server_ca_cert_path: Option<String>,
#[serde(default)]
server_ca_cert_data: Option<String>,
#[serde(default)]
server_name: Option<String>,
#[serde(default)]
disable_host_verification: bool,
}
impl StrictTomlClientConfigTLS {
fn into_client_config(self) -> Result<ClientConfigTLS, ConfigError> {
if self.client_cert_path.is_some() && self.client_cert_data.is_some() {
return Err(ConfigError::InvalidConfig(
"Cannot specify both client_cert_path and client_cert_data".to_string(),
));
}
if self.client_key_path.is_some() && self.client_key_data.is_some() {
return Err(ConfigError::InvalidConfig(
"Cannot specify both client_key_path and client_key_data".to_string(),
));
}
if self.server_ca_cert_path.is_some() && self.server_ca_cert_data.is_some() {
return Err(ConfigError::InvalidConfig(
"Cannot specify both server_ca_cert_path and server_ca_cert_data".to_string(),
));
}
let string_to_bytes = |s: Option<String>| {
s.and_then(|val| {
if val.is_empty() {
None
} else {
Some(val.as_bytes().to_vec())
}
})
};
Ok(ClientConfigTLS {
disabled: self.disabled,
client_cert: self
.client_cert_path
.map(DataSource::Path)
.or_else(|| string_to_bytes(self.client_cert_data).map(DataSource::Data)),
client_key: self
.client_key_path
.map(DataSource::Path)
.or_else(|| string_to_bytes(self.client_key_data).map(DataSource::Data)),
server_ca_cert: self
.server_ca_cert_path
.map(DataSource::Path)
.or_else(|| string_to_bytes(self.server_ca_cert_data).map(DataSource::Data)),
server_name: self.server_name,
disable_host_verification: self.disable_host_verification,
})
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictTomlClientConfigCodec {
#[serde(default)]
endpoint: Option<String>,
#[serde(default)]
auth: Option<String>,
}
impl StrictTomlClientConfigCodec {
fn into_client_config(self) -> ClientConfigCodec {
ClientConfigCodec {
endpoint: self.endpoint,
auth: self.auth,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_client_config_toml_multiple_profiles() {
let toml_str = r#"
[profile.default]
address = "localhost:7233"
namespace = "default"
api_key = "test-key"
[profile.default.tls]
disabled = false
client_cert_path = "/path/to/cert"
[profile.prod]
address = "prod.temporal.io:7233"
namespace = "production"
"#;
let config = ClientConfig::from_toml(toml_str.as_bytes(), Default::default()).unwrap();
let default_profile = config.profiles.get("default").unwrap();
assert_eq!(default_profile.address.as_ref().unwrap(), "localhost:7233");
assert_eq!(default_profile.namespace.as_ref().unwrap(), "default");
assert_eq!(default_profile.api_key.as_ref().unwrap(), "test-key");
let tls = default_profile.tls.as_ref().unwrap();
assert_eq!(tls.disabled, Some(false)); assert_eq!(
tls.client_cert,
Some(DataSource::Path("/path/to/cert".to_string()))
);
let prod_profile = config.profiles.get("prod").unwrap();
assert_eq!(
prod_profile.address.as_ref().unwrap(),
"prod.temporal.io:7233"
);
assert_eq!(prod_profile.namespace.as_ref().unwrap(), "production");
}
#[test]
fn test_client_config_toml_roundtrip() {
let mut prof = ClientConfigProfile {
address: Some("addr".to_string()),
namespace: Some("ns".to_string()),
api_key: Some("key".to_string()),
..Default::default()
};
prof.grpc_meta.insert("k".to_string(), "v".to_string());
let tls = ClientConfigTLS {
client_cert: Some(DataSource::Data(b"cert".to_vec())),
server_ca_cert: Some(DataSource::Data(b"ca".to_vec())),
..Default::default()
};
prof.tls = Some(tls);
let mut conf = ClientConfig::default();
conf.profiles.insert("default".to_string(), prof);
let toml_bytes = conf.to_toml().unwrap();
let new_conf = ClientConfig::from_toml(&toml_bytes, Default::default()).unwrap();
assert_eq!(conf, new_conf);
}
#[test]
fn test_load_client_config_profile_from_file() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(
temp_file,
r#"
[profile.default]
address = "my-address"
namespace = "my-namespace"
"#
)
.unwrap();
let options = LoadClientConfigProfileOptions {
config_source: Some(DataSource::Path(
temp_file.path().to_string_lossy().to_string(),
)),
..Default::default()
};
let profile = load_client_config_profile(options, None).unwrap();
assert_eq!(profile.address.as_ref().unwrap(), "my-address");
assert_eq!(profile.namespace.as_ref().unwrap(), "my-namespace");
}
#[test]
fn test_load_client_config_profile_from_env_file_path() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(
temp_file,
r#"
[profile.default]
address = "my-address"
namespace = "my-namespace"
"#
)
.unwrap();
let mut vars = HashMap::new();
vars.insert(
"TEMPORAL_CONFIG_FILE".to_string(),
temp_file.path().to_string_lossy().to_string(),
);
let options = LoadClientConfigProfileOptions {
..Default::default()
};
let profile = load_client_config_profile(options, Some(&vars)).unwrap();
assert_eq!(profile.address.as_ref().unwrap(), "my-address");
assert_eq!(profile.namespace.as_ref().unwrap(), "my-namespace");
}
#[test]
fn test_load_client_config_profile_with_env_overrides() {
let toml_str = r#"
[profile.default]
address = "my-address"
namespace = "my-namespace"
api_key = "my-api-key"
[profile.default.tls]
disabled = true
client_cert_path = "my-client-cert-path"
client_key_path = "my-client-key-path"
server_name = "my-server-name"
disable_host_verification = true
[profile.default.codec]
endpoint = "my-codec-endpoint"
auth = "my-codec-auth"
[profile.default.grpc_meta]
some-header = "some-value"
some-other-header = "some-value2"
"#;
let mut vars = HashMap::new();
vars.insert("TEMPORAL_ADDRESS".to_string(), "my-address-new".to_string());
vars.insert(
"TEMPORAL_NAMESPACE".to_string(),
"my-namespace-new".to_string(),
);
vars.insert("TEMPORAL_API_KEY".to_string(), "my-api-key-new".to_string());
vars.insert("TEMPORAL_TLS".to_string(), "true".to_string());
vars.insert(
"TEMPORAL_TLS_CLIENT_CERT_PATH".to_string(),
"my-client-cert-path-new".to_string(),
);
vars.insert(
"TEMPORAL_TLS_CLIENT_KEY_PATH".to_string(),
"my-client-key-path-new".to_string(),
);
vars.insert(
"TEMPORAL_TLS_SERVER_NAME".to_string(),
"my-server-name-new".to_string(),
);
vars.insert(
"TEMPORAL_TLS_DISABLE_HOST_VERIFICATION".to_string(),
"false".to_string(),
);
vars.insert(
"TEMPORAL_CODEC_ENDPOINT".to_string(),
"my-codec-endpoint-new".to_string(),
);
vars.insert(
"TEMPORAL_CODEC_AUTH".to_string(),
"my-codec-auth-new".to_string(),
);
vars.insert(
"TEMPORAL_GRPC_META_SOME_HEADER".to_string(),
"some-value-new".to_string(),
);
vars.insert(
"TEMPORAL_GRPC_META_SOME_THIRD_HEADER".to_string(),
"some-value3-new".to_string(),
);
vars.insert(
"TEMPORAL_GRPC_META_some_value4".to_string(),
"some-value4-new".to_string(),
);
let options = LoadClientConfigProfileOptions {
config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
config_file_profile: Some("default".to_string()),
..Default::default()
};
let profile = load_client_config_profile(options, Some(&vars)).unwrap();
assert_eq!(profile.address.as_ref().unwrap(), "my-address-new");
assert_eq!(profile.namespace.as_ref().unwrap(), "my-namespace-new");
assert_eq!(profile.api_key.as_ref().unwrap(), "my-api-key-new");
let tls = profile.tls.as_ref().unwrap();
assert_eq!(tls.disabled, Some(false)); assert_eq!(
tls.client_cert,
Some(DataSource::Path("my-client-cert-path-new".to_string()))
);
assert_eq!(
tls.client_key,
Some(DataSource::Path("my-client-key-path-new".to_string()))
);
assert_eq!(tls.server_name.as_ref().unwrap(), "my-server-name-new");
assert!(!tls.disable_host_verification);
let codec = profile.codec.as_ref().unwrap();
assert_eq!(codec.endpoint.as_ref().unwrap(), "my-codec-endpoint-new");
assert_eq!(codec.auth.as_ref().unwrap(), "my-codec-auth-new");
assert_eq!(
profile.grpc_meta.get("some-header").unwrap(),
"some-value-new"
);
assert_eq!(
profile.grpc_meta.get("some-other-header").unwrap(),
"some-value2"
);
assert_eq!(
profile.grpc_meta.get("some-third-header").unwrap(),
"some-value3-new"
);
assert_eq!(
profile.grpc_meta.get("some-value4").unwrap(),
"some-value4-new"
);
}
#[test]
fn test_client_config_toml_full() {
let toml_str = r#"
[profile.foo]
address = "my-address"
namespace = "my-namespace"
api_key = "my-api-key"
some_future_key = "some future value not handled"
[profile.foo.tls]
disabled = true
client_cert_path = "my-client-cert-path"
client_key_path = "my-client-key-path"
server_ca_cert_path = "my-server-ca-cert-path"
server_name = "my-server-name"
disable_host_verification = true
[profile.foo.codec]
endpoint = "my-endpoint"
auth = "my-auth"
[profile.foo.grpc_meta]
sOme-hEader_key = "some-value"
"#;
let config = ClientConfig::from_toml(toml_str.as_bytes(), Default::default()).unwrap();
let profile = config.profiles.get("foo").unwrap();
assert_eq!(profile.address.as_ref().unwrap(), "my-address");
assert_eq!(profile.namespace.as_ref().unwrap(), "my-namespace");
assert_eq!(profile.api_key.as_ref().unwrap(), "my-api-key");
let codec = profile.codec.as_ref().unwrap();
assert_eq!(codec.endpoint.as_ref().unwrap(), "my-endpoint");
assert_eq!(codec.auth.as_ref().unwrap(), "my-auth");
let tls = profile.tls.as_ref().unwrap();
assert_eq!(tls.disabled, Some(true)); assert_eq!(
tls.client_cert,
Some(DataSource::Path("my-client-cert-path".to_string()))
);
assert_eq!(
tls.client_key,
Some(DataSource::Path("my-client-key-path".to_string()))
);
assert_eq!(
tls.server_ca_cert,
Some(DataSource::Path("my-server-ca-cert-path".to_string()))
);
assert_eq!(tls.server_name.as_ref().unwrap(), "my-server-name");
assert!(tls.disable_host_verification);
assert_eq!(profile.grpc_meta.len(), 1);
assert_eq!(
profile.grpc_meta.get("some-header-key").unwrap(),
"some-value"
);
let toml_out = config.to_toml().unwrap();
let config2 = ClientConfig::from_toml(&toml_out, Default::default()).unwrap();
assert_eq!(config, config2);
}
#[test]
fn test_client_config_toml_partial() {
let toml_str = r#"
[profile.foo]
api_key = "my-api-key"
[profile.foo.tls]
"#;
let config = ClientConfig::from_toml(toml_str.as_bytes(), Default::default()).unwrap();
let profile = config.profiles.get("foo").unwrap();
assert!(profile.address.is_none());
assert!(profile.namespace.is_none());
assert_eq!(profile.api_key.as_ref().unwrap(), "my-api-key");
assert!(profile.codec.is_none());
assert!(profile.tls.is_some());
let tls = profile.tls.as_ref().unwrap();
assert_eq!(tls.disabled, None); assert!(tls.client_cert.is_none());
}
#[test]
fn test_client_config_toml_empty() {
let config = ClientConfig::from_toml("".as_bytes(), Default::default()).unwrap();
assert!(config.profiles.is_empty());
let toml_out = config.to_toml().unwrap();
let config2 = ClientConfig::from_toml(&toml_out, Default::default()).unwrap();
assert_eq!(config, config2);
}
#[test]
fn test_profile_not_found() {
let toml_str = r#"
[profile.existing]
address = "localhost:7233"
"#;
let options = LoadClientConfigProfileOptions {
config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
config_file_profile: Some("nonexistent".to_string()),
..Default::default()
};
let result = load_client_config_profile(options, None);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ConfigError::ProfileNotFound(p) if p == "nonexistent"
));
}
#[test]
fn test_client_config_toml_strict_unrecognized_field() {
let toml_str = r#"
[profile.default]
unrecognized_field = "is-bad"
"#;
let err = ClientConfig::from_toml(
toml_str.as_bytes(),
ClientConfigFromTOMLOptions { strict: true },
)
.unwrap_err();
let err_str = err.to_string();
assert!(err_str.contains("unrecognized_field"));
}
#[test]
fn test_client_config_toml_strict_unrecognized_table() {
let toml_str = r#"
[unrecognized_table]
foo = "bar"
"#;
let err = ClientConfig::from_toml(
toml_str.as_bytes(),
ClientConfigFromTOMLOptions { strict: true },
)
.unwrap_err();
let err_str = err.to_string();
assert!(err_str.contains("unrecognized_table"));
}
#[test]
fn test_client_config_both_path_and_data_fails() {
let mut vars = HashMap::new();
vars.insert(
"TEMPORAL_TLS_CLIENT_CERT_PATH".to_string(),
"some-path".to_string(),
);
vars.insert(
"TEMPORAL_TLS_CLIENT_CERT_DATA".to_string(),
"some-data".to_string(),
);
let options = LoadClientConfigProfileOptions {
..Default::default()
};
let err = load_client_config_profile(options, Some(&vars)).unwrap_err();
assert!(matches!(err, ConfigError::InvalidConfig(_)));
assert!(err.to_string().contains("Cannot specify both"));
let toml_str = r#"
[profile.default]
[profile.default.tls]
client_cert_path = "some-path"
client_cert_data = "some-data"
"#;
let err = ClientConfig::from_toml(toml_str.as_bytes(), Default::default()).unwrap_err();
assert!(matches!(err, ConfigError::InvalidConfig(_)));
assert!(err.to_string().contains("Cannot specify both"));
}
#[test]
fn test_client_config_path_data_conflict_across_sources() {
let toml_str = r#"
[profile.default]
[profile.default.tls]
client_cert_path = "some-path"
"#;
let mut vars = HashMap::new();
vars.insert(
"TEMPORAL_TLS_CLIENT_CERT_DATA".to_string(),
"some-data".to_string(),
);
let options = LoadClientConfigProfileOptions {
config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
..Default::default()
};
let err = load_client_config_profile(options, Some(&vars)).unwrap_err();
assert!(matches!(err, ConfigError::InvalidConfig(_)));
assert!(
err.to_string()
.contains("when cert path is already specified")
);
let toml_str = r#"
[profile.default]
[profile.default.tls]
client_cert_data = "some-data"
"#;
let mut vars = HashMap::new();
vars.insert(
"TEMPORAL_TLS_CLIENT_CERT_PATH".to_string(),
"some-path".to_string(),
);
let options = LoadClientConfigProfileOptions {
config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
..Default::default()
};
let err = load_client_config_profile(options, Some(&vars)).unwrap_err();
assert!(matches!(err, ConfigError::InvalidConfig(_)));
assert!(
err.to_string()
.contains("when cert data is already specified")
);
}
#[test]
fn test_default_profile_not_found_is_ok() {
let toml_str = r#"
[profile.existing]
address = "localhost:7233"
"#;
let options = LoadClientConfigProfileOptions {
config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
..Default::default()
};
let profile = load_client_config_profile(options, None).unwrap();
assert_eq!(profile, ClientConfigProfile::default());
}
#[test]
fn test_normalize_grpc_meta_key() {
assert_eq!(normalize_grpc_meta_key("SOME_HEADER"), "some-header");
assert_eq!(normalize_grpc_meta_key("some_header"), "some-header");
assert_eq!(normalize_grpc_meta_key("Some_Header"), "some-header");
}
#[test]
fn test_env_var_to_bool() {
assert_eq!(env_var_to_bool("true"), Some(true));
assert_eq!(env_var_to_bool("TRUE"), Some(true));
assert_eq!(env_var_to_bool("1"), Some(true));
assert_eq!(env_var_to_bool("false"), Some(false));
assert_eq!(env_var_to_bool("FALSE"), Some(false));
assert_eq!(env_var_to_bool("0"), Some(false));
assert_eq!(env_var_to_bool("invalid"), None);
assert_eq!(env_var_to_bool("yes"), None);
assert_eq!(env_var_to_bool("no"), None);
}
#[test]
fn test_load_client_config_profile_disables_are_an_error() {
let options = LoadClientConfigProfileOptions {
disable_file: true,
disable_env: true,
..Default::default()
};
let result = load_client_config_profile(options, None);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Cannot disable both file and environment")
);
}
#[test]
fn test_load_client_config_profile_from_env_only() {
let mut vars = HashMap::new();
vars.insert("TEMPORAL_ADDRESS".to_string(), "env-address".to_string());
vars.insert(
"TEMPORAL_NAMESPACE".to_string(),
"env-namespace".to_string(),
);
let options = LoadClientConfigProfileOptions {
disable_file: true,
..Default::default()
};
let profile = load_client_config_profile(options, Some(&vars)).unwrap();
assert_eq!(profile.address.as_ref().unwrap(), "env-address");
assert_eq!(profile.namespace.as_ref().unwrap(), "env-namespace");
}
#[test]
fn test_no_api_key_no_tls_is_none() {
let toml_str = r#"
[profile.default]
address = "some-address"
"#;
let options = LoadClientConfigProfileOptions {
config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
..Default::default()
};
let profile = load_client_config_profile(options, None).unwrap();
assert!(profile.tls.is_none());
}
#[test]
fn test_load_client_config_profile_from_system_env() {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let output = std::process::Command::new(cargo)
.arg("test")
.arg("-F")
.arg("envconfig")
.arg("envconfig::tests::test_load_client_config_profile_from_system_env_impl")
.arg("--")
.arg("--exact")
.arg("--ignored")
.env("TEMPORAL_ADDRESS", "system-address")
.env("TEMPORAL_NAMESPACE", "system-namespace")
.output()
.expect("Failed to execute subprocess test");
assert!(
output.status.success(),
"Subprocess test failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
#[test]
#[ignore] fn test_load_client_config_profile_from_system_env_impl() {
if std::env::var("TEMPORAL_ADDRESS").is_err()
|| std::env::var("TEMPORAL_NAMESPACE").is_err()
{
eprintln!("Skipping test - required env vars not set");
return; }
let options = LoadClientConfigProfileOptions {
disable_file: true, ..Default::default()
};
let profile = load_client_config_profile(options, None).unwrap();
assert_eq!(profile.address.as_ref().unwrap(), "system-address");
assert_eq!(profile.namespace.as_ref().unwrap(), "system-namespace");
}
#[test]
fn test_tls_disabled_tri_state_behavior() {
let tls_unset = ClientConfigTLS {
disabled: None,
..Default::default()
};
let profile_unset = ClientConfigProfile {
address: Some("localhost:7233".to_string()),
tls: Some(tls_unset),
..Default::default()
};
let mut config_unset = ClientConfig::default();
config_unset
.profiles
.insert("test".to_string(), profile_unset);
let toml_unset = config_unset.to_toml().unwrap();
let toml_str_unset = String::from_utf8(toml_unset).unwrap();
assert!(!toml_str_unset.contains("disabled"));
let parsed_unset =
ClientConfig::from_toml(toml_str_unset.as_bytes(), Default::default()).unwrap();
assert_eq!(
parsed_unset
.profiles
.get("test")
.unwrap()
.tls
.as_ref()
.unwrap()
.disabled,
None
);
let tls_enabled = ClientConfigTLS {
disabled: Some(false),
..Default::default()
};
let profile_enabled = ClientConfigProfile {
address: Some("localhost:7233".to_string()),
tls: Some(tls_enabled),
..Default::default()
};
let mut config_enabled = ClientConfig::default();
config_enabled
.profiles
.insert("test".to_string(), profile_enabled);
let toml_enabled = config_enabled.to_toml().unwrap();
let toml_str_enabled = String::from_utf8(toml_enabled).unwrap();
assert!(toml_str_enabled.contains("disabled = false"));
let parsed_enabled =
ClientConfig::from_toml(toml_str_enabled.as_bytes(), Default::default()).unwrap();
assert_eq!(
parsed_enabled
.profiles
.get("test")
.unwrap()
.tls
.as_ref()
.unwrap()
.disabled,
Some(false)
);
let tls_disabled = ClientConfigTLS {
disabled: Some(true),
..Default::default()
};
let profile_disabled = ClientConfigProfile {
address: Some("localhost:7233".to_string()),
tls: Some(tls_disabled),
..Default::default()
};
let mut config_disabled = ClientConfig::default();
config_disabled
.profiles
.insert("test".to_string(), profile_disabled);
let toml_disabled = config_disabled.to_toml().unwrap();
let toml_str_disabled = String::from_utf8(toml_disabled).unwrap();
assert!(toml_str_disabled.contains("disabled = true"));
let parsed_disabled =
ClientConfig::from_toml(toml_str_disabled.as_bytes(), Default::default()).unwrap();
assert_eq!(
parsed_disabled
.profiles
.get("test")
.unwrap()
.tls
.as_ref()
.unwrap()
.disabled,
Some(true)
);
}
#[test]
fn test_load_default_config_path_not_exist() {
let config = load_client_config_inner(
LoadClientConfigOptions::default(),
Some(&HashMap::new()),
None,
)
.unwrap();
assert_eq!(config, ClientConfig::default());
}
}