diff --git a/src/commands/auth.rs b/src/commands/auth.rs index 190e25c..909c594 100644 --- a/src/commands/auth.rs +++ b/src/commands/auth.rs @@ -11,9 +11,7 @@ use oauth2::{ }; use tiny_http::{Response, Server}; -use crate::config::user::Token; - -use super::CommandBase; +use crate::config::user::{Token, UserConfig}; #[derive(Parser, Debug)] #[command( @@ -30,10 +28,10 @@ pub struct Auth { } impl Auth { - pub fn execute(self, base: CommandBase) -> Result<()> { + pub fn execute(self) -> Result<()> { match self.command { - Some(Commands::Login(login)) => login.execute(base), - Some(Commands::Docker(docker)) => docker.execute(base), + Some(Commands::Login(login)) => login.execute(), + Some(Commands::Docker(docker)) => docker.execute(), None => Ok(()), } } @@ -52,12 +50,12 @@ pub enum Commands { pub struct Login {} impl Login { - pub fn execute(self, mut base: CommandBase) -> Result<()> { + pub fn execute(self) -> Result<()> { let server = Server::http("localhost:0").unwrap(); let local_port = server.server_addr().to_ip().unwrap().port(); let redirect_uri = format!("http://localhost:{}/oauth2/callback", local_port); - let url = base.user_config().get_url(); + let url = UserConfig::get_url(); let client = BasicClient::new( ClientId::new("124a489e-93f7-4dd6-abae-1ed4c692bdc7".to_string()), None, @@ -104,7 +102,7 @@ impl Login { token.expiry = Some(Utc::now() + chrono::Duration::hours(1)); } - base.user_config_mut().write_token(token)?; + UserConfig::set_token(token)?; request.respond(Response::from_string("Success! You can close this tab now"))?; @@ -116,12 +114,11 @@ impl Login { pub struct Docker {} impl Docker { - pub fn execute(self, base: CommandBase) -> Result<()> { - let token = base.user_config.get_token().ok_or_else(|| { - anyhow!("Could not get Molnett token. Please run molnctl auth login.") - })?; + pub fn execute(self) -> Result<()> { + let token = UserConfig::get_token() + .ok_or_else(|| anyhow!("Could not get Molnett token. Please run molnctl auth login."))?; - if base.user_config.is_token_expired() { + if UserConfig::is_token_expired() { println!("Token expired. Please run molnctl auth login."); return Ok(()); } diff --git a/src/commands/environments.rs b/src/commands/environments.rs index 1798a37..062d8c5 100644 --- a/src/commands/environments.rs +++ b/src/commands/environments.rs @@ -1,5 +1,5 @@ use super::CommandBase; -use anyhow::{anyhow, Result}; +use anyhow::Result; use clap::{Parser, Subcommand}; use dialoguer::FuzzySelect; use tabled::Table; @@ -54,13 +54,10 @@ pub struct Create { impl Create { pub fn execute(self, base: CommandBase) -> Result<()> { let org_name = base.get_org()?; - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; let response = base.api_client().create_environment( - token, + &token, &self.name, &org_name, self.copy_from.as_deref(), @@ -79,12 +76,9 @@ pub struct List {} impl List { pub fn execute(self, base: CommandBase) -> Result<()> { let org_name = base.get_org()?; - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; - let response = base.api_client().get_environments(token, &org_name)?; + let response = base.api_client().get_environments(&token, &org_name)?; let table = Table::new(response.environments).to_string(); println!("{}", table); @@ -104,10 +98,7 @@ pub struct Delete { impl Delete { pub fn execute(self, base: CommandBase) -> Result<()> { let org_name = base.get_org()?; - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; if !self.confirm_deletion(&org_name)? { println!("Delete cancelled"); @@ -115,7 +106,7 @@ impl Delete { } base.api_client() - .delete_environment(token, &org_name, &self.name)?; + .delete_environment(&token, &org_name, &self.name)?; println!("Environment {} deleted", self.name); Ok(()) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index fd1103b..41d0eae 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -10,42 +10,34 @@ pub mod environments; pub mod orgs; pub mod secrets; pub mod services; +pub mod projects; -pub struct CommandBase<'a> { - user_config: &'a mut UserConfig, +pub struct CommandBase { org_arg: Option, } -impl CommandBase<'_> { - pub fn new(user_config: &mut UserConfig, org_arg: Option) -> CommandBase { +impl CommandBase { + pub fn new(org_arg: Option) -> CommandBase { CommandBase { - user_config, org_arg, } } pub fn api_client(&self) -> APIClient { - let url = self.user_config.get_url(); - APIClient::new(url) + APIClient::new(&UserConfig::get_url()) } - pub fn user_config(&self) -> &UserConfig { - self.user_config - } - - pub fn user_config_mut(&mut self) -> &mut UserConfig { - self.user_config + pub fn get_token(&self) -> Result { + UserConfig::get_token() + .ok_or_else(|| anyhow!("No token found. Please login first.")) } pub fn get_org(&self) -> Result { - let org_name = if self.org_arg.is_some() { - self.org_arg.clone().unwrap() - } else { - match self.user_config.get_default_org() { - Some(cfg) => cfg.to_string(), - None => return Err(anyhow!("Either set a default org in the config or provide one via --org")) - } - }; - Ok(org_name) + if let Some(org) = &self.org_arg { + return Ok(org.clone()); + } + + UserConfig::get_default_org() + .ok_or_else(|| anyhow!("Either set a default org in the config or provide one via --org")) } } diff --git a/src/commands/orgs.rs b/src/commands/orgs.rs index 89d9891..490df8c 100644 --- a/src/commands/orgs.rs +++ b/src/commands/orgs.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; use dialoguer::{FuzzySelect, Input}; use tabled::Table; +use crate::config::user::UserConfig; use super::CommandBase; #[derive(Parser, Debug)] @@ -44,12 +45,8 @@ pub struct List {} impl List { pub fn execute(self, base: CommandBase) -> Result<()> { - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; - - let response = base.api_client().get_organizations(token)?; + let token = base.get_token()?; + let response = base.api_client().get_organizations(&token)?; let table = Table::new(response.organizations).to_string(); println!("{}", table); @@ -69,18 +66,14 @@ pub struct Create { impl Create { pub fn execute(self, base: CommandBase) -> Result<()> { - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; - + let token = base.get_token()?; let plan = CreatePlan::builder() .name(self.name.as_deref()) .billing_email(self.billing_email.as_deref()) .build()?; let response = base.api_client().create_organization( - token, + &token, plan.name.as_str(), plan.billing_email.as_str(), )?; @@ -169,10 +162,11 @@ pub struct Switch { } impl Switch { - pub fn execute(self, mut base: CommandBase) -> Result<()> { + pub fn execute(self, base: CommandBase) -> Result<()> { + let token = base.get_token()?; let orgs = base .api_client() - .get_organizations(base.user_config().get_token().unwrap())?; + .get_organizations(&token)?; let org_names = orgs .organizations .iter() @@ -198,7 +192,7 @@ impl Switch { org_names[selection].to_string() }; - match base.user_config_mut().write_default_org(org_name) { + match UserConfig::set_default_org(org_name) { Ok(_) => Ok(()), Err(err) => { println!("Error while writing config: {}", err); diff --git a/src/commands/projects.rs b/src/commands/projects.rs new file mode 100644 index 0000000..b96264c --- /dev/null +++ b/src/commands/projects.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; +use crate::config::user::UserConfig; + +// This just serves as an example for now +// of how to create a command that is hidden from the help menu +// based on the user's permissions. + +pub fn is_hidden() -> bool { + !UserConfig::get_permissions().contains(&"tenant:manage".to_string()) +} + +#[derive(Debug, Parser)] +#[command( + author, + version, + about, + long_about, + subcommand_required = true, + arg_required_else_help = true, + hide = is_hidden(), +)] +pub struct Projects { + #[command(subcommand)] + pub command: Option, +} + +impl Projects { + pub fn execute(self) -> Result<()> { + match self.command { + None => Ok(()), + } + } +} + +#[derive(Debug, Subcommand)] +pub enum Commands { +} diff --git a/src/commands/secrets.rs b/src/commands/secrets.rs index 4ea56f0..cec976d 100644 --- a/src/commands/secrets.rs +++ b/src/commands/secrets.rs @@ -1,5 +1,5 @@ use super::CommandBase; -use anyhow::{anyhow, Result}; +use anyhow::Result; use clap::{Parser, Subcommand}; use dialoguer::{FuzzySelect, Input}; use std::io::{self, BufRead}; @@ -54,10 +54,7 @@ pub struct Create { impl Create { pub fn execute(self, base: CommandBase) -> Result<()> { let org_name = base.get_org()?; - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; let value: String = if let Some(true) = self.stdin { self.read_stdin()? @@ -69,7 +66,7 @@ impl Create { }; base.api_client() - .create_secret(token, &org_name, &self.env, &self.name, &value)?; + .create_secret(&token, &org_name, &self.env, &self.name, &value)?; println!("Secret {} created", &self.name); Ok(()) @@ -106,12 +103,9 @@ pub struct List { impl List { pub fn execute(self, base: CommandBase) -> Result<()> { let org_name = base.get_org()?; - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; - let response = base.api_client().get_secrets(token, &org_name, &self.env)?; + let response = base.api_client().get_secrets(&token, &org_name, &self.env)?; let table = Table::new(response.secrets).to_string(); println!("{}", table); @@ -133,10 +127,7 @@ pub struct Delete { impl Delete { pub fn execute(self, base: CommandBase) -> Result<()> { let org_name = base.get_org()?; - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; if let Some(false) = self.no_confirm { let prompt = format!("Org: {}, Environment: {}, Secret: {}. Are you sure you want to delete this secret?", org_name, self.env, self.name); @@ -149,7 +140,7 @@ impl Delete { } base.api_client() - .delete_secret(token, &org_name, &self.env, &self.name)?; + .delete_secret(&token, &org_name, &self.env, &self.name)?; println!("Secret {} deleted", self.name); Ok(()) diff --git a/src/commands/services.rs b/src/commands/services.rs index 49d2c17..9a05b4a 100644 --- a/src/commands/services.rs +++ b/src/commands/services.rs @@ -17,9 +17,14 @@ use tungstenite::ClientRequestBuilder; use crate::api::types::{ComposeService, DisplayHashMap, DisplayOption, DisplayVec, Port, Service}; use crate::api::APIClient; +use crate::config::user::UserConfig; use difference::{Changeset, Difference}; use term; +fn is_hidden() -> bool { + UserConfig::get_permissions().contains(&"superadmin".to_string()) +} + #[derive(Debug, Parser)] #[command( author, @@ -28,7 +33,8 @@ use term; long_about, subcommand_required = true, arg_required_else_help = true, - visible_alias = "svcs" + visible_alias = "svcs", + hide = is_hidden(), )] pub struct Services { #[command(subcommand)] @@ -64,10 +70,10 @@ pub enum Commands { #[derive(Debug, Parser)] pub struct Deploy { - #[arg(long, help = "Environment to deploy to (new compose format only)")] - env: Option, + #[arg(long, help = "Environment to deploy to")] + env: String, #[arg( - help = "Path to compose or manifest file", + help = "Path to compose file", default_value("./molnett.yaml") )] manifest: String, @@ -75,38 +81,27 @@ pub struct Deploy { no_confirm: Option, } -#[derive(Deserialize, Debug, Serialize)] -pub struct Manifest { - environment: String, - service: Service, +#[derive(Debug, Serialize, Deserialize, PartialEq)] +struct ComposeFile { + version: u16, + services: Vec, } impl Deploy { pub fn execute(self, base: CommandBase) -> Result<()> { let org_name = base.get_org()?; - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; let compose = read_manifest(&self.manifest)?; - // TODO: remove this once we remove the old manifest format - let environment = if let Some(env) = &self.env { - env.clone() - } else if let Ok(manifest) = - serde_yaml::from_str::(&std::fs::read_to_string(&self.manifest)?) - { - manifest.environment - } else { - return Err(anyhow!("No environment specified")); - }; + let environment = &self.env; let env_exists = base .api_client() - .get_environments(token, &org_name)? + .get_environments(&token, &org_name)? .environments .iter() - .any(|env| env.name == environment); + .any(|env| env.name == *environment); + if !env_exists { return Err(anyhow!("Environment {} does not exist", environment)); } @@ -135,7 +130,7 @@ impl Deploy { let response = base.api_client() - .get_service(token, &org_name, &environment, &service.name); + .get_service(&token, &org_name, &environment, &service.name); if let Some(false) = self.no_confirm { let existing_svc_yaml = match response? { @@ -172,7 +167,7 @@ impl Deploy { let result = base.api_client() - .deploy_service(token, &org_name, &environment, service)?; + .deploy_service(&token, &org_name, &environment, service)?; println!("Service {} deployed: {:?}", service_name, result); } @@ -260,12 +255,9 @@ impl Initialize { } } - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; - let compose = ComposeBuilder::new(token.to_string(), base.api_client(), base.get_org()?) + let compose = ComposeBuilder::new(token, base.api_client(), base.get_org()?) .add_services()? .build(); @@ -276,12 +268,6 @@ impl Initialize { } } -#[derive(Debug, Serialize, Deserialize, PartialEq)] -struct ComposeFile { - version: u16, - services: Vec, -} - #[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] enum Value { @@ -488,14 +474,11 @@ pub struct ImageName { impl ImageName { pub fn execute(self, base: CommandBase) -> Result<()> { - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; let image_name = get_image_name( &base.api_client(), - token, + &token, &base.get_org()?, &self.image_name, )?; @@ -503,29 +486,19 @@ impl ImageName { let full_image = format!("{}:{}", image_name, image_tag); if let Some(path) = self.update_manifest.clone() { - match &self.service { - Some(service_name) => { - // Handle compose file format - let mut compose = read_manifest(&path)?; - let service = compose - .services - .iter_mut() - .find(|s| s.name == *service_name) - .ok_or_else(|| { - anyhow!("Service {} not found in compose file", service_name) - })?; - service.image = full_image.clone(); - write_manifest(&path, &compose)?; - } - None => { - // Handle old manifest format - let mut manifest: Manifest = - serde_yaml::from_str(&std::fs::read_to_string(&path)?)?; - manifest.service.image = full_image.clone(); - let mut file = File::create(&path)?; - serde_yaml::to_writer(&mut file, &manifest)?; - } - } + let service_name = self.service.as_ref().ok_or_else(|| { + anyhow!("Service name is required when updating a compose file") + })?; + let mut compose = read_manifest(&path)?; + let service = compose + .services + .iter_mut() + .find(|s| s.name == *service_name) + .ok_or_else(|| { + anyhow!("Service {} not found in compose file", service_name) + })?; + service.image = full_image.clone(); + write_manifest(&path, &compose)?; } println!("{}", full_image); @@ -542,14 +515,11 @@ pub struct List { impl List { pub fn execute(self, base: CommandBase) -> Result<()> { let org_name = base.get_org()?; - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; let response = base .api_client() - .get_services(token, &org_name, &self.env)?; + .get_services(&token, &org_name, &self.env)?; let table = Table::new(response.services).to_string(); println!("{}", table); @@ -571,10 +541,7 @@ pub struct Delete { impl Delete { pub fn execute(self, base: CommandBase) -> Result<()> { let org_name = base.get_org()?; - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; if let Some(false) = self.no_confirm { let prompt = format!("Org: {}, Environment: {}, Service: {}. Are you sure you want to delete this service?", org_name, self.env, self.name); @@ -587,7 +554,7 @@ impl Delete { } base.api_client() - .delete_service(token, &org_name, &self.env, &self.name)?; + .delete_service(&token, &org_name, &self.env, &self.name)?; println!("Service {} deleted", self.name); Ok(()) @@ -608,10 +575,7 @@ pub struct Logs { impl Logs { pub fn execute(self, base: CommandBase) -> Result<()> { let org_name = base.get_org()?; - let token = base - .user_config() - .get_token() - .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let token = base.get_token()?; let compose = read_manifest(&self.manifest)?; let service = compose @@ -622,7 +586,7 @@ impl Logs { let logurl: Uri = url::Url::parse( format!( "{}/orgs/{}/envs/{}/svcs/{}/logs", - base.user_config().get_url().replace("http", "ws"), + UserConfig::get_url().replace("http", "ws"), org_name, self.environment, service.name, @@ -649,41 +613,7 @@ impl Logs { fn read_manifest(path: &str) -> Result { let mut file_content = String::new(); File::open(&path)?.read_to_string(&mut file_content)?; - - // Try to parse as new compose format first - match serde_yaml::from_str::(&file_content) { - Ok(compose) => return Ok(compose), - Err(e) => println!("Failed to parse as compose file: {}", e), - } - - println!("Trying old manifest format"); - // If not a compose file, try to parse as our old manifest format - let manifest: Manifest = serde_yaml::from_str(&file_content) - .map_err(|e| anyhow!("Failed to parse manifest: {}", e))?; - - // Convert old manifest to new compose format - let service = ComposeService { - name: manifest.service.name, - image: manifest.service.image, - ports: vec![Port { - target: manifest.service.container_port, - published: None, - }], - environment: DisplayOption(manifest.service.env.0.map(|env_map| { - let mut environment = IndexMap::new(); - for (k, v) in env_map.0 { - environment.insert(k, v.clone()); - } - DisplayHashMap(environment) - })), - secrets: manifest.service.secrets.clone(), - command: manifest.service.command.clone(), - }; - - Ok(ComposeFile { - version: 2, - services: vec![service], - }) + serde_yaml::from_str(&file_content).map_err(|e| anyhow!("Failed to parse compose file: {}", e)) } fn write_manifest(path: &str, compose: &ComposeFile) -> Result<()> { diff --git a/src/config/user.rs b/src/config/user.rs index 1d82403..353ca79 100644 --- a/src/config/user.rs +++ b/src/config/user.rs @@ -1,14 +1,19 @@ use camino::Utf8PathBuf; use config::Config; - +use once_cell::unsync::OnceCell; +use anyhow::{Result, anyhow}; +use std::cell::RefCell; use crate::Cli; - use super::{default_user_config_path, write_to_disk_json, Error}; +thread_local! { + static CONFIG: OnceCell> = OnceCell::new(); +} + #[derive(Debug, Clone)] pub struct UserConfig { - config: UserConfigInner, + inner: UserConfigInner, disk_config: UserConfigInner, path: Utf8PathBuf, } @@ -19,12 +24,18 @@ pub struct UserConfigInner { default_org: Option, #[serde(default = "default_url")] url: String, + #[serde(default = "default_permissions")] + permissions: Vec, } fn default_url() -> String { "https://api.molnett.org".to_string() } +fn default_permissions() -> Vec { + vec!["superadmin".to_string()] +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] pub struct Token { pub access_token: String, @@ -46,53 +57,79 @@ pub struct UserConfigLoader { } impl UserConfig { - pub fn new(cli: &Cli) -> Self { - let config_path = match &cli.config { - Some(path) => path.clone(), - None => default_user_config_path() - .expect("No config path provided and default path not found"), - }; - let mut config = - UserConfigLoader::load(&config_path).expect("Loading config from disk failed"); - - // TODO: write config to disk after reading so it gets written if it doesn't exist - - if let Some(h) = &cli.url { - config.set_url(h.to_string()); - } + pub fn load_from_disk() -> Result<()> { + let config_path = default_user_config_path()?; + let config = UserConfigLoader::load(&config_path)?; + CONFIG.with(|cell| cell.set(RefCell::new(config))) + .map_err(|_| anyhow!("UserConfig has already been initialized")) + } + + pub fn apply_cli_options(cli: &Cli) { + CONFIG.with(|cell| { + let mut config = cell.get().expect("UserConfig has not been initialized").borrow_mut(); + + if let Some(url) = &cli.url { + config.inner.url = url.to_string(); + } - config + config.inner.permissions = vec!["superadmin".to_string()]; + }); } - pub fn get_token(&self) -> Option<&str> { - self.config.token.as_ref().map(|u| u.access_token.as_str()) + + pub fn get_token() -> Option { + CONFIG.with(|cell| { + let config = cell.get().expect("UserConfig has not been initialized").borrow(); + config.inner.token.as_ref().map(|u| u.access_token.as_str()).map(|t| t.to_string()) + }) } - pub fn is_token_expired(&self) -> bool { - if let Some(token) = &self.config.token { - if let Some(expiry) = token.expiry { - return expiry < chrono::Utc::now(); - } - } - true + + pub fn get_url() -> String { + CONFIG.with(|cell| { + let config = cell.get().expect("UserConfig has not been initialized").borrow(); + config.inner.url.clone() + }) } - pub fn write_token(&mut self, token: Token) -> Result<(), super::Error> { - self.disk_config.token = Some(token.clone()); - self.config.token = Some(token); - write_to_disk_json(&self.path, &self.disk_config) + pub fn get_default_org() -> Option { + CONFIG.with(|cell| { + let config = cell.get().expect("UserConfig has not been initialized").borrow(); + config.inner.default_org.clone() + }) } - pub fn write_default_org(&mut self, org_name: String) -> Result<(), super::Error> { - self.disk_config.default_org = Some(org_name.clone()); - self.config.default_org = Some(org_name); - write_to_disk_json(&self.path, &self.disk_config) + + pub fn get_permissions() -> Vec { + CONFIG.with(|cell| { + let config = cell.get().expect("UserConfig has not been initialized").borrow(); + config.inner.permissions.clone() + }) } - pub fn get_default_org(&self) -> Option<&str> { - self.config.default_org.as_deref() + + pub fn set_token(token: Token) -> Result<(), Error> { + CONFIG.with(|cell| { + let mut config = cell.get().expect("UserConfig has not been initialized").borrow_mut(); + config.disk_config.token = Some(token.clone()); + config.inner.token = Some(token); + write_to_disk_json(&config.path, &config.disk_config) + }) } - pub fn get_url(&self) -> &str { - self.config.url.as_ref() + + pub fn set_default_org(org_name: String) -> Result<(), Error> { + CONFIG.with(|cell| { + let mut config = cell.get().expect("UserConfig has not been initialized").borrow_mut(); + config.disk_config.default_org = Some(org_name.clone()); + config.inner.default_org = Some(org_name); + write_to_disk_json(&config.path, &config.disk_config) + }) } - fn set_url(&mut self, url: String) { - self.config.url = url; + + pub fn is_token_expired() -> bool { + CONFIG.with(|cell| { + let config = cell.get().expect("UserConfig has not been initialized").borrow(); + config.inner.token + .as_ref() + .map(|u| u.expiry.is_some() && u.expiry.unwrap() < chrono::Utc::now()) + .unwrap_or(false) + }) } } @@ -109,7 +146,7 @@ impl UserConfigLoader { let config = Config::builder().add_source(disk_config.clone()).build()?; Ok(UserConfig { - config: config.try_deserialize()?, + inner: config.try_deserialize()?, disk_config: disk_config.try_deserialize()?, path: path.clone(), }) diff --git a/src/main.rs b/src/main.rs index c9bfc0a..e77aa43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ use crate::config::user::UserConfig; use anyhow::{anyhow, Result}; -use camino::Utf8PathBuf; use clap::{Parser, Subcommand}; use commands::CommandBase; use dialoguer::console::style; @@ -22,16 +21,6 @@ mod config; arg_required_else_help = true )] pub struct Cli { - #[arg( - global = true, - short, - long, - value_name = "FILE", - env("MOLNETT_CONFIG"), - help = "config file, default is $HOME/.config/molnett/config.json" - )] - config: Option, - #[arg( global = true, long, @@ -73,9 +62,15 @@ enum Commands { Secrets(commands::secrets::Secrets), /// Deploy and manage services Services(commands::services::Services), + /// Create and manage projects + Projects(commands::projects::Projects), } fn main() -> Result<()> { + // First phase: Load config from disk before parsing CLI args + // so we can use it to determine if we should hide commands + UserConfig::load_from_disk()?; + let cli = Cli::parse(); if !cli.quiet { @@ -86,15 +81,13 @@ fn main() -> Result<()> { } } - if let Some(config_path) = cli.config.as_deref() { - println!("Config path: {}", config_path); - } + // Second phase: Apply CLI options + UserConfig::apply_cli_options(&cli); - let mut config = UserConfig::new(&cli); - let base = CommandBase::new(&mut config, cli.org); + let base = CommandBase::new(cli.org); match cli.command { - Some(Commands::Auth(auth)) => auth.execute(base), + Some(Commands::Auth(auth)) => auth.execute(), Some(Commands::Environments(environments)) => environments.execute(base), Some(Commands::Deploy(deploy)) => deploy.execute(base), Some(Commands::Logs(logs)) => logs.execute(base), @@ -102,6 +95,7 @@ fn main() -> Result<()> { Some(Commands::Orgs(orgs)) => orgs.execute(base), Some(Commands::Secrets(secrets)) => secrets.execute(base), Some(Commands::Services(svcs)) => svcs.execute(base), + Some(Commands::Projects(projects)) => projects.execute(), None => Ok(()), } }