use std::collections::BTreeMap;
use std::time::Duration;
use serde::Serialize;
use crate::cli::{
HostArgs, HostCommand, HostLeaseAcquireArgs, HostLeaseArgs, HostLeaseCommand,
HostLeasePriorityArg, HostLeaseReleaseArgs, HostLeaseRenewArgs, HostLeaseStatusArgs,
};
use crate::json_envelope::{to_string_pretty, JsonEnvelope, JsonError};
pub const HOST_LEASE_CLI_SCHEMA_VERSION: u32 = 1;
const EX_TEMPFAIL: i32 = 75;
pub(crate) fn run(args: HostArgs) -> i32 {
match args.command {
HostCommand::Lease(args) => run_lease(args),
}
}
fn run_lease(args: HostLeaseArgs) -> i32 {
let json = lease_command_json(&args.command);
let store = match harn_hostlib::HostLeaseStore::from_env() {
Ok(store) => store,
Err(error) => return print_error("host_lease_store", &error.to_string(), json),
};
match args.command {
HostLeaseCommand::Acquire(args) => acquire(&store, args),
HostLeaseCommand::Renew(args) => renew(&store, args),
HostLeaseCommand::Release(args) => release(&store, args),
HostLeaseCommand::Status(args) => status(&store, args),
}
}
fn lease_command_json(command: &HostLeaseCommand) -> bool {
match command {
HostLeaseCommand::Acquire(args) => args.json,
HostLeaseCommand::Renew(args) => args.json,
HostLeaseCommand::Release(args) => args.json,
HostLeaseCommand::Status(args) => args.json,
}
}
fn acquire(store: &harn_hostlib::HostLeaseStore, args: HostLeaseAcquireArgs) -> i32 {
let json = args.json;
let host = args
.host
.unwrap_or_else(harn_hostlib::HostLeaseStore::default_host);
let request = harn_hostlib::HostLeaseRequest {
host,
owner: args.owner,
priority_class: priority(args.priority_class),
ttl_ms: (!args.no_expiry).then_some(args.ttl_ms),
owner_pid: args.owner_pid,
reason: args.reason,
metadata: BTreeMap::new(),
};
let result = if args.wait_ms == 0 {
store.try_acquire(request)
} else {
store.acquire_wait(request, Duration::from_millis(args.wait_ms))
};
match result {
Ok(receipt) if receipt.status == harn_hostlib::HostLeaseAcquireStatus::Acquired => {
print_success(&receipt, json, |receipt| {
let handle = receipt
.handle
.as_ref()
.expect("acquired receipt has handle");
format!(
"Acquired {} lease on {} as {} ({})",
handle.priority_class.as_str(),
handle.host,
handle.owner,
handle.lease_id
)
});
0
}
Ok(receipt) => {
if json {
let error_code = receipt
.defer
.as_ref()
.expect("deferred receipt has reason")
.deferred_reason
.as_str();
let envelope = JsonEnvelope {
schema_version: HOST_LEASE_CLI_SCHEMA_VERSION,
ok: false,
data: Some(receipt),
error: Some(JsonError {
code: error_code.to_string(),
message: "host lease remains held by another owner".to_string(),
details: serde_json::Value::Null,
}),
warnings: Vec::new(),
};
println!("{}", to_string_pretty(&envelope));
} else if let Some(defer) = receipt.defer {
if let Some(active) = defer.active {
eprintln!(
"Host {} is leased by {} ({}, lease {})",
defer.host,
active.owner,
active.priority_class.as_str(),
active.lease_id
);
} else {
eprintln!("Host {} lease registry is busy", defer.host);
}
}
EX_TEMPFAIL
}
Err(error) => print_error("host_lease_acquire", &error.to_string(), json),
}
}
fn renew(store: &harn_hostlib::HostLeaseStore, args: HostLeaseRenewArgs) -> i32 {
let host = args
.host
.unwrap_or_else(harn_hostlib::HostLeaseStore::default_host);
match store.renew(&host, &args.lease_id, args.ttl_ms) {
Ok(receipt) if receipt.renewed => {
print_success(&receipt, args.json, |_| {
format!("Renewed lease {}", args.lease_id)
});
0
}
Ok(receipt) => {
print_failure_data(
"host_lease_not_owned",
"no matching active lease",
receipt,
args.json,
);
EX_TEMPFAIL
}
Err(error) => print_error("host_lease_renew", &error.to_string(), args.json),
}
}
fn release(store: &harn_hostlib::HostLeaseStore, args: HostLeaseReleaseArgs) -> i32 {
let host = args
.host
.unwrap_or_else(harn_hostlib::HostLeaseStore::default_host);
match store.release(&host, &args.lease_id) {
Ok(receipt) if receipt.released => {
print_success(&receipt, args.json, |_| {
format!("Released lease {}", args.lease_id)
});
0
}
Ok(receipt) => {
print_failure_data(
"host_lease_not_owned",
"no matching active lease",
receipt,
args.json,
);
EX_TEMPFAIL
}
Err(error) => print_error("host_lease_release", &error.to_string(), args.json),
}
}
fn status(store: &harn_hostlib::HostLeaseStore, args: HostLeaseStatusArgs) -> i32 {
let host = args
.host
.unwrap_or_else(harn_hostlib::HostLeaseStore::default_host);
match store.status(&host) {
Ok(state) => {
print_success(&state, args.json, |state| match state.active.as_ref() {
Some(active) => format!(
"Host {} is leased by {} ({}, lease {})",
state.host,
active.owner,
active.priority_class.as_str(),
active.lease_id
),
None => format!("Host {} is available", state.host),
});
0
}
Err(error) => print_error("host_lease_status", &error.to_string(), args.json),
}
}
fn priority(value: HostLeasePriorityArg) -> harn_hostlib::HostLeasePriorityClass {
match value {
HostLeasePriorityArg::Interactive => harn_hostlib::HostLeasePriorityClass::Interactive,
HostLeasePriorityArg::Measurement => harn_hostlib::HostLeasePriorityClass::Measurement,
HostLeasePriorityArg::CiVerify => harn_hostlib::HostLeasePriorityClass::CiVerify,
HostLeasePriorityArg::Deferrable => harn_hostlib::HostLeasePriorityClass::Deferrable,
}
}
fn print_success<T, F>(value: &T, json: bool, human: F)
where
T: Serialize,
F: FnOnce(&T) -> String,
{
if json {
println!(
"{}",
to_string_pretty(&JsonEnvelope::ok(HOST_LEASE_CLI_SCHEMA_VERSION, value))
);
} else {
println!("{}", human(value));
}
}
fn print_failure_data<T: Serialize>(code: &str, message: &str, value: T, json: bool) {
if json {
let envelope = JsonEnvelope {
schema_version: HOST_LEASE_CLI_SCHEMA_VERSION,
ok: false,
data: Some(value),
error: Some(JsonError {
code: code.to_string(),
message: message.to_string(),
details: serde_json::Value::Null,
}),
warnings: Vec::new(),
};
println!("{}", to_string_pretty(&envelope));
} else {
eprintln!("{message}");
}
}
fn print_error(code: &str, message: &str, json: bool) -> i32 {
if json {
let envelope: JsonEnvelope<serde_json::Value> =
JsonEnvelope::err(HOST_LEASE_CLI_SCHEMA_VERSION, code, message);
println!("{}", to_string_pretty(&envelope));
} else {
eprintln!("error: {message}");
}
1
}