use crate::error::Result as CliResult;
use anyhow::Context;
use redisctl_core::{Config, DeploymentType};
use tracing::{debug, info, trace};
const REDISCTL_USER_AGENT: &str = concat!("redisctl/", env!("CARGO_PKG_VERSION"));
#[allow(dead_code)] pub struct CloudConnectionInfo {
pub base_url: String,
pub api_key: String,
pub api_secret: String,
pub user_agent: String,
}
#[allow(dead_code)] pub struct EnterpriseConnectionInfo {
pub base_url: String,
pub username: String,
pub password: Option<String>,
pub insecure: bool,
pub ca_cert: Option<String>,
pub user_agent: String,
}
#[allow(dead_code)] #[derive(Clone)]
pub struct ConnectionManager {
pub config: Config,
pub config_path: Option<std::path::PathBuf>,
}
impl ConnectionManager {
#[allow(dead_code)] pub fn new(config: Config) -> Self {
Self {
config,
config_path: None,
}
}
#[allow(dead_code)] pub fn with_config_path(config: Config, config_path: Option<std::path::PathBuf>) -> Self {
Self {
config,
config_path,
}
}
#[allow(dead_code)] pub fn save_config(&self) -> CliResult<()> {
if let Some(ref path) = self.config_path {
self.config
.save_to_path(path)
.context("Failed to save configuration")?;
} else {
self.config.save().context("Failed to save configuration")?;
}
Ok(())
}
#[allow(dead_code)] pub fn resolve_cloud_connection(
&self,
profile_name: Option<&str>,
) -> CliResult<CloudConnectionInfo> {
let (api_key, api_secret, base_url) = self.resolve_cloud_credentials(profile_name)?;
Ok(CloudConnectionInfo {
base_url,
api_key,
api_secret,
user_agent: REDISCTL_USER_AGENT.to_string(),
})
}
#[allow(dead_code)] pub async fn create_cloud_client(
&self,
profile_name: Option<&str>,
) -> CliResult<redis_cloud::CloudClient> {
debug!("Creating Redis Cloud client");
let (final_api_key, final_api_secret, final_api_url) =
self.resolve_cloud_credentials(profile_name)?;
info!("Connecting to Redis Cloud API: {}", final_api_url);
trace!(
"API key: {}...",
&final_api_key[..final_api_key.len().min(8)]
);
let client = redis_cloud::CloudClient::builder()
.api_key(&final_api_key)
.api_secret(&final_api_secret)
.base_url(&final_api_url)
.user_agent(REDISCTL_USER_AGENT)
.build()
.context("Failed to create Redis Cloud client")?;
debug!("Redis Cloud client created successfully");
Ok(client)
}
fn resolve_cloud_credentials(
&self,
profile_name: Option<&str>,
) -> CliResult<(String, String, String)> {
trace!("Profile name: {:?}", profile_name);
let use_env_vars = self.config_path.is_none();
debug!(
"Config path: {:?}, use_env_vars: {}",
self.config_path, use_env_vars
);
if !use_env_vars {
info!("--config-file specified explicitly, ignoring environment variables");
}
let env_api_key = if use_env_vars {
std::env::var("REDIS_CLOUD_API_KEY").ok()
} else {
None
};
let env_api_secret = if use_env_vars {
std::env::var("REDIS_CLOUD_SECRET_KEY").ok()
} else {
None
};
let env_api_url = if use_env_vars {
std::env::var("REDIS_CLOUD_API_URL").ok()
} else {
None
};
if env_api_key.is_some() {
debug!("Found REDIS_CLOUD_API_KEY environment variable");
}
if env_api_secret.is_some() {
debug!("Found REDIS_CLOUD_SECRET_KEY environment variable");
}
if env_api_url.is_some() {
debug!("Found REDIS_CLOUD_API_URL environment variable");
}
let result = if let (Some(key), Some(secret)) = (&env_api_key, &env_api_secret) {
info!("Using Redis Cloud credentials from environment variables");
let url = env_api_url.unwrap_or_else(|| "https://api.redislabs.com/v1".to_string());
(key.clone(), secret.clone(), url)
} else {
let resolved_profile_name = self.config.resolve_cloud_profile(profile_name)?;
info!("Using Redis Cloud profile: {}", resolved_profile_name);
let profile = self
.config
.profiles
.get(&resolved_profile_name)
.with_context(|| format!("Profile '{}' not found", resolved_profile_name))?;
if profile.deployment_type != DeploymentType::Cloud {
return Err(crate::error::RedisCtlError::ProfileTypeMismatch {
name: resolved_profile_name.to_string(),
actual_type: match profile.deployment_type {
DeploymentType::Cloud => "cloud",
DeploymentType::Enterprise => "enterprise",
DeploymentType::Database => "database",
}
.to_string(),
expected_type: "cloud".to_string(),
available_profiles: self
.config
.get_profiles_of_type(DeploymentType::Cloud)
.into_iter()
.map(String::from)
.collect(),
});
}
let (api_key, api_secret, api_url) = profile
.resolve_cloud_credentials()
.context("Failed to resolve Cloud credentials")?
.context("Profile is not configured for Redis Cloud")?;
let has_overrides =
env_api_key.is_some() || env_api_secret.is_some() || env_api_url.is_some();
let key = env_api_key.unwrap_or(api_key);
let secret = env_api_secret.unwrap_or(api_secret);
let url = env_api_url.unwrap_or(api_url);
if has_overrides {
debug!("Applied partial environment variable overrides");
}
(key, secret, url)
};
Ok(result)
}
#[allow(dead_code)] pub fn resolve_enterprise_connection(
&self,
profile_name: Option<&str>,
) -> CliResult<EnterpriseConnectionInfo> {
let (url, username, password, insecure, ca_cert) =
self.resolve_enterprise_credentials(profile_name)?;
Ok(EnterpriseConnectionInfo {
base_url: url,
username,
password,
insecure,
ca_cert,
user_agent: REDISCTL_USER_AGENT.to_string(),
})
}
#[allow(dead_code)] pub async fn create_enterprise_client(
&self,
profile_name: Option<&str>,
) -> CliResult<redis_enterprise::EnterpriseClient> {
debug!("Creating Redis Enterprise client");
let (final_url, final_username, final_password, final_insecure, final_ca_cert) =
self.resolve_enterprise_credentials(profile_name)?;
info!("Connecting to Redis Enterprise: {}", final_url);
debug!("Username: {}", final_username);
debug!(
"Password: {}",
if final_password.is_some() {
"configured"
} else {
"not set"
}
);
debug!("Insecure mode: {}", final_insecure);
debug!(
"CA cert: {}",
if final_ca_cert.is_some() {
"configured"
} else {
"not set"
}
);
let mut builder = redis_enterprise::EnterpriseClient::builder()
.base_url(&final_url)
.username(&final_username)
.user_agent(REDISCTL_USER_AGENT);
if let Some(ref password) = final_password {
builder = builder.password(password);
trace!("Password added to client builder");
}
if final_insecure {
builder = builder.insecure(true);
debug!("SSL certificate verification disabled");
}
if let Some(ref ca_cert_path) = final_ca_cert {
builder = builder.ca_cert(ca_cert_path);
debug!("Using custom CA certificate: {}", ca_cert_path);
}
let client = builder
.build()
.context("Failed to create Redis Enterprise client")?;
debug!("Redis Enterprise client created successfully");
Ok(client)
}
#[allow(clippy::type_complexity)]
fn resolve_enterprise_credentials(
&self,
profile_name: Option<&str>,
) -> CliResult<(String, String, Option<String>, bool, Option<String>)> {
trace!("Profile name: {:?}", profile_name);
let use_env_vars = self.config_path.is_none();
debug!(
"Config path: {:?}, use_env_vars: {}",
self.config_path, use_env_vars
);
if !use_env_vars {
info!("--config-file specified explicitly, ignoring environment variables");
}
let env_url = if use_env_vars {
std::env::var("REDIS_ENTERPRISE_URL").ok()
} else {
None
};
let env_user = if use_env_vars {
std::env::var("REDIS_ENTERPRISE_USER").ok()
} else {
None
};
let env_password = if use_env_vars {
std::env::var("REDIS_ENTERPRISE_PASSWORD").ok()
} else {
None
};
let env_insecure = if use_env_vars {
std::env::var("REDIS_ENTERPRISE_INSECURE").ok()
} else {
None
};
let env_ca_cert = if use_env_vars {
std::env::var("REDIS_ENTERPRISE_CA_CERT").ok()
} else {
None
};
if env_url.is_some() {
debug!("Found REDIS_ENTERPRISE_URL environment variable");
}
if env_user.is_some() {
debug!("Found REDIS_ENTERPRISE_USER environment variable");
}
if env_password.is_some() {
debug!("Found REDIS_ENTERPRISE_PASSWORD environment variable");
}
if env_insecure.is_some() {
debug!("Found REDIS_ENTERPRISE_INSECURE environment variable");
}
if env_ca_cert.is_some() {
debug!("Found REDIS_ENTERPRISE_CA_CERT environment variable");
}
let result = if let (Some(url), Some(user)) = (&env_url, &env_user) {
info!("Using Redis Enterprise credentials from environment variables");
let password = env_password.clone();
let insecure = env_insecure
.as_ref()
.map(|s| s.to_lowercase() == "true" || s == "1")
.unwrap_or(false);
let ca_cert = env_ca_cert.clone();
(url.clone(), user.clone(), password, insecure, ca_cert)
} else {
let resolved_profile_name = self.config.resolve_enterprise_profile(profile_name)?;
info!("Using Redis Enterprise profile: {}", resolved_profile_name);
let profile = self
.config
.profiles
.get(&resolved_profile_name)
.with_context(|| format!("Profile '{}' not found", resolved_profile_name))?;
if profile.deployment_type != DeploymentType::Enterprise {
return Err(crate::error::RedisCtlError::ProfileTypeMismatch {
name: resolved_profile_name.to_string(),
actual_type: match profile.deployment_type {
DeploymentType::Cloud => "cloud",
DeploymentType::Enterprise => "enterprise",
DeploymentType::Database => "database",
}
.to_string(),
expected_type: "enterprise".to_string(),
available_profiles: self
.config
.get_profiles_of_type(DeploymentType::Enterprise)
.into_iter()
.map(String::from)
.collect(),
});
}
let (url, username, password, insecure, profile_ca_cert) = profile
.resolve_enterprise_credentials()
.context("Failed to resolve Enterprise credentials")?
.context("Profile is not configured for Redis Enterprise")?;
let has_overrides = env_url.is_some()
|| env_user.is_some()
|| env_password.is_some()
|| env_insecure.is_some()
|| env_ca_cert.is_some();
let final_url = env_url.unwrap_or(url);
let final_user = env_user.unwrap_or(username);
let final_password = env_password.or(password);
let final_insecure = env_insecure
.as_ref()
.map(|s| s.to_lowercase() == "true" || s == "1")
.unwrap_or(insecure);
let final_ca_cert = env_ca_cert.or(profile_ca_cert);
if has_overrides {
debug!("Applied partial environment variable overrides");
}
(
final_url,
final_user,
final_password,
final_insecure,
final_ca_cert,
)
};
Ok(result)
}
}