diff --git a/openstack_tui/src/app.rs b/openstack_tui/src/app.rs index 3ed1980f8..fb74e714a 100644 --- a/openstack_tui/src/app.rs +++ b/openstack_tui/src/app.rs @@ -81,6 +81,20 @@ impl App { let client_config = args.os_client_config_file.clone(); let client_secure_config = args.os_client_secure_file.clone(); + // When --cloud-config-from-env is set, build the cloud config from + // environment variables (same behavior as the CLI). + let (env_cloud_name, env_cloud_config) = if args.cloud_config_from_env { + let cloud_name = args + .os_cloud_name + .clone() + .unwrap_or_else(|| String::from("envvars")); + let mut cfg = openstack_sdk::config::CloudConfig::from_env()?; + cfg.name = Some(cloud_name.clone()); + (Some(cloud_name), Some(cfg)) + } else { + (args.os_cloud_name.clone(), None) + }; + // Is there a way to initialize HashMap with Box as keys in one operation? let mut components: HashMap> = HashMap::new(); components.insert(Mode::Home, Box::new(Home::new())); @@ -284,6 +298,7 @@ impl App { client_config, client_secure_config, auth_helper_control_channel_tx_clone, + env_cloud_config, )?; tokio::spawn(async move { if let Err(err) = cloud @@ -307,7 +322,7 @@ impl App { action_rx, cloud_worker_tx: cloud_worker, last_tick_key_events: Vec::new(), - cloud_name: args.os_cloud.clone(), + cloud_name: args.os_cloud.clone().or(env_cloud_name), cloud_connected: false, active_popup: None, popups, diff --git a/openstack_tui/src/cli.rs b/openstack_tui/src/cli.rs index 9092bc56b..cc5fe00ea 100644 --- a/openstack_tui/src/cli.rs +++ b/openstack_tui/src/cli.rs @@ -31,9 +31,24 @@ pub struct Cli { pub frame_rate: f64, /// Cloud name to connect to after the start - #[arg(long, env = "OS_CLOUD")] + /// + /// Conflicts with the `--cloud-config-from-env` option. + #[arg(long, env = "OS_CLOUD", conflicts_with = "cloud_config_from_env")] pub os_cloud: Option, + /// Get the cloud config from environment variables. + /// + /// Conflicts with the `--os-cloud` option. No merging of environment variables with the + /// options from the `clouds.yaml` file done. This effectively limits the TUI to a single + /// cloud connection. + #[arg(long)] + pub cloud_config_from_env: bool, + + /// Cloud name used when configuration is retrieved from environment variables. When not + /// specified the `envvars` would be used as a default. + #[arg(long, env = "OS_CLOUD_NAME")] + pub os_cloud_name: Option, + /// Custom path to the `clouds.yaml` config file #[arg(long, env = "OS_CLIENT_CONFIG_FILE", value_hint = ValueHint::FilePath)] pub os_client_config_file: Option, diff --git a/openstack_tui/src/cloud_worker.rs b/openstack_tui/src/cloud_worker.rs index 0c4ebcaa1..04cb67e75 100644 --- a/openstack_tui/src/cloud_worker.rs +++ b/openstack_tui/src/cloud_worker.rs @@ -22,7 +22,7 @@ use eyre::{Result, eyre}; use openstack_sdk::{ AsyncOpenStack, MicroVersionStrategy, RenewHandle, auth::auth_helper::{AuthHelper, AuthHelperError}, - config::ConfigFile, + config::{CloudConfig, ConfigFile}, }; use secrecy::SecretString; use std::path::PathBuf; @@ -68,6 +68,10 @@ const AUTO_RENEW_MARGIN: TimeDelta = TimeDelta::seconds(59 * 60); pub(crate) struct Cloud { cloud_configs: ConfigFile, pub(crate) cloud: Option, + /// Pre-loaded cloud config from environment variables. When set, the + /// TUI operates in single-cloud mode similar to `--cloud-config-from-env` + /// in the CLI. + env_cloud_config: Option, auth_helper: TuiAuthHelper, /// Handle to the background token-renewal task for `cloud`. Dropped /// (stopping the task) when replaced by a new connection/scope change, @@ -87,15 +91,29 @@ impl Cloud { client_config_config_file: Option, client_secure_config_file: Option, auth_helper_control_tx: mpsc::Sender>, + env_cloud_config: Option, ) -> Result { - let cfg = ConfigFile::new_with_user_specified_configs( - client_config_config_file.as_deref(), - client_secure_config_file.as_deref(), - )?; + // When running with env-based config the config file is not needed + // and parsing it may fail for unrelated reasons. Skip the parse in + // that case so that a broken or missing `clouds.yaml` does not block + // `--cloud-config-from-env` operation. + let cfg = if env_cloud_config.is_some() { + ConfigFile { + cache: None, + clouds: None, + public_clouds: None, + } + } else { + ConfigFile::new_with_user_specified_configs( + client_config_config_file.as_deref(), + client_secure_config_file.as_deref(), + )? + }; Ok(Self { cloud_configs: cfg, cloud: None, + env_cloud_config, auth_helper: TuiAuthHelper::new(auth_helper_control_tx), _renew_handle: None, }) @@ -103,10 +121,13 @@ impl Cloud { pub async fn connect_to_cloud(&mut self, cloud: String) -> Result<()> { debug!("Connecting to cloud {}", cloud); - let profile = self - .cloud_configs - .get_cloud_config(cloud.clone())? - .ok_or_else(|| eyre!("Cloud `{}` is not present in configuration files", cloud))?; + let profile = if let Some(ref env_config) = self.env_cloud_config { + env_config.clone() + } else { + self.cloud_configs + .get_cloud_config(cloud.clone())? + .ok_or_else(|| eyre!("Cloud `{}` is not present in configuration files", cloud))? + }; let session = AsyncOpenStack::builder(&profile) .auth_helper(self.auth_helper.clone()) .renew_auth(false) @@ -264,7 +285,15 @@ impl Cloud { } } Action::ListClouds => { - app_tx.send(Action::Clouds(self.cloud_configs.get_available_clouds()))?; + if let Some(ref env_config) = self.env_cloud_config { + let cloud_name = env_config + .name + .clone() + .unwrap_or_else(|| String::from("envvars")); + app_tx.send(Action::Clouds(vec![cloud_name]))?; + } else { + app_tx.send(Action::Clouds(self.cloud_configs.get_available_clouds()))?; + } } Action::CloudChangeScope(ref scope) => { let _ = self.switch_auth_scope(scope, &app_tx, &action).await;