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

added outputs command #29

Merged
merged 3 commits into from
Nov 10, 2021
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
33 changes: 33 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) -> i32 {
Expand All @@ -27,6 +52,14 @@ pub async fn run_with(args: Vec<String>) -> 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!(),
}
}
Expand Down
128 changes: 4 additions & 124 deletions src/commands/deploy.rs
Original file line number Diff line number Diff line change
@@ -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<std::process::Output> {
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<String, String> {
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<Option<Project>, 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(&current_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 {
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod deploy;
pub mod outputs;
65 changes: 65 additions & 0 deletions src/commands/outputs.rs
Original file line number Diff line number Diff line change
@@ -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<String, serde_yaml::Value>)> = resources
.iter()
.filter_map(|r| r.outputs.as_ref().map(|o| (r.get_ref(), o)))
.collect();

let mut outputs_map: BTreeMap<String, BTreeMap<String, BTreeMap<String, serde_yaml::Value>>> =
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
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod cli;
mod commands;
mod config;
mod logger;
mod project;
mod resource_manager;
mod resources;
mod roblox_api;
Expand Down
128 changes: 128 additions & 0 deletions src/project.rs
Original file line number Diff line number Diff line change
@@ -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<std::process::Output> {
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<String, String> {
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<Option<Project>, 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(&current_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,
}))
}