trisync 0.1.3

A friendly CLI Tool for automating synchronization of multiple TRIRIGA environment by using the OM API
Documentation
use crate::{
	config::{ApplicationConfig, TririgaConfig, TririgaOMObject},
	repo::Repo,
	tririga::api::{Api, Credentials, PackageStatus},
	tririga::deploy::Deployer,
};

use std::env::current_dir;
use std::error::Error;
use std::fs::OpenOptions;
use std::fs::{read_to_string, File};
use std::io::prelude::*;
use std::io::BufReader;
use std::path::Path;
use std::str;
use std::{thread, time};

const APP_CONFIG_PATH: &str = "./config.yaml";
const TRIRIGA_CONFIG_PATH: &str = "./tririga.yaml";
const DELAY: time::Duration = time::Duration::from_secs(3);

/// Pulling changes from remote
/// Run git pull
/// Copy (if there was any change) the updated zip file to the OM directory
/// Watch the log file for import progress
/// Notify user once migration is done
pub async fn pull() -> Result<(), Box<dyn Error>> {
	let current_directory = current_dir()?.to_str().unwrap().to_owned();
	// Open Git repo and pull
	let repo = Repo::new(&current_directory);
	let (repo_ahead, repo_behind) = repo.is_ahead_behind_remote();
	if repo_ahead {
		println!("✋ Repo is ahead of remote. Please push changes first");
	}
	if repo_behind {
		repo.pull()?;
		// Init TRIRIGA API
		deploy_from_directory(&repo.last_commit_id()?[..7], &current_directory).await?;
	} else {
		println!("🤷 No update")
	}
	Ok(())
}

/// Deploy package from current directory to TRIRIGA
pub async fn deploy() -> Result<(), Box<dyn Error>> {
	let current_directory = current_dir()?.to_str().unwrap().to_owned();
	// Open Git repo and pull
	let repo = Repo::new(&current_directory);
	deploy_from_directory(&repo.last_commit_id()?[..7], &current_directory).await?;

	Ok(())
}

/// Deploy package from specified directory to TRIRIGA with
/// the package named by last commit hash
async fn deploy_from_directory(
	repo_last_commit_id: &str,
	current_directory: &str,
) -> Result<(), Box<dyn Error>> {
	let app_config = ApplicationConfig::from_yaml(APP_CONFIG_PATH)?;
	let tririga_config = TririgaConfig::from_yaml(TRIRIGA_CONFIG_PATH)?;
	let api = Api::new(
		&tririga_config.url,
		Credentials {
			user: tririga_config.user.clone(),
			password: tririga_config.password.clone(),
		},
	);
	api.login().await?;

	// Deployment
	// Include commit hash in deployement package name
	// Two different package name. One with .zip for the OS and one without for TRIRIGA
	let tririga_deployed_package_name =
		format!("{}-{}", repo_last_commit_id, &app_config.package_name);
	let os_deployed_package_name = format!("{}.zip", &tririga_deployed_package_name);
	deploy_package(
		&api,
		&tririga_config.om_path,
		&app_config.package_name,
		current_directory,
		&os_deployed_package_name,
		&tririga_deployed_package_name,
	)
	.await?;
	Ok(())
}

