diff --git a/src/cli.rs b/src/cli.rs index 19066fc..9d496e9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -18,6 +18,31 @@ fn get_app() -> App<'static, 'static> { .takes_value(true), ), ) + .subcommand(SubCommand::with_name("outputs") + .about("Prints the outputs from the project to the console or a file") + .arg( + Arg::with_name("PROJECT") + .index(1) + .help("The project to print outputs from: either the path to a directory containing a 'rocat.yml' file or the path to a configuration file. Defaults to the current directory.") + .takes_value(true), + ) + .arg( + Arg::with_name("output") + .long("output") + .short("o") + .help("A file path to print the outputs to") + .value_name("FILE") + .takes_value(true)) + .arg( + Arg::with_name("format") + .long("format") + .short("f") + .help("The format to print the outputs in") + .value_name("FORMAT") + .takes_value(true) + .possible_values(&["json","yaml"]) + .default_value("json")) + ) } pub async fn run_with(args: Vec) -> i32 { @@ -27,6 +52,14 @@ pub async fn run_with(args: Vec) -> i32 { ("deploy", Some(deploy_matches)) => { commands::deploy::run(deploy_matches.value_of("PROJECT")).await } + ("outputs", Some(outputs_matches)) => { + commands::outputs::run( + outputs_matches.value_of("PROJECT"), + outputs_matches.value_of("output"), + outputs_matches.value_of("format").unwrap(), + ) + .await + } _ => unreachable!(), } } diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 597294c..ed458bb 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -1,135 +1,15 @@ -use std::{ - path::{Path, PathBuf}, - process::Command, - str, -}; +use std::str; use yansi::Paint; use crate::{ - config::{load_config_file, Config, DeploymentConfig}, logger, + project::{load_project, Project}, resource_manager::RobloxResourceManager, - resources::{EvaluateResults, ResourceGraph, ResourceManager}, - state::{get_desired_graph, get_previous_state, save_state, ResourceState}, + resources::{EvaluateResults, ResourceManager}, + state::save_state, }; -fn run_command(command: &str) -> std::io::Result { - if cfg!(target_os = "windows") { - return Command::new("cmd").arg("/C").arg(command).output(); - } else { - return Command::new("sh").arg("-c").arg(command).output(); - } -} - -fn get_current_branch() -> Result { - let output = run_command("git symbolic-ref --short HEAD"); - let result = match output { - Ok(v) => v, - Err(e) => { - return Err(format!( - "Unable to determine git branch. Are you in a git repository?\n\t{}", - e - )) - } - }; - - if !result.status.success() { - return Err("Unable to determine git branch. Are you in a git repository?".to_string()); - } - - let current_branch = str::from_utf8(&result.stdout).unwrap().trim(); - if current_branch.is_empty() { - return Err("Unable to determine git branch. Are you in a git repository?".to_string()); - } - - Ok(current_branch.to_owned()) -} - -fn match_branch(branch: &str, patterns: &[String]) -> bool { - for pattern in patterns { - let glob_pattern = glob::Pattern::new(pattern); - if glob_pattern.is_ok() && glob_pattern.unwrap().matches(branch) { - return true; - } - } - false -} - -fn parse_project(project: Option<&str>) -> Result<(PathBuf, PathBuf), String> { - let project = project.unwrap_or("."); - let project_path = Path::new(project).to_owned(); - - let (project_dir, config_file) = if project_path.is_dir() { - (project_path.clone(), project_path.join("rocat.yml")) - } else if project_path.is_file() { - (project_path.parent().unwrap().into(), project_path) - } else { - return Err(format!("Unable to load project path: {}", project)); - }; - - if config_file.exists() { - return Ok((project_dir, config_file)); - } - - Err(format!("Config file {} not found", config_file.display())) -} - -struct Project { - project_path: PathBuf, - next_graph: ResourceGraph, - previous_graph: ResourceGraph, - state: ResourceState, - deployment_config: DeploymentConfig, - config: Config, -} - -async fn load_project(project: Option<&str>) -> Result, String> { - let (project_path, config_file) = parse_project(project)?; - - let config = load_config_file(&config_file)?; - logger::log(format!("Loaded config file {}", config_file.display())); - - let current_branch = get_current_branch()?; - - let deployment_config = config - .deployments - .iter() - .find(|deployment| match_branch(¤t_branch, &deployment.branches)); - - let deployment_config = match deployment_config { - Some(v) => v, - None => { - logger::log(format!( - "No deployment configuration found for branch '{}'", - current_branch - )); - return Ok(None); - } - }; - logger::log(format!( - "Found deployment configuration '{}' for branch '{}'", - deployment_config.name, current_branch - )); - - // Get previous state - let state = get_previous_state(project_path.as_path(), &config, deployment_config).await?; - - // Get our resource graphs - let previous_graph = - ResourceGraph::new(state.deployments.get(&deployment_config.name).unwrap()); - let next_graph = get_desired_graph(project_path.as_path(), &config, deployment_config)?; - - Ok(Some(Project { - project_path, - next_graph, - previous_graph, - state, - deployment_config: deployment_config.clone(), - config, - })) -} - pub async fn run(project: Option<&str>) -> i32 { logger::start_action("Loading project:"); let Project { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 61d7d29..52aa4a0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1 +1,2 @@ pub mod deploy; +pub mod outputs; diff --git a/src/commands/outputs.rs b/src/commands/outputs.rs new file mode 100644 index 0000000..6e51f95 --- /dev/null +++ b/src/commands/outputs.rs @@ -0,0 +1,65 @@ +use std::{collections::BTreeMap, fs}; + +use yansi::Paint; + +use crate::{ + logger, + project::{load_project, Project}, + resources::ResourceRef, +}; + +pub async fn run(project: Option<&str>, output: Option<&str>, format: &str) -> i32 { + logger::start_action("Load outputs:"); + let Project { previous_graph, .. } = match load_project(project).await { + Ok(Some(v)) => v, + Ok(None) => { + logger::end_action("No outputs available"); + return 0; + } + Err(e) => { + logger::end_action(Paint::red(e)); + return 1; + } + }; + + let resources = previous_graph.get_resource_list(); + let outputs_list: Vec<(ResourceRef, &BTreeMap)> = resources + .iter() + .filter_map(|r| r.outputs.as_ref().map(|o| (r.get_ref(), o))) + .collect(); + + let mut outputs_map: BTreeMap>> = + BTreeMap::new(); + for ((resource_type, resource_id), outputs) in outputs_list { + let type_map = outputs_map + .entry(resource_type) + .or_insert_with(BTreeMap::new); + type_map.insert(resource_id, outputs.clone()); + } + + let outputs_string = match match format { + "json" => serde_json::to_string_pretty(&outputs_map).map_err(|e| e.to_string()), + "yaml" => serde_yaml::to_string(&outputs_map).map_err(|e| e.to_string()), + _ => Err(format!("Unknown format: {}", format)), + } { + Ok(v) => v, + Err(e) => { + logger::end_action(Paint::red(format!("Failed to serialize outputs: {}", e))); + return 1; + } + }; + logger::end_action("Succeeded"); + + if let Some(output) = output { + if let Err(e) = fs::write(output, outputs_string) + .map_err(|e| format!("Unable to write outputs file: {}\n\t{}", output, e)) + { + logger::log(Paint::red(e)); + return 1; + } + } else { + logger::log(outputs_string); + } + + 0 +} diff --git a/src/main.rs b/src/main.rs index 7a5a957..36cd39d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod cli; mod commands; mod config; mod logger; +mod project; mod resource_manager; mod resources; mod roblox_api; diff --git a/src/project.rs b/src/project.rs new file mode 100644 index 0000000..c43e29e --- /dev/null +++ b/src/project.rs @@ -0,0 +1,128 @@ +use std::{ + path::{Path, PathBuf}, + process::Command, + str, +}; + +use crate::{ + config::{load_config_file, Config, DeploymentConfig}, + logger, + resources::ResourceGraph, + state::{get_desired_graph, get_previous_state, ResourceState}, +}; + +fn parse_project(project: Option<&str>) -> Result<(PathBuf, PathBuf), String> { + let project = project.unwrap_or("."); + let project_path = Path::new(project).to_owned(); + + let (project_dir, config_file) = if project_path.is_dir() { + (project_path.clone(), project_path.join("rocat.yml")) + } else if project_path.is_file() { + (project_path.parent().unwrap().into(), project_path) + } else { + return Err(format!("Unable to load project path: {}", project)); + }; + + if config_file.exists() { + return Ok((project_dir, config_file)); + } + + Err(format!("Config file {} not found", config_file.display())) +} + +fn run_command(command: &str) -> std::io::Result { + if cfg!(target_os = "windows") { + return Command::new("cmd").arg("/C").arg(command).output(); + } else { + return Command::new("sh").arg("-c").arg(command).output(); + } +} + +fn get_current_branch() -> Result { + let output = run_command("git symbolic-ref --short HEAD"); + let result = match output { + Ok(v) => v, + Err(e) => { + return Err(format!( + "Unable to determine git branch. Are you in a git repository?\n\t{}", + e + )) + } + }; + + if !result.status.success() { + return Err("Unable to determine git branch. Are you in a git repository?".to_string()); + } + + let current_branch = str::from_utf8(&result.stdout).unwrap().trim(); + if current_branch.is_empty() { + return Err("Unable to determine git branch. Are you in a git repository?".to_string()); + } + + Ok(current_branch.to_owned()) +} + +fn match_branch(branch: &str, patterns: &[String]) -> bool { + for pattern in patterns { + let glob_pattern = glob::Pattern::new(pattern); + if glob_pattern.is_ok() && glob_pattern.unwrap().matches(branch) { + return true; + } + } + false +} + +pub struct Project { + pub project_path: PathBuf, + pub next_graph: ResourceGraph, + pub previous_graph: ResourceGraph, + pub state: ResourceState, + pub deployment_config: DeploymentConfig, + pub config: Config, +} + +pub async fn load_project(project: Option<&str>) -> Result, String> { + let (project_path, config_file) = parse_project(project)?; + + let config = load_config_file(&config_file)?; + logger::log(format!("Loaded config file {}", config_file.display())); + + let current_branch = get_current_branch()?; + + let deployment_config = config + .deployments + .iter() + .find(|deployment| match_branch(¤t_branch, &deployment.branches)); + + let deployment_config = match deployment_config { + Some(v) => v, + None => { + logger::log(format!( + "No deployment configuration found for branch '{}'", + current_branch + )); + return Ok(None); + } + }; + logger::log(format!( + "Found deployment configuration '{}' for branch '{}'", + deployment_config.name, current_branch + )); + + // Get previous state + let state = get_previous_state(project_path.as_path(), &config, deployment_config).await?; + + // Get our resource graphs + let previous_graph = + ResourceGraph::new(state.deployments.get(&deployment_config.name).unwrap()); + let next_graph = get_desired_graph(project_path.as_path(), &config, deployment_config)?; + + Ok(Some(Project { + project_path, + next_graph, + previous_graph, + state, + deployment_config: deployment_config.clone(), + config, + })) +}