use anyhow::{Context, Result};
use redis_cloud::CloudClient;
use redis_enterprise::EnterpriseClient;
use redisctl_config::Config;
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum CredentialSource {
Profile(Option<String>),
OAuth {
issuer: Option<String>,
audience: Option<String>,
},
}
pub struct CachedClients {
pub cloud: Option<CloudClient>,
pub enterprise: Option<EnterpriseClient>,
}
pub struct AppState {
pub credential_source: CredentialSource,
pub read_only: bool,
pub database_url: Option<String>,
config: Option<Config>,
clients: RwLock<CachedClients>,
}
impl AppState {
pub fn new(
credential_source: CredentialSource,
read_only: bool,
database_url: Option<String>,
) -> Result<Self> {
let config = match &credential_source {
CredentialSource::Profile(_) => Config::load().ok(),
CredentialSource::OAuth { .. } => None,
};
Ok(Self {
credential_source,
read_only,
database_url,
config,
clients: RwLock::new(CachedClients {
cloud: None,
enterprise: None,
}),
})
}
pub async fn cloud_client(&self) -> Result<CloudClient> {
{
let clients = self.clients.read().await;
if let Some(client) = &clients.cloud {
return Ok(client.clone());
}
}
let client = self.create_cloud_client().await?;
{
let mut clients = self.clients.write().await;
clients.cloud = Some(client.clone());
}
Ok(client)
}
pub async fn enterprise_client(&self) -> Result<EnterpriseClient> {
{
let clients = self.clients.read().await;
if let Some(client) = &clients.enterprise {
return Ok(client.clone());
}
}
let client = self.create_enterprise_client().await?;
{
let mut clients = self.clients.write().await;
clients.enterprise = Some(client.clone());
}
Ok(client)
}
async fn create_cloud_client(&self) -> Result<CloudClient> {
match &self.credential_source {
CredentialSource::Profile(profile_name) => {
let config = self
.config
.as_ref()
.context("No redisctl config available")?;
let resolved_profile_name = config
.resolve_cloud_profile(profile_name.as_deref())
.context("Failed to resolve cloud profile")?;
let profile = config
.profiles
.get(&resolved_profile_name)
.with_context(|| format!("Profile '{}' not found", resolved_profile_name))?;
let (api_key, api_secret, _base_url) = profile
.resolve_cloud_credentials()
.context("Failed to resolve cloud credentials")?
.context("No cloud credentials in profile")?;
CloudClient::builder()
.api_key(api_key)
.api_secret(api_secret)
.build()
.context("Failed to build Cloud client")
}
CredentialSource::OAuth { .. } => {
let api_key =
std::env::var("REDIS_CLOUD_API_KEY").context("REDIS_CLOUD_API_KEY not set")?;
let api_secret = std::env::var("REDIS_CLOUD_API_SECRET")
.context("REDIS_CLOUD_API_SECRET not set")?;
CloudClient::builder()
.api_key(api_key)
.api_secret(api_secret)
.build()
.context("Failed to build Cloud client")
}
}
}
async fn create_enterprise_client(&self) -> Result<EnterpriseClient> {
match &self.credential_source {
CredentialSource::Profile(profile_name) => {
let config = self
.config
.as_ref()
.context("No redisctl config available")?;
let resolved_profile_name = config
.resolve_enterprise_profile(profile_name.as_deref())
.context("Failed to resolve enterprise profile")?;
let profile = config
.profiles
.get(&resolved_profile_name)
.with_context(|| format!("Profile '{}' not found", resolved_profile_name))?;
let (url, username, password, insecure) = profile
.resolve_enterprise_credentials()
.context("Failed to resolve enterprise credentials")?
.context("No enterprise credentials in profile")?;
let mut builder = EnterpriseClient::builder()
.base_url(&url)
.username(&username)
.insecure(insecure);
if let Some(pwd) = password {
builder = builder.password(&pwd);
}
builder.build().context("Failed to build Enterprise client")
}
CredentialSource::OAuth { .. } => {
let url = std::env::var("REDIS_ENTERPRISE_URL")
.context("REDIS_ENTERPRISE_URL not set")?;
let username = std::env::var("REDIS_ENTERPRISE_USER")
.context("REDIS_ENTERPRISE_USER not set")?;
let password = std::env::var("REDIS_ENTERPRISE_PASSWORD").ok();
let insecure = std::env::var("REDIS_ENTERPRISE_INSECURE")
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
let mut builder = EnterpriseClient::builder()
.base_url(&url)
.username(&username)
.insecure(insecure);
if let Some(pwd) = password {
builder = builder.password(&pwd);
}
builder.build().context("Failed to build Enterprise client")
}
}
}
#[allow(dead_code)]
pub async fn redis_connection(&self) -> Result<redis::aio::MultiplexedConnection> {
let url = self
.database_url
.as_ref()
.cloned()
.or_else(|| std::env::var("REDIS_URL").ok())
.context("No Redis URL configured")?;
let client = redis::Client::open(url.as_str()).context("Failed to create Redis client")?;
client
.get_multiplexed_async_connection()
.await
.context("Failed to connect to Redis")
}
#[allow(dead_code)]
pub fn is_write_allowed(&self) -> bool {
!self.read_only
}
}
impl Clone for AppState {
fn clone(&self) -> Self {
Self {
credential_source: self.credential_source.clone(),
read_only: self.read_only,
database_url: self.database_url.clone(),
config: self.config.clone(),
clients: RwLock::new(CachedClients {
cloud: None,
enterprise: None,
}),
}
}
}