From da7c5b06f9a25f30690367fcfb04be219232c5a2 Mon Sep 17 00:00:00 2001 From: Sascha Eglau Date: Tue, 20 Feb 2024 20:02:37 +0100 Subject: [PATCH 1/6] feat: add manifest support --- src/commands/services.rs | 121 ++++++++++++++++++++++++++++----------- src/main.rs | 2 +- 2 files changed, 87 insertions(+), 36 deletions(-) diff --git a/src/commands/services.rs b/src/commands/services.rs index 4529053..15927c9 100644 --- a/src/commands/services.rs +++ b/src/commands/services.rs @@ -2,6 +2,8 @@ use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; use dialoguer::{FuzzySelect, Input}; use difference::{Difference, Changeset}; +use std::fs::File; +use std::io::Read; use std::io::Write; use std::path::Path; use super::CommandBase; @@ -65,6 +67,8 @@ pub struct Deploy { port: Option, #[arg(long, help = "Organization to deploy to")] org: Option, + #[arg(short, long, help = "Path to molnett manifest")] + manifest: Option, } impl Deploy { @@ -86,58 +90,43 @@ impl Deploy { &self.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(); + let new_svc = if self.manifest.is_some() { + self.construct_service_from_args()? + } else { + let mut new_svc = existing_svc.clone(); 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() - } + new_svc }; if let Some(false) = self.no_confirm { - if existing_svc.is_some() && existing_svc.clone().unwrap() == new_svc { + if existing_svc == new_svc { 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 existing_svc_yaml = serde_yaml::to_string(&existing_svc)?; let new_svc_yaml = serde_yaml::to_string(&new_svc)?; 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(()) @@ -149,6 +138,68 @@ impl Deploy { Ok(()) } + fn create_new_service(&self, base: &CommandBase, token: &str, org_name: &str) -> Result<()> { + let svc = self.construct_service_from_args()?; + + if let Some(false) = self.no_confirm { + let new_svc_yaml = serde_yaml::to_string(&svc)?; + 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, &self.env, svc)?; + println!("Service {} deployed", result.name); + Ok(()) + } + + fn construct_service_from_args(&self) -> Result { + if self.manifest.is_some() { + if self.image.is_some() || self.port.is_some() { + return Err(anyhow!("CLI arguments for service attributes can not be used together with manifest")) + } + + let svc = self.read_manifest()?; + if self.name != svc.name { + return Err(anyhow!("Name given as CLI argument needs to match name in manifest")) + } + + return Ok(svc) + } else { + // 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")) + } + + return Ok(Service { + name: self.name.clone(), + image: self.image.clone().unwrap(), + container_port: self.port.clone().unwrap() + }) + } + } + + 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 read_manifest(&self) -> Result { + let file_path = self.manifest.clone().unwrap(); + let mut file_content = String::new(); + File::open(file_path)?.read_to_string(&mut file_content)?; + let svc = serde_yaml::from_str(&file_content)?; + Ok(svc) + } + fn render_diff(&self, a: String, b: String) -> Result<()> { let Changeset { diffs, .. } = Changeset::new(&a, &b, "\n"); let mut t = term::stdout().unwrap(); diff --git a/src/main.rs b/src/main.rs index 3c1ca9d..2d81415 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ 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; From e731d28d6066ea947e647b4555f922e1fb9bf61a Mon Sep 17 00:00:00 2001 From: Sascha Eglau Date: Wed, 21 Feb 2024 09:26:41 +0100 Subject: [PATCH 2/6] feat: make manifest the default method and remove args --- src/commands/services.rs | 98 ++++++++++++---------------------------- 1 file changed, 28 insertions(+), 70 deletions(-) diff --git a/src/commands/services.rs b/src/commands/services.rs index 15927c9..602ef32 100644 --- a/src/commands/services.rs +++ b/src/commands/services.rs @@ -2,6 +2,7 @@ use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; use dialoguer::{FuzzySelect, Input}; use difference::{Difference, Changeset}; +use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::Read; use std::io::Write; @@ -14,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, @@ -40,8 +40,7 @@ impl Services { } } -#[derive(Subcommand)] -#[derive(Debug)] +#[derive(Debug, Subcommand)] pub enum Commands { /// Deploy a service #[command(arg_required_else_help = true)] @@ -52,23 +51,20 @@ 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, - #[arg(short, long, help = "Path to molnett manifest")] - manifest: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Manifest { + environment: String, + service: Service } impl Deploy { @@ -83,11 +79,13 @@ impl Deploy { .get_token() .ok_or_else(|| anyhow!("No token found. Please login first."))?; + let manifest = self.read_manifest()?; + let response = base.api_client().get_service( token, &org_name, - &self.env, - &self.name + &manifest.environment, + &manifest.service.name ); let existing_svc = match response { @@ -105,26 +103,13 @@ impl Deploy { } }; - let new_svc = if self.manifest.is_some() { - self.construct_service_from_args()? - } else { - let mut new_svc = existing_svc.clone(); - if let Some(image) = &self.image { - new_svc.image = image.to_string() - } - if let Some(port) = self.port { - new_svc.container_port = port - } - new_svc - }; - if let Some(false) = self.no_confirm { - if existing_svc == new_svc { + if existing_svc == manifest.service { println!("no changes detected"); return Ok(()) } let existing_svc_yaml = serde_yaml::to_string(&existing_svc)?; - let new_svc_yaml = serde_yaml::to_string(&new_svc)?; + let new_svc_yaml = serde_yaml::to_string(&manifest.service)?; self.render_diff(existing_svc_yaml, new_svc_yaml)?; let selection = self.user_confirmation(); if selection == 0 { @@ -133,16 +118,15 @@ impl Deploy { } } - 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 svc = self.construct_service_from_args()?; - + let manifest = self.read_manifest()?; if let Some(false) = self.no_confirm { - let new_svc_yaml = serde_yaml::to_string(&svc)?; + let new_svc_yaml = serde_yaml::to_string(&manifest.service)?; self.render_diff("".to_string(), new_svc_yaml)?; } @@ -152,35 +136,17 @@ impl Deploy { return Ok(()) } - let result = base.api_client().deploy_service(token, org_name, &self.env, svc)?; + let result = base.api_client().deploy_service(token, org_name, &manifest.environment, manifest.service)?; println!("Service {} deployed", result.name); Ok(()) } - fn construct_service_from_args(&self) -> Result { - if self.manifest.is_some() { - if self.image.is_some() || self.port.is_some() { - return Err(anyhow!("CLI arguments for service attributes can not be used together with manifest")) - } - - let svc = self.read_manifest()?; - if self.name != svc.name { - return Err(anyhow!("Name given as CLI argument needs to match name in manifest")) - } - - return Ok(svc) - } else { - // 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")) - } - - return Ok(Service { - name: self.name.clone(), - image: self.image.clone().unwrap(), - container_port: self.port.clone().unwrap() - }) - } + 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 { @@ -192,14 +158,6 @@ impl Deploy { .unwrap() } - fn read_manifest(&self) -> Result { - let file_path = self.manifest.clone().unwrap(); - let mut file_content = String::new(); - File::open(file_path)?.read_to_string(&mut file_content)?; - let svc = serde_yaml::from_str(&file_content)?; - Ok(svc) - } - fn render_diff(&self, a: String, b: String) -> Result<()> { let Changeset { diffs, .. } = Changeset::new(&a, &b, "\n"); let mut t = term::stdout().unwrap(); From 7dcd14167f9f407552bc89ebe35f0d6d83ff421f Mon Sep 17 00:00:00 2001 From: Sascha Eglau Date: Wed, 21 Feb 2024 09:31:46 +0100 Subject: [PATCH 3/6] fix: check if environment exists --- src/commands/services.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/commands/services.rs b/src/commands/services.rs index 602ef32..11c2691 100644 --- a/src/commands/services.rs +++ b/src/commands/services.rs @@ -81,6 +81,11 @@ impl Deploy { 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, From 5caeaf5ba87f932d3d637ad3f3890d2f4e304977 Mon Sep 17 00:00:00 2001 From: Sascha Eglau Date: Wed, 21 Feb 2024 11:20:57 +0100 Subject: [PATCH 4/6] feat: refactor org name handling, make global args global --- src/commands/environments.rs | 25 +++++++------------------ src/commands/mod.rs | 18 ++++++++++++++++-- src/commands/services.rs | 12 ++---------- src/main.rs | 13 ++++++++++++- 4 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/commands/environments.rs b/src/commands/environments.rs index 3d41c0f..92fa513 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,7 +37,7 @@ pub enum Commands { List(List), } -#[derive(Parser, Debug)] +#[derive(Debug, Parser)] pub struct Create { #[arg(help = "Name of the environment to create")] name: String, @@ -47,11 +47,7 @@ pub struct Create { 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 +64,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 11c2691..8bcd942 100644 --- a/src/commands/services.rs +++ b/src/commands/services.rs @@ -69,11 +69,7 @@ pub struct Manifest { 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() @@ -419,11 +415,7 @@ 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/main.rs b/src/main.rs index 2d81415..82cc925 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ 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), From 6edd839f241e7cbeeb4ad6bcc44e739b245ebbe3 Mon Sep 17 00:00:00 2001 From: Sascha Eglau Date: Wed, 21 Feb 2024 11:26:08 +0100 Subject: [PATCH 5/6] refactor: org is now a global arg --- src/commands/environments.rs | 2 -- src/commands/services.rs | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/commands/environments.rs b/src/commands/environments.rs index 92fa513..36b1a59 100644 --- a/src/commands/environments.rs +++ b/src/commands/environments.rs @@ -41,8 +41,6 @@ pub enum Commands { 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 { diff --git a/src/commands/services.rs b/src/commands/services.rs index 8bcd942..abe757c 100644 --- a/src/commands/services.rs +++ b/src/commands/services.rs @@ -57,8 +57,6 @@ pub struct Deploy { 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(long, help = "Organization to deploy to")] - org: Option, } #[derive(Serialize, Deserialize, Debug)] @@ -407,8 +405,6 @@ 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, } From 287b12e93ab9bdc3e10b4d0ec2c1940bbce6f309 Mon Sep 17 00:00:00 2001 From: Sascha Eglau Date: Wed, 21 Feb 2024 11:30:10 +0100 Subject: [PATCH 6/6] chore: serialize is not needed --- src/commands/services.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/services.rs b/src/commands/services.rs index abe757c..78dead8 100644 --- a/src/commands/services.rs +++ b/src/commands/services.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; use dialoguer::{FuzzySelect, Input}; use difference::{Difference, Changeset}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::fs::File; use std::io::Read; use std::io::Write; @@ -59,7 +59,7 @@ pub struct Deploy { no_confirm: Option, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Deserialize, Debug)] pub struct Manifest { environment: String, service: Service