Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add manifest support #10

Merged
merged 6 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 7 additions & 20 deletions src/commands/environments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use anyhow::{anyhow, Result};
use clap::{Parser, Subcommand};
use tabled::Table;

#[derive(Parser, Debug)]
#[derive(Debug, Parser)]
#[command(
author,
version,
Expand All @@ -27,7 +27,7 @@ impl Environments {
}
}

#[derive(Subcommand, Debug)]
#[derive(Debug, Subcommand)]
pub enum Commands {
/// Create an environment
#[command(arg_required_else_help = true)]
Expand All @@ -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<String>,
}

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()
Expand All @@ -68,19 +62,12 @@ impl Create {
}
}

#[derive(Parser, Debug)]
pub struct List {
#[arg(long, help = "Organization to list the environments of")]
org: Option<String>,
}
#[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()
Expand Down
18 changes: 16 additions & 2 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell;

use crate::{
Expand All @@ -19,13 +19,15 @@ pub mod services;
pub struct CommandBase<'a> {
user_config: &'a mut UserConfig,
app_config: OnceCell<ApplicationConfig>,
org_arg: Option<String>,
}

impl CommandBase<'_> {
pub fn new(user_config: &mut UserConfig) -> CommandBase {
pub fn new(user_config: &mut UserConfig, org_arg: Option<String>) -> CommandBase {
CommandBase {
user_config,
app_config: OnceCell::new(),
org_arg,
}
}

Expand Down Expand Up @@ -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<String> {
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)
}
}
150 changes: 76 additions & 74 deletions src/commands/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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)]
Expand All @@ -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<String>,
#[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<bool>,
#[arg(short, long, help = "Port the application listens on")]
port: Option<u16>,
#[arg(long, help = "Organization to deploy to")]
org: Option<String>,
}

#[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<Service>;
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<Manifest> {
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();
Expand Down Expand Up @@ -397,19 +405,13 @@ impl InitPlan {
#[derive(Parser)]
#[derive(Debug)]
pub struct List {
#[arg(long, help = "Organization to list the services of")]
org: Option<String>,
#[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()
Expand Down
15 changes: 13 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -18,6 +19,7 @@ mod config;
)]
pub struct Cli {
#[arg(
global = true,
short,
long,
value_name = "FILE",
Expand All @@ -27,12 +29,21 @@ pub struct Cli {
config: Option<Utf8PathBuf>,

#[arg(
global = true,
long,
env("MOLNETT_API_URL"),
help = "Url of the Molnett API, default is https://api.molnett.org"
)]
url: Option<String>,

#[arg(
global = true,
long,
env("MOLNETT_ORG"),
help = "Organization to use (overrides default in config)"
)]
org: Option<String>,

#[command(subcommand)]
command: Option<Commands>,
}
Expand All @@ -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),
Expand Down
Loading