diff --git a/src/commands/environments.rs b/src/commands/environments.rs index 3d41c0f..36b1a59 100644 --- a/src/commands/environments.rs +++ b/src/commands/environments.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; use tabled::Table; -#[derive(Parser, Debug)] +#[derive(Debug, Parser)] #[command( author, version, @@ -27,7 +27,7 @@ impl Environments { } } -#[derive(Subcommand, Debug)] +#[derive(Debug, Subcommand)] pub enum Commands { /// Create an environment #[command(arg_required_else_help = true)] @@ -37,21 +37,15 @@ pub enum Commands { List(List), } -#[derive(Parser, Debug)] +#[derive(Debug, Parser)] pub struct Create { #[arg(help = "Name of the environment to create")] name: String, - #[arg(long, help = "Organization to create the environment in")] - org: Option, } impl Create { pub fn execute(&self, base: &CommandBase) -> Result<()> { - let org_name = if self.org.is_some() { - self.org.clone().unwrap() - } else { - base.user_config().get_default_org().unwrap().to_string() - }; + let org_name = base.get_org()?; let token = base .user_config() .get_token() @@ -68,19 +62,12 @@ impl Create { } } -#[derive(Parser, Debug)] -pub struct List { - #[arg(long, help = "Organization to list the environments of")] - org: Option, -} +#[derive(Debug, Parser)] +pub struct List {} impl List { pub fn execute(&self, base: &CommandBase) -> Result<()> { - let org_name = if self.org.is_some() { - self.org.clone().unwrap() - } else { - base.user_config().get_default_org().unwrap().to_string() - }; + let org_name = base.get_org()?; let token = base .user_config() .get_token() diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 3992f14..12db80e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use once_cell::sync::OnceCell; use crate::{ @@ -19,13 +19,15 @@ pub mod services; pub struct CommandBase<'a> { user_config: &'a mut UserConfig, app_config: OnceCell, + org_arg: Option, } impl CommandBase<'_> { - pub fn new(user_config: &mut UserConfig) -> CommandBase { + pub fn new(user_config: &mut UserConfig, org_arg: Option) -> CommandBase { CommandBase { user_config, app_config: OnceCell::new(), + org_arg, } } @@ -54,4 +56,16 @@ impl CommandBase<'_> { self.app_config()?; Ok(self.app_config.get_mut().ok_or(Error::UserConfigNotInit)?) } + + 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) + } } diff --git a/src/commands/services.rs b/src/commands/services.rs index 4529053..78dead8 100644 --- a/src/commands/services.rs +++ b/src/commands/services.rs @@ -2,6 +2,9 @@ use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; use dialoguer::{FuzzySelect, Input}; use difference::{Difference, Changeset}; +use serde::Deserialize; +use std::fs::File; +use std::io::Read; use std::io::Write; use std::path::Path; use super::CommandBase; @@ -12,8 +15,7 @@ use crate::{config::{ scan::{scan_directory_for_type, ApplicationType}, }, api::types::Service}; -#[derive(Parser)] -#[derive(Debug)] +#[derive(Debug, Parser)] #[command( author, version, @@ -38,8 +40,7 @@ impl Services { } } -#[derive(Subcommand)] -#[derive(Debug)] +#[derive(Debug, Subcommand)] pub enum Commands { /// Deploy a service #[command(arg_required_else_help = true)] @@ -50,105 +51,112 @@ pub enum Commands { List(List) } -#[derive(Parser)] -#[derive(Debug)] +#[derive(Debug, Parser)] pub struct Deploy { - #[arg(help = "Name of the app to deploy")] - name: String, - #[arg(short, long, help = "Environment to deploy to")] - env: String, - #[arg(short, long, help = "The image to deploy, e.g. yourimage:v1")] - image: Option, + #[arg(help = "Path to molnett manifest")] + manifest: String, #[arg(long, help = "Skip confirmation", default_missing_value("true"), default_value("false"), num_args(0..=1), require_equals(true))] no_confirm: Option, - #[arg(short, long, help = "Port the application listens on")] - port: Option, - #[arg(long, help = "Organization to deploy to")] - org: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Manifest { + environment: String, + service: Service } impl Deploy { pub fn execute(&self, base: &CommandBase) -> Result<()> { - let org_name = if self.org.is_some() { - self.org.clone().unwrap() - } else { - base.user_config().get_default_org().unwrap().to_string() - }; + let org_name = base.get_org()?; let token = base .user_config() .get_token() .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let manifest = self.read_manifest()?; + + let env_exists = base.api_client().get_environments(token, &org_name)?.contains(&manifest.environment); + if !env_exists { + return Err(anyhow!("Environment {} does not exist", manifest.environment)) + } + let response = base.api_client().get_service( token, &org_name, - &self.env, - &self.name + &manifest.environment, + &manifest.service.name ); - let existing_svc: Option; - if let Err(e) = response { - existing_svc = None; - if let Some(reqwest::StatusCode::NOT_FOUND) = e.status() { - // User needs to set every attribute if service does not exist yet - if self.image.is_none() || self.port.is_none() { - return Err(anyhow!("Image and port are mandatory if service does not exist")) + let existing_svc = match response { + Ok(svc) => svc, + Err(e) => match e.status() { + Some(reqwest::StatusCode::NOT_FOUND) => { + return self.create_new_service(base, token, &org_name) + }, + Some(reqwest::StatusCode::UNAUTHORIZED) => { + return Err(anyhow!("Unauthorized, please login first")) + }, + _ => { + return Err(anyhow!("Could not check whether service exists or not")) } - } else if let Some(reqwest::StatusCode::UNAUTHORIZED) = e.status() { - return Err(anyhow!("Unauthorized, please login first")) - } else { - return Err(anyhow!("Could not check whether service exists or not")) - } - } else { - existing_svc = Some(response.unwrap()); - } - - let mut new_svc: Service; - if existing_svc.is_some() { - new_svc = existing_svc.clone().unwrap(); - if let Some(image) = &self.image { - new_svc.image = image.to_string() - } - if let Some(port) = self.port { - new_svc.container_port = port - } - } else { - new_svc = Service { - name: self.name.clone(), - image: self.image.clone().unwrap(), - container_port: self.port.clone().unwrap() } }; if let Some(false) = self.no_confirm { - if existing_svc.is_some() && existing_svc.clone().unwrap() == new_svc { + if existing_svc == manifest.service { println!("no changes detected"); return Ok(()) } - let existing_svc_yaml = if existing_svc.is_some() { - serde_yaml::to_string(&existing_svc)? - } else { - "".to_string() - }; - let new_svc_yaml = serde_yaml::to_string(&new_svc)?; + let existing_svc_yaml = serde_yaml::to_string(&existing_svc)?; + let new_svc_yaml = serde_yaml::to_string(&manifest.service)?; self.render_diff(existing_svc_yaml, new_svc_yaml)?; - let selection = FuzzySelect::with_theme(&dialoguer::theme::ColorfulTheme::default()) - .with_prompt("Do you want to apply the above changes?") - .items(&["no", "yes"]) - .default(0) - .interact() - .unwrap(); + let selection = self.user_confirmation(); if selection == 0 { println!("Cancelling..."); return Ok(()) } } - let result = base.api_client().deploy_service(token, &org_name, &self.env, new_svc)?; + let result = base.api_client().deploy_service(token, &org_name, &manifest.environment, manifest.service)?; + println!("Service {} deployed", result.name); + Ok(()) + } + + fn create_new_service(&self, base: &CommandBase, token: &str, org_name: &str) -> Result<()> { + let manifest = self.read_manifest()?; + if let Some(false) = self.no_confirm { + let new_svc_yaml = serde_yaml::to_string(&manifest.service)?; + self.render_diff("".to_string(), new_svc_yaml)?; + } + + let selection = self.user_confirmation(); + if selection == 0 { + println!("Cancelling..."); + return Ok(()) + } + + let result = base.api_client().deploy_service(token, org_name, &manifest.environment, manifest.service)?; println!("Service {} deployed", result.name); Ok(()) } + fn read_manifest(&self) -> Result { + let file_path = self.manifest.clone(); + let mut file_content = String::new(); + File::open(file_path)?.read_to_string(&mut file_content)?; + let manifest = serde_yaml::from_str(&file_content)?; + Ok(manifest) + } + + fn user_confirmation(&self) -> usize { + FuzzySelect::with_theme(&dialoguer::theme::ColorfulTheme::default()) + .with_prompt("Do you want to apply the above changes?") + .items(&["no", "yes"]) + .default(0) + .interact() + .unwrap() + } + fn render_diff(&self, a: String, b: String) -> Result<()> { let Changeset { diffs, .. } = Changeset::new(&a, &b, "\n"); let mut t = term::stdout().unwrap(); @@ -397,19 +405,13 @@ impl InitPlan { #[derive(Parser)] #[derive(Debug)] pub struct List { - #[arg(long, help = "Organization to list the services of")] - org: Option, #[arg(long, help = "Environment to list the services of")] env: String, } impl List { pub fn execute(&self, base: &CommandBase) -> Result<()> { - let org_name = if self.org.is_some() { - self.org.clone().unwrap() - } else { - base.user_config().get_default_org().unwrap().to_string() - }; + let org_name = base.get_org()?; let token = base .user_config() .get_token() diff --git a/src/main.rs b/src/main.rs index 3c1ca9d..82cc925 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,12 @@ use crate::config::user::UserConfig; use anyhow::Result; use camino::Utf8PathBuf; use clap::{Parser, Subcommand}; -use commands::{environments, CommandBase}; +use commands::CommandBase; mod api; mod commands; mod config; + #[derive(Debug, Parser)] #[command( author, @@ -18,6 +19,7 @@ mod config; )] pub struct Cli { #[arg( + global = true, short, long, value_name = "FILE", @@ -27,12 +29,21 @@ pub struct Cli { config: Option, #[arg( + global = true, long, env("MOLNETT_API_URL"), help = "Url of the Molnett API, default is https://api.molnett.org" )] url: Option, + #[arg( + global = true, + long, + env("MOLNETT_ORG"), + help = "Organization to use (overrides default in config)" + )] + org: Option, + #[command(subcommand)] command: Option, } @@ -57,7 +68,7 @@ fn main() -> Result<()> { } let mut config = UserConfig::new(&cli); - let mut base = CommandBase::new(&mut config); + let mut base = CommandBase::new(&mut config, cli.org); match cli.command { Some(Commands::Services(svcs)) => svcs.execute(&mut base),