/// Pushing local changes
/// Add all changes to the package (git add .)
/// Export package by ID (git commit)
/// Dowload the package from the user files
/// Push changes to git
pub async fn push(message: &str) -> Result<(), Box<dyn Error>> {
	let current_directory = current_dir()?.to_str().unwrap().to_owned();
	// Open git repo
	let app_config = ApplicationConfig::from_yaml(APP_CONFIG_PATH)?;
	let tririga_config = TririgaConfig::from_yaml(TRIRIGA_CONFIG_PATH)?;
	let repo = Repo::new(&current_directory);
	let (_, repo_behind) = repo.is_ahead_behind_remote();
	if repo_behind {
		println!("✋ Repo is behind remote. Please pull changes first");
	} else {
		// Init API
		let api = Api::new(
			&tririga_config.url,
			Credentials {
				user: tririga_config.user.clone(),
				password: tririga_config.password.clone(),
			},
		);
		api.login().await?;
		// Create a new package
		let current_time_millis = std::time::SystemTime::now()
			.duration_since(std::time::UNIX_EPOCH)
			.expect("Time went backward")
			.as_millis();
		let package_name =
			format!("{} - {}", &app_config.package_name, current_time_millis);
		let package_description = format!(
			"{} - {}. {}",
			current_time_millis, message, &app_config.package_description
		);
		let package_id = api
			.create_new_package(&package_name, &package_description)
			.await?;
		// Add all application changes to the package
		println!("🌀 Adding objects to package");
		for object in app_config.objects {
			api.add_object_to_package_by_id(
				&package_id.to_string(),
				&object.object_type,
				&object.object_name,
				&object.module_name,
				&object.bo_name.clone().unwrap_or("".to_string()),
			)
			.await?;
			println!(
				"\t{} {} added to package",
				&object.object_type, &object.object_name
			);
		}
		// Export package
		api.export_package_by_id(&package_id.to_string()).await?;
		loop {
			let package_status = api
				.get_package_status_by_id(&package_id.to_string())
				.await?;
			match package_status {
				PackageStatus::Exported => {
					println!("📦 Package Exported");
					break;
				}
				PackageStatus::ExportFailed => {
					println!("❌ Package Export Failed. Consider inspecting log files from TRIRIGA");
					break;
				}

				PackageStatus::New => {
					println!("🛑 There is a problem exporting. Package is declared as 'New'. Take a look at the TRIRIGA OM Logs and Console");
				}
				PackageStatus::ExportPending => {
					continue;
				}
				_ => {
					println!(
			"🛑 The package status is {:?}, which is impossible since we are trying to export it",
			&package_status
		    )
				}
			}
			thread::sleep(DELAY);
		}
		// Download package
		println!("📥 Saving package as zip file");
		let zip_file_path = format!(
			"{}.{}",
			Path::new(&current_directory)
				.join(Path::new(&app_config.package_name))
				.to_str()
				.unwrap(),
			"zip"
		);
		api.download_package_as_zip(&package_name, &Path::new(&zip_file_path))
			.await?;
		// Commit changes and push
		let index = repo.add_all()?;
		repo.commit(index, message)?;
		repo.push()?;
		println!("🚀 All changes commited and pushed");
	}
	Ok(())
}

/// Launch initialization process
pub async fn init() -> Result<(), Box<dyn Error>> {
	ApplicationConfig::init(APP_CONFIG_PATH)?;
	let tririga_config = TririgaConfig::init(TRIRIGA_CONFIG_PATH)?;
	match tririga_config {
		Some(_) => {
			println!("✅ Initialization successful");
			add_config_to_git_ignore()?;
			Ok(())
		}
		None => {
			println!("❌ Initialization abandoned");
			Ok(())
		}
	}
}

/// Create a dialog and add the OM object to config file. Then perform a push to the repository
/// with commit message 'OM object list add - <object's name>'
pub fn add_tririga_object_to_config() -> Result<(), Box<dyn Error>> {
	let current_directory = current_dir()?.to_str().unwrap().to_owned();
	let repo = Repo::new(&current_directory);
	let (_, is_behind) = repo.is_ahead_behind_remote();
	if is_behind {
		println!("🛑 Repository is behind remote. Perform a 'pull' first");
		return Ok(());
	}
	let object = TririgaOMObject::from_dialog()?;

	println!("⏳ Modifying config file");
	// Add the new object to config and deserialize it to disk
	let mut app_config = ApplicationConfig::from_yaml(APP_CONFIG_PATH)?;
	app_config.objects.push(object.clone());
	ApplicationConfig::to_yaml(&app_config, APP_CONFIG_PATH)?;
	println!("📝 Config file saved locally");
	println!("📩 Pushing change to remote");

	// Commit changes and push
	let index = repo.add_all()?;
	let commit_message = format!("OM Object list update - {:#?}", object);
	repo.commit(index, &commit_message)?;
	repo.push()?;
	println!("🚀 Config file updated and pushed");
	Ok(())
}

