use std::path::PathBuf;
use std::sync::Arc;
use super::error::AgentError;
use super::registry::AgentSeats;
use crate::config::types::LysConfig;
use crate::link_pass::MachinePass;
use crate::services::lifecycle::Placer;
use crate::services::lys_http::{HttpLys, LysHttp, segment};
struct Effects {
http: HttpLys,
app_credential: Option<PathBuf>,
pass: Option<MachinePass>,
machine: Option<String>,
}
impl std::fmt::Debug for Effects {
fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
out.debug_struct("AgentLysEffects").finish_non_exhaustive()
}
}
type Bindings = (Option<Arc<dyn Placer>>, Option<Arc<dyn AgentSeats>>);
pub fn configured(
lys: &LysConfig,
runtime: Option<tokio::runtime::Handle>,
) -> Result<Bindings, AgentError> {
if lys.app_credential_path.is_none() && lys.machine.is_none() {
return Ok((None, None));
}
let handle = runtime.ok_or(AgentError::Invalid(
"agent Lys effects require the server runtime",
))?;
let http = HttpLys::new(lys.api_url.as_deref().unwrap_or(&lys.issuer), handle)
.map_err(|_| AgentError::Invalid("agent Lys API origin"))?;
let pass = lys
.machine
.as_ref()
.map(MachinePass::configured)
.transpose()
.map_err(|_| AgentError::Invalid("agent Lys machine pass source"))?;
let effects = Arc::new(Effects {
http,
app_credential: lys.app_credential_path.clone(),
pass,
machine: lys
.services
.as_ref()
.map(|services| services.machine.clone()),
});
let placer: Option<Arc<dyn Placer>> = lys
.app_credential_path
.as_ref()
.map(|_| effects.clone() as Arc<dyn Placer>);
let seats: Option<Arc<dyn AgentSeats>> = effects
.machine
.as_ref()
.map(|_| effects.clone() as Arc<dyn AgentSeats>);
Ok((placer, seats))
}
impl Placer for Effects {
fn place(
&self,
operation: &str,
child: (&str, &str),
parent: (&str, &str),
) -> Result<(), String> {
let path = self
.app_credential
.as_ref()
.ok_or("agent_app_credential_absent")?;
let credential = zeroize::Zeroizing::new(
std::fs::read_to_string(path).map_err(|_| "agent_app_credential_unreadable")?,
);
let answer = self
.http
.post(
credential.trim(),
"apps/liminal/placements",
&serde_json::json!({
"operation":operation, "child":{"kind":child.0,"id":child.1},
"parent":{"kind":parent.0,"id":parent.1}}),
)
.map_err(|_| "lys_could_not_be_asked")?;
if answer.succeeded() {
Ok(())
} else {
Err(stable_refusal(&answer))
}
}
}
impl Effects {
fn seat(&self, name: &str, operation: &str, start: bool) -> Result<(), AgentError> {
let pass = self
.pass
.as_ref()
.ok_or(AgentError::Invalid("agent machine pass absent"))?
.current()
.map_err(|_| AgentError::Invalid("agent machine pass unavailable"))?;
let route = if start {
format!("agents/{}/start-command", segment(name))
} else {
format!("seats/{}/stop", segment(name))
};
let body = if start {
serde_json::json!({"operation":operation,
"machine":self.machine.as_ref().ok_or(AgentError::Invalid("agent machine absent"))?})
} else {
serde_json::json!({"operation":operation,"force":false})
};
let answer = self
.http
.post(&pass, &route, &body)
.map_err(|_| AgentError::Delivery("lys_could_not_be_asked"))?;
if answer.succeeded() {
Ok(())
} else {
Err(AgentError::Store(stable_refusal(&answer)))
}
}
}
impl AgentSeats for Effects {
fn start(&self, name: &str, operation: &str) -> Result<(), AgentError> {
self.seat(name, operation, true)
}
fn stop(&self, name: &str, operation: &str) -> Result<(), AgentError> {
self.seat(name, operation, false)
}
}
fn stable_refusal(answer: &crate::services::lys_http::LysAnswer) -> String {
let name = answer.refusal();
if name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
&& !name.is_empty()
{
name
} else {
format!("lys_status_{}", answer.status)
}
}
#[cfg(test)]
mod tests {
#[test]
fn effects_use_the_supplied_runtime_outside_a_tokio_context()
-> Result<(), Box<dyn std::error::Error>> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
assert!(tokio::runtime::Handle::try_current().is_err());
let lys = serde_json::from_value(serde_json::json!({
"issuer": "https://issuer.test",
"network_id": "network",
"keys_path": "keys.json",
"app_credential_path": "connector"
}))?;
let (placer, seats) = super::configured(&lys, Some(runtime.handle().clone()))?;
assert!(placer.is_some());
assert!(seats.is_none());
Ok(())
}
}