use super::{
LoggingConfig, azure::ServicebusConfig, keys::KeyBindingsConfig, limits::*, ui::UIConfig,
validation::ConfigValidationError,
};
use crate::constants::env_vars::*;
use crate::theme::types::ThemeConfig;
use crate::utils::auth::{
AUTH_METHOD_CLIENT_SECRET, AUTH_METHOD_CONNECTION_STRING, AUTH_METHOD_DEVICE_CODE, AuthUtils,
};
use quetty_server::bulk_operations::BatchConfig;
use quetty_server::service_bus_manager::AzureAdConfig;
use serde::Deserialize;
use std::time::Duration;
#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
page_size: Option<u32>,
crossterm_input_listener_interval_ms: Option<u64>,
crossterm_input_listener_retries: Option<usize>,
poll_timeout_ms: Option<u64>,
tick_interval_millis: Option<u64>,
queue_stats_display_enabled: Option<bool>,
queue_stats_cache_ttl_seconds: Option<u64>,
queue_stats_use_management_api: Option<bool>,
azure_resource_cache_ttl_seconds: Option<u64>,
azure_resource_cache_max_entries: Option<usize>,
#[serde(flatten, default)]
batch: BatchConfig,
#[serde(flatten, default)]
ui: UIConfig,
#[serde(default)]
keys: KeyBindingsConfig,
#[serde(default)]
servicebus: ServicebusConfig,
#[serde(default)]
azure_ad: AzureAdConfig,
#[serde(default)]
logging: LoggingConfig,
theme: Option<ThemeConfig>,
}
impl AppConfig {
pub fn validate(&self) -> Result<(), Vec<ConfigValidationError>> {
let mut errors = Vec::new();
let page_size = self.page_size();
if page_size < MIN_PAGE_SIZE {
errors.push(ConfigValidationError::PageSize {
configured: page_size,
min_limit: MIN_PAGE_SIZE,
max_limit: MAX_PAGE_SIZE,
});
}
if page_size > MAX_PAGE_SIZE {
errors.push(ConfigValidationError::PageSize {
configured: page_size,
min_limit: MIN_PAGE_SIZE,
max_limit: MAX_PAGE_SIZE,
});
}
if self.batch.max_batch_size() > AZURE_SERVICE_BUS_MAX_BATCH_SIZE {
errors.push(ConfigValidationError::BatchSize {
configured: self.batch.max_batch_size(),
limit: AZURE_SERVICE_BUS_MAX_BATCH_SIZE,
});
}
if self.batch.operation_timeout_secs() > MAX_OPERATION_TIMEOUT_SECS {
errors.push(ConfigValidationError::OperationTimeout {
configured: self.batch.operation_timeout_secs(),
limit: MAX_OPERATION_TIMEOUT_SECS,
});
}
if self.batch.bulk_chunk_size() > MAX_BULK_CHUNK_SIZE {
errors.push(ConfigValidationError::BulkChunkSize {
configured: self.batch.bulk_chunk_size(),
limit: MAX_BULK_CHUNK_SIZE,
});
}
if self.batch.bulk_processing_time_secs() > MAX_BULK_PROCESSING_TIME_SECS {
errors.push(ConfigValidationError::BulkProcessingTime {
configured: self.batch.bulk_processing_time_secs(),
limit: MAX_BULK_PROCESSING_TIME_SECS,
});
}
if self.batch.lock_timeout_secs() > MAX_LOCK_TIMEOUT_SECS {
errors.push(ConfigValidationError::LockTimeout {
configured: self.batch.lock_timeout_secs(),
limit: MAX_LOCK_TIMEOUT_SECS,
});
}
if self.batch.max_messages_to_process() > MAX_MESSAGES_TO_PROCESS_LIMIT {
errors.push(ConfigValidationError::MaxMessagesToProcess {
configured: self.batch.max_messages_to_process(),
limit: MAX_MESSAGES_TO_PROCESS_LIMIT,
});
}
let ttl = self.queue_stats_cache_ttl_seconds();
if !(MIN_QUEUE_STATS_CACHE_TTL_SECONDS..=MAX_QUEUE_STATS_CACHE_TTL_SECONDS).contains(&ttl) {
errors.push(ConfigValidationError::QueueStatsCacheTtl {
configured: ttl,
min_limit: MIN_QUEUE_STATS_CACHE_TTL_SECONDS,
max_limit: MAX_QUEUE_STATS_CACHE_TTL_SECONDS,
});
}
self.validate_auth_config(&mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn page_size(&self) -> u32 {
self.page_size.unwrap_or(100)
}
pub fn max_messages(&self) -> u32 {
self.page_size()
}
pub fn crossterm_input_listener_interval(&self) -> Duration {
Duration::from_millis(self.crossterm_input_listener_interval_ms.unwrap_or(10))
}
pub fn crossterm_input_listener_retries(&self) -> usize {
self.crossterm_input_listener_retries.unwrap_or(10)
}
pub fn poll_timeout(&self) -> Duration {
Duration::from_millis(self.poll_timeout_ms.unwrap_or(50))
}
pub fn tick_interval(&self) -> Duration {
Duration::from_millis(self.tick_interval_millis.unwrap_or(50))
}
pub fn queue_stats_display_enabled(&self) -> bool {
self.queue_stats_display_enabled.unwrap_or(true)
}
pub fn queue_stats_cache_ttl_seconds(&self) -> u64 {
self.queue_stats_cache_ttl_seconds.unwrap_or(60)
}
pub fn queue_stats_use_management_api(&self) -> bool {
self.queue_stats_use_management_api.unwrap_or(true)
}
pub fn azure_resource_cache_ttl_seconds(&self) -> u64 {
self.azure_resource_cache_ttl_seconds.unwrap_or(300) }
pub fn azure_resource_cache_max_entries(&self) -> usize {
self.azure_resource_cache_max_entries.unwrap_or(100) }
pub fn batch(&self) -> &BatchConfig {
&self.batch
}
pub fn ui(&self) -> &UIConfig {
&self.ui
}
pub fn keys(&self) -> &KeyBindingsConfig {
&self.keys
}
pub fn servicebus(&self) -> &ServicebusConfig {
&self.servicebus
}
pub fn azure_ad(&self) -> &AzureAdConfig {
&self.azure_ad
}
pub fn logging(&self) -> &LoggingConfig {
&self.logging
}
pub fn theme(&self) -> ThemeConfig {
self.theme.clone().unwrap_or_default()
}
fn validate_auth_config(&self, errors: &mut Vec<ConfigValidationError>) {
let auth_method = &self.azure_ad.auth_method;
match auth_method.as_str() {
AUTH_METHOD_CONNECTION_STRING => {
if !self.servicebus.has_connection_string() {
errors.push(ConfigValidationError::ConflictingAuthConfig {
message: "Authentication method is set to 'connection_string' but no encrypted Service Bus connection string is provided.\n\n\
Please either:\n\
1. Add servicebus.encrypted_connection_string and servicebus.encryption_salt to your config.toml\n\
2. Set SERVICEBUS__ENCRYPTED_CONNECTION_STRING and SERVICEBUS__ENCRYPTION_SALT environment variables\n\
3. Change azure_ad.auth_method to 'device_code' for Azure AD authentication".to_string()
});
}
}
AUTH_METHOD_DEVICE_CODE | AUTH_METHOD_CLIENT_SECRET => {
self.validate_azure_ad_config(errors);
}
method => {
errors.push(ConfigValidationError::InvalidAuthMethod {
method: method.to_string(),
});
}
}
}
fn validate_azure_ad_config(&self, errors: &mut Vec<ConfigValidationError>) {
let auth_method = &self.azure_ad.auth_method;
match auth_method.as_str() {
AUTH_METHOD_DEVICE_CODE => self.validate_device_code_config(errors),
AUTH_METHOD_CLIENT_SECRET => self.validate_client_secret_config(errors),
_ => {
errors.push(ConfigValidationError::InvalidAzureAdFlow {
flow: auth_method.clone(),
});
}
}
}
fn validate_device_code_config(&self, errors: &mut Vec<ConfigValidationError>) {
if !self.azure_ad.has_tenant_id() && std::env::var(AZURE_AD_TENANT_ID).is_err() {
errors.push(ConfigValidationError::MissingAzureAdField {
field: "tenant_id".to_string(),
});
}
if !self.azure_ad.has_client_id() && std::env::var(AZURE_AD_CLIENT_ID).is_err() {
errors.push(ConfigValidationError::MissingAzureAdField {
field: "client_id".to_string(),
});
}
if self.queue_stats_use_management_api() {
log::debug!(
"Device code flow with management API - optional fields can be discovered interactively"
);
}
}
fn validate_client_secret_config(&self, errors: &mut Vec<ConfigValidationError>) {
if !self.azure_ad.has_tenant_id() && std::env::var(AZURE_AD_TENANT_ID).is_err() {
errors.push(ConfigValidationError::MissingAzureAdField {
field: "tenant_id".to_string(),
});
}
if !self.azure_ad.has_client_id() && std::env::var(AZURE_AD_CLIENT_ID).is_err() {
errors.push(ConfigValidationError::MissingAzureAdField {
field: "client_id".to_string(),
});
}
if !self.azure_ad.has_client_secret()
&& std::env::var(AZURE_AD_CLIENT_SECRET).is_err()
&& std::env::var(AZURE_AD_ENCRYPTED_CLIENT_SECRET).is_err()
{
errors.push(ConfigValidationError::MissingAzureAdField {
field: "client_secret".to_string(),
});
}
if self.queue_stats_use_management_api() {
log::debug!(
"Client secret flow with management API - optional fields can be discovered interactively"
);
}
}
pub fn has_required_auth_fields(&self) -> bool {
if AuthUtils::is_connection_string_auth(self) {
self.servicebus.has_connection_string()
} else if AuthUtils::is_device_code_auth(self) {
(self.azure_ad.has_tenant_id() || std::env::var(AZURE_AD_TENANT_ID).is_ok())
&& (self.azure_ad.has_client_id() || std::env::var(AZURE_AD_CLIENT_ID).is_ok())
} else if AuthUtils::is_client_secret_auth(self) {
(self.azure_ad.has_tenant_id() || std::env::var(AZURE_AD_TENANT_ID).is_ok())
&& (self.azure_ad.has_client_id() || std::env::var(AZURE_AD_CLIENT_ID).is_ok())
&& (self.azure_ad.has_client_secret()
|| std::env::var(AZURE_AD_CLIENT_SECRET).is_ok()
|| std::env::var(AZURE_AD_ENCRYPTED_CLIENT_SECRET).is_ok())
} else {
false
}
}
pub fn get_encrypted_auth_methods(&self) -> Vec<String> {
let mut methods = Vec::new();
let auth_method = &self.azure_ad().auth_method;
if auth_method == "connection_string"
&& std::env::var(SERVICEBUS_ENCRYPTED_CONNECTION_STRING).is_ok()
&& std::env::var(SERVICEBUS_ENCRYPTION_SALT).is_ok()
{
methods.push("Connection String".to_string());
}
if auth_method == "client_secret"
&& std::env::var(AZURE_AD_ENCRYPTED_CLIENT_SECRET).is_ok()
&& std::env::var(AZURE_AD_CLIENT_SECRET_ENCRYPTION_SALT).is_ok()
{
methods.push("Azure AD Client Secret".to_string());
}
methods
}
}