use std::{
collections::HashMap,
time::{Duration, Instant},
};
use anyhow::{Context, bail};
use serde_json::Value;
use tokio::io::AsyncBufReadExt as _;
use manta_shared::types::dto::CfsSessionGetResponse;
use manta_shared::types::params::session::GetSessionParams;
use super::exec::SatApplyOptions;
use crate::http_client::{CreateImageCfsSessionRequest, MantaClient};
const POLL_INTERVAL: Duration = Duration::from_secs(10);
const POLL_BUDGET: Duration = Duration::from_secs(4 * 60 * 60);
const NOT_VISIBLE_BUDGET: Duration = Duration::from_secs(5 * 60);
pub async fn run_image_pipeline(
client: &MantaClient,
token: &str,
image: &Value,
ref_lookup: &HashMap<String, String>,
opts: &SatApplyOptions<'_>,
) -> anyhow::Result<Value> {
let session = client
.create_image_cfs_session(
token,
&CreateImageCfsSessionRequest {
image: image.clone(),
ref_lookup: ref_lookup.clone(),
ansible_verbosity: opts.ansible_verbosity_opt,
ansible_passthrough: opts.ansible_passthrough_opt.map(str::to_string),
dry_run: opts.dry_run,
},
)
.await
.context("create CFS session from SAT image entry")?;
let session_name = session.name.clone();
let image_name = image
.get("name")
.and_then(Value::as_str)
.unwrap_or("<unnamed>");
tracing::info!(
"CFS session '{session_name}' created for SAT image '{image_name}'"
);
if opts.dry_run {
let id = session
.get_first_result_id()
.unwrap_or_else(|| format!("DRYRUN-{session_name}"));
let image_name = image
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
return Ok(serde_json::json!({ "id": id, "name": image_name }));
}
if opts.watch_logs {
stream_session_until_terminal(
client,
token,
&session_name,
opts.timestamps,
)
.await?;
} else {
poll_session_until_terminal(client, token, &session_name).await?;
}
let stamped = client
.stamp_image_from_cfs_session(token, &session_name)
.await
.with_context(|| {
format!("stamp image from CFS session '{session_name}'")
})?;
serde_json::to_value(&stamped).context("serialise stamped image")
}
async fn stream_session_until_terminal(
client: &MantaClient,
token: &str,
session_name: &str,
timestamps: bool,
) -> anyhow::Result<()> {
tracing::info!("Streaming logs for CFS session '{session_name}' ...");
let reader = client
.stream_session_logs(token, session_name, timestamps)
.await
.with_context(|| {
format!("open SSE log stream for CFS session '{session_name}'")
})?;
let mut lines = reader.lines();
while let Some(raw) = lines
.next_line()
.await
.context("read CFS session log stream")?
{
if let Some(content) = raw.strip_prefix("data: ") {
println!("{content}");
}
}
poll_session_until_terminal(client, token, session_name).await
}
async fn poll_session_until_terminal(
client: &MantaClient,
token: &str,
session_name: &str,
) -> anyhow::Result<()> {
tracing::info!(
"Polling CFS session '{session_name}' until it reaches terminal status \
(poll interval: {}s, hard cap: {} h)",
POLL_INTERVAL.as_secs(),
POLL_BUDGET.as_secs() / 3600,
);
let start = Instant::now();
let mut first_not_visible_at: Option<Instant> = None;
loop {
if start.elapsed() > POLL_BUDGET {
bail!(
"CFS session '{session_name}' did not reach terminal status \
within {} h; aborting monitor. The session may still be running — \
inspect it directly with `manta get sessions --name {session_name}`.",
POLL_BUDGET.as_secs() / 3600,
);
}
match fetch_session_opt(client, token, session_name).await? {
None => {
let stuck_for = first_not_visible_at
.get_or_insert_with(Instant::now)
.elapsed();
if stuck_for > NOT_VISIBLE_BUDGET {
bail!(
"CFS session '{session_name}' was never visible after {} min of \
polling. The create call returned a session name we can't \
fetch back — check the manta-server log for backend errors.",
NOT_VISIBLE_BUDGET.as_secs() / 60,
);
}
tokio::time::sleep(POLL_INTERVAL).await;
}
Some(session) => {
first_not_visible_at = None;
match session.status().as_deref() {
Some("complete") | Some("succeeded") | Some("success") => {
tracing::info!("CFS session '{session_name}' complete");
return Ok(());
}
Some(s) if s.contains("fail") => {
bail!("CFS session '{session_name}' failed (status: '{s}')");
}
Some(s) => {
tracing::debug!(
"CFS session '{session_name}' still running (status: '{s}')",
);
tokio::time::sleep(POLL_INTERVAL).await;
}
None => {
tracing::debug!("CFS session '{session_name}' has no status yet",);
tokio::time::sleep(POLL_INTERVAL).await;
}
}
}
}
}
}
async fn fetch_session_opt(
client: &MantaClient,
token: &str,
session_name: &str,
) -> anyhow::Result<Option<CfsSessionGetResponse>> {
let params = GetSessionParams {
group: None,
xnames: Vec::new(),
min_age: None,
max_age: None,
session_type: None,
status: None,
name: Some(session_name.to_string()),
limit: None,
};
let sessions = client
.get_sessions(token, ¶ms)
.await
.with_context(|| format!("fetch CFS session '{session_name}'"))?;
Ok(sessions.into_iter().next())
}