/// Create a multi select dialog to delete selected object from config file. Then perform a push to the repository
/// with commit message 'OM object list deletion - [<object name>]'
pub fn delete_tririga_object_from_config() -> Result<(), Box<dyn Error>> {
	let current_directory = current_dir()?.to_str().unwrap().to_owned();
	let repo = Repo::new(&current_directory);
	let (_, is_behind) = repo.is_ahead_behind_remote();
	if is_behind {
		println!("🛑 Repository is behind remote. Perform a 'pull' first");
		return Ok(());
	}
	let mut app_config = ApplicationConfig::from_yaml(APP_CONFIG_PATH)?;
	let deleted_objects = app_config.delete_tririga_om_objects_dialog()?;
	println!("⏳ Modifying config file");
	ApplicationConfig::to_yaml(&app_config, APP_CONFIG_PATH)?;

	println!("📝 Config file saved locally");
	println!("📩 Pushing change to remote");

	// Commit changes and push
	let index = repo.add_all()?;
	let commit_message = format!("OM object list deletion - {:#?}", &deleted_objects);
	repo.commit(index, &commit_message)?;
	repo.push()?;
	println!("🚀 Config file updated and pushed");
	Ok(())
}

/// Add the 'tririga.yaml' entry to the .gitignore file in the current directory.
/// Exit if line already exist.
fn add_config_to_git_ignore() -> Result<(), Box<dyn Error>> {
	let file_path = ".gitignore";
	let tririga_yaml_file_name = "tririga.yaml";
	if Path::new(file_path).exists() {
		let file = std::fs::File::open(file_path)?;
		let reader = BufReader::new(&file);
		for line in reader.lines() {
			match line {
				Ok(line) => {
					if line.contains(tririga_yaml_file_name) {
						return Ok(());
					}
				}
				Err(e) => println!("ERROR: {}", e),
			}
		}
		// If there is already a line in .gitignore file, write a line skip then append 'tririga.yaml'
		let mut file = OpenOptions::new()
			.write(true)
			.append(true)
			.open(&file_path)
			.unwrap();
		if read_to_string(tririga_yaml_file_name)?.len() > 0 {
			println!("There is something in the .gitignore file. Skipping a line to write");
			if let Err(e) = writeln!(file) {
				eprintln!("Couldn't write to file: {}", e);
			}
		}
		if let Err(e) = write!(file, "{}", tririga_yaml_file_name) {
			eprintln!("Couldn't write to file: {}", e);
		}
		return Ok(());
	}
	let mut git_ignore = File::create(Path::new(file_path))?;
	if let Err(e) = write!(git_ignore, "{}", tririga_yaml_file_name) {
		eprintln!("Couldn't write to file: {}", e);
	}
	Ok(())
}

/// Deploy a package to mapped TIRRIGA directory and wait for the OM Agent to pick up the file.
/// Then watch for package status update via API and exit when the package is deployed or has an inappropriate status.
async fn deploy_package(
	api: &Api,
	om_path: &str,
	package_name: &str,
	current_directory: &str,
	os_deployed_package_name: &str,
	tririga_deployed_package_name: &str,
) -> Result<(), Box<dyn Error>> {
	let deployer = Deployer::new(om_path, package_name)?;
	println!("🎬 Begin deploying package");
	deployer.execute(current_directory, os_deployed_package_name)?;
	// Monitor package existence in deployment path
	println!("📡 Package will be picked up automatically by OM Agent");
	while Path::new(om_path)
		.join(Path::new(&os_deployed_package_name))
		.exists()
	{
		let cooling_duration = std::time::Duration::from_secs(2);
		std::thread::sleep(cooling_duration);
	}
	println!("🍒 Package deployed. Waiting for status update from API...");
	// Monitor package status
	loop {
		let package_status = api
			.get_package_status_by_name(tririga_deployed_package_name)
			.await?;
		match package_status {
			PackageStatus::Imported => {
				println!("✅ Package Deployed (Imported)");
				return Ok(());
			}
			PackageStatus::ImportFailed => {
				panic!("❌ Package Import Failed. Consider inspecting log files from TRIRIGA");
			}

			PackageStatus::New => {
				panic!("⚠️️ There is a problem deploying. Package is declared as 'New'. Take a look at the TRIRIGA OM Logs and Console");
			}
			PackageStatus::ImportPending => {
				continue;
			}
			_ => {
				println!(
			    "🛑 The package status is {:?}, which is impossible since we are trying to export it",
			    &package_status
			)
			}
		}

		thread::sleep(DELAY);
	}
}