use std::path::PathBuf;
use anyhow::{Context, Error, bail};
use clap::ArgMatches;
use crate::common::app_context::AppContext;
use crate::common::clap_ext::ArgMatchesExt;
use crate::common::confirm;
use crate::http_client::{MantaClient, OpenApiResultExt};
use crate::openapi_client::types::CreateSessionRequest;
use crate::output::action_result;
mod local_git_repo;
const GITEA_REPO_NAME_PREFIX: &str = "cray/";
pub async fn exec(
cli_run_session: &ArgMatches,
ctx: &AppContext<'_>,
token: &str,
) -> Result<(), Error> {
let repo_path_vec: Vec<PathBuf> = cli_run_session
.get_many("repo-path")
.context("'repo-path' argument not provided")?
.cloned()
.collect();
let group_name_arg_opt = cli_run_session.opt_str("group");
let cfs_conf_sess_name_opt = cli_run_session.opt_str("name");
let playbook_file_name_opt = cli_run_session.opt_str("playbook-name");
let group_members_opt = cli_run_session.opt_str("ansible-limit");
let ansible_verbosity = cli_run_session.opt_str("ansible-verbosity");
let ansible_passthrough = cli_run_session.opt_str("ansible-passthrough");
let watch_logs: bool = cli_run_session.get_flag("watch-logs");
let timestamps: bool = cli_run_session.get_flag("timestamps");
let output_opt = cli_run_session.opt_str("output");
let dry_run: bool = cli_run_session.get_flag("dry-run");
run_session(
ctx,
token,
SessionParams {
session_name: cfs_conf_sess_name_opt,
playbook: playbook_file_name_opt,
group_name: group_name_arg_opt,
repos: &repo_path_vec,
ansible_limit: group_members_opt,
ansible_verbosity,
ansible_passthrough,
watch_logs,
timestamps,
output: output_opt,
dry_run,
},
)
.await
}
struct SessionParams<'a> {
session_name: Option<&'a str>,
playbook: Option<&'a str>,
group_name: Option<&'a str>,
repos: &'a [PathBuf],
ansible_limit: Option<&'a str>,
ansible_verbosity: Option<&'a str>,
ansible_passthrough: Option<&'a str>,
watch_logs: bool,
timestamps: bool,
output: Option<&'a str>,
dry_run: bool,
}
async fn run_session(
ctx: &AppContext<'_>,
shasta_token: &str,
p: SessionParams<'_>,
) -> Result<(), Error> {
let cfs_conf_sess_name = p.session_name;
let playbook_yaml_file_name_opt = p.playbook;
let group_name_opt = p.group_name;
let repos_paths = p.repos;
let ansible_limit_opt = p.ansible_limit;
let ansible_verbosity = p.ansible_verbosity;
let ansible_passthrough = p.ansible_passthrough;
let watch_logs = p.watch_logs;
let timestamps = p.timestamps;
let output_opt = p.output;
let (repo_name_vec, repo_last_commit_id_vec) =
check_local_repos(repos_paths, p.dry_run)?;
let req = CreateSessionRequest {
cfs_conf_sess_name: cfs_conf_sess_name.map(str::to_string),
playbook_yaml_file_name: playbook_yaml_file_name_opt.map(str::to_string),
hsm_group: group_name_opt.map(str::to_string),
repo_names: repo_name_vec,
repo_last_commit_ids: repo_last_commit_id_vec,
ansible_limit: ansible_limit_opt.map(str::to_string),
ansible_verbosity: ansible_verbosity.map(str::to_string),
ansible_passthrough: ansible_passthrough.map(str::to_string),
};
if p.dry_run {
return action_result::preview_request(
"POST",
"/sessions",
&req,
output_opt,
);
}
let client = MantaClient::from_app_ctx(ctx, Some(shasta_token))?;
let created = client
.openapi
.create_session(client.site_name(), &req)
.await
.into_anyhow()?;
let cfs_configuration_name = created.configuration_name;
let cfs_session_name = created.session_name;
if watch_logs {
tracing::info!("Fetching logs ...");
use tokio::io::AsyncBufReadExt as _;
let reader = client
.stream_session_logs(&cfs_session_name, timestamps)
.await
.context("Failed to get CFS session log stream from server")?;
let mut lines = reader.lines();
while let Some(raw) = lines
.next_line()
.await
.context("Failed to read CFS session log stream")?
{
if let Some(content) = raw.strip_prefix("data: ") {
println!("{content}");
}
}
}
action_result::print_with_data(
&format!(
"CFS session '{cfs_session_name}' created (configuration: '{cfs_configuration_name}')"
),
&serde_json::json!({
"cfs_configuration_name": cfs_configuration_name,
"cfs_session_name": cfs_session_name,
}),
output_opt,
)?;
Ok(())
}
fn check_local_repos(
repos: &[PathBuf],
dry_run: bool,
) -> Result<(Vec<String>, Vec<String>), Error> {
let mut layers_summary = vec![];
let mut repo_name_vec = Vec::new();
let mut repo_last_commit_id_vec = Vec::new();
for (i, repo_path) in repos.iter().enumerate() {
let repo = match local_git_repo::get_repo(&repo_path.to_string_lossy()) {
Ok(repo) => repo,
Err(_) => {
bail!(
"Could not find a git repo in {}",
repo_path.to_string_lossy()
);
}
};
let local_last_commit = local_git_repo::get_last_commit(&repo)
.with_context(|| {
format!(
"Could not get last commit from repo at {}",
repo_path.display()
)
})?;
tracing::info!("Checking local repo status ({})", &repo.path().display());
let all_committed = local_git_repo::untracked_changed_local_files(&repo)
.map_err(|e| Error::msg(e.to_string()))
.with_context(|| {
format!(
"Could not check local repo status at {}",
repo_path.display()
)
})?;
if !all_committed {
if dry_run {
tracing::warn!(
"[dry-run] Local repo at {} has uncommitted changes; \
preview reflects the last committed id {}.",
repo_path.display(),
local_last_commit.id()
);
} else if confirm::confirm(
"Your local repo has uncommitted changes. Do you want to continue?",
false,
) {
println!(
"Continue. Checking commit id {} against remote",
local_last_commit.id()
);
} else {
bail!("Operation cancelled by user");
}
}
let repo_name_raw = local_git_repo::parse_repo_name_from_remote(&repo)
.with_context(|| {
format!(
"Could not extract repo name from remote in {}",
repo_path.display()
)
})?;
let timestamp = local_last_commit.time().seconds();
let tm = chrono::DateTime::from_timestamp(timestamp, 0)
.context("Could not parse commit timestamp")?;
tracing::debug!(
"\n\nCommit details to apply to CFS layer:\nCommit {}\nAuthor: {}\nDate: {}\n\n {}\n",
local_last_commit.id(),
local_last_commit.author(),
tm,
local_last_commit.message().unwrap_or("no commit message")
);
layers_summary.push(vec![
i.to_string(),
repo_name_raw.clone(),
all_committed.to_string(),
]);
repo_last_commit_id_vec.push(local_last_commit.id().to_string());
let repo_name = GITEA_REPO_NAME_PREFIX.to_owned() + &repo_name_raw;
repo_name_vec.push(repo_name);
}
println!("Please review the following CFS layers:");
for layer_summary in layers_summary {
let layer_num = layer_summary
.first()
.map_or("?", std::string::String::as_str);
let repo_name = layer_summary
.get(1)
.map_or("unknown", std::string::String::as_str);
let committed = layer_summary
.get(2)
.map_or("unknown", std::string::String::as_str);
println!(
" - Layer-{layer_num}; repo name: {repo_name}; local changes committed: {committed}"
);
}
if !dry_run {
if !confirm::confirm(
"Please review the layers and its order and confirm if proceed. Do you want to continue?",
false,
) {
bail!("Operation cancelled by user");
}
println!("Continue. Creating new CFS configuration and layer(s)");
}
Ok((repo_name_vec, repo_last_commit_id_vec))
}
#[cfg(test)]
mod tests {
#[test]
fn accepts_dry_run() {
let result = crate::build::build_cli().try_get_matches_from([
"manta",
"run",
"session",
"--name",
"test-session",
"--repo-path",
"/tmp/repo",
"--ansible-limit",
"x1000c0s0b0n0",
"--dry-run",
]);
assert!(
result.is_ok(),
"expected --dry-run to parse on `run session`: {result:?}"
);
}
#[test]
fn accepts_dry_run_short_alias() {
let result = crate::build::build_cli().try_get_matches_from([
"manta",
"run",
"session",
"--name",
"test-session",
"--repo-path",
"/tmp/repo",
"--ansible-limit",
"x1000c0s0b0n0",
"-d",
]);
assert!(
result.is_ok(),
"expected -d short alias to parse: {result:?}"
);
}
}