use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::path::Path;
use std::time::Duration;
const PROBE_TIMEOUT: Duration = Duration::from_secs(2);
pub(crate) fn validate(rest: Vec<String>) -> anyhow::Result<()> {
let resolve = rest.iter().any(|a| a == "--resolve");
let path = rest
.iter()
.find(|a| a.as_str() != "--resolve")
.ok_or_else(|| anyhow::anyhow!("usage: plecto validate [--resolve] <manifest.toml>"))?
.clone();
match plecto_control::validate_manifest_path(Path::new(&path)) {
Ok(outcome) => {
println!(
"manifest OK: {path} (config version {})",
outcome.config_version
);
for warning in &outcome.warnings {
println!("warning {warning}");
}
note_restart_only_fields(&path)?;
}
Err(e) => anyhow::bail!("manifest INVALID: {path}: {e}"),
}
if !resolve {
return Ok(());
}
match plecto_control::resolve_manifest_path(Path::new(&path)) {
Ok(checks) => {
for check in &checks {
println!(
"filter {} OK: artifact verified ({})",
check.id, check.digest
);
}
Ok(())
}
Err(e) => anyhow::bail!("artifact INVALID: {path}: {e}"),
}
}
fn note_restart_only_fields(path: &str) -> anyhow::Result<()> {
let raw = std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("read {path}: {e}"))?;
let fields = plecto_control::Manifest::from_toml(&raw)
.map_err(|e| anyhow::anyhow!("parse {path}: {e}"))?
.restart_only_fields();
if fields.is_empty() {
return Ok(());
}
println!(
"note: {} — startup-fixed, not part of the config version; changing only these takes a \
restart, not a reload",
fields.join(", ")
);
Ok(())
}
pub(crate) fn conformance(rest: Vec<String>) -> anyhow::Result<()> {
let json = rest.iter().any(|a| a == "--json");
let path = rest
.iter()
.find(|a| a.as_str() != "--json")
.ok_or_else(|| anyhow::anyhow!("usage: plecto conformance <component.wasm> [--json]"))?
.clone();
let bytes = std::fs::read(&path).map_err(|e| anyhow::anyhow!("read {path}: {e}"))?;
let report = plecto_control::run_conformance(&bytes);
if json {
let checks: Vec<serde_json::Value> = report
.checks
.iter()
.map(|c| {
serde_json::json!({
"id": c.id,
"name": c.name,
"passed": c.passed,
"verdict": c.verdict.as_str(),
"detail": c.detail,
})
})
.collect();
println!(
"{}",
serde_json::json!({"conformant": report.is_conformant(), "checks": checks})
);
} else {
for check in &report.checks {
println!(
"[{}] {} — {}",
check.verdict.as_str(),
check.name,
check.detail
);
}
}
if report.is_conformant() {
return Ok(());
}
anyhow::bail!("{path} is not conformant with plecto:filter");
}
pub(crate) fn package(rest: Vec<String>) -> anyhow::Result<()> {
let usage = || {
anyhow::anyhow!(
"usage: plecto package <component.wasm> --key <pkcs8.pem> --out <layout-dir> \
[--sbom <statement.json>]"
)
};
let mut component_path: Option<String> = None;
let mut key_path: Option<String> = None;
let mut out_dir: Option<String> = None;
let mut sbom_path: Option<String> = None;
let mut args = rest.into_iter();
while let Some(arg) = args.next() {
match arg.as_str() {
"--key" => key_path = Some(args.next().ok_or_else(usage)?),
"--out" => out_dir = Some(args.next().ok_or_else(usage)?),
"--sbom" => sbom_path = Some(args.next().ok_or_else(usage)?),
_ if component_path.is_none() && !arg.starts_with('-') => component_path = Some(arg),
_ => return Err(usage()),
}
}
let component_path = component_path.ok_or_else(usage)?;
let key_path = key_path.ok_or_else(usage)?;
let out_dir = out_dir.ok_or_else(usage)?;
let component = std::fs::read(&component_path)
.map_err(|e| anyhow::anyhow!("read {component_path}: {e}"))?;
let report = plecto_control::run_conformance(&component);
if !report.is_conformant() {
for check in report.checks.iter().filter(|c| !c.passed) {
eprintln!(
"[{}] {} — {}",
check.verdict.as_str(),
check.name,
check.detail
);
}
anyhow::bail!("{component_path} is not conformant with plecto:filter — nothing packaged");
}
let key_pem = zeroize::Zeroizing::new(
std::fs::read(&key_path).map_err(|e| anyhow::anyhow!("read {key_path}: {e}"))?,
);
let signer = plecto_control::PemSigner::from_private_key_pem(&key_pem)
.map_err(|e| anyhow::anyhow!("load signing key {key_path}: {e}"))?;
let sbom = match &sbom_path {
Some(path) => std::fs::read(path).map_err(|e| anyhow::anyhow!("read {path}: {e}"))?,
None => plecto_control::bound_sbom(&component),
};
let component_signature = signer.sign(&component)?;
let sbom_signature = signer.sign(&sbom)?;
let digest = plecto_control::oci::write_layout(
Path::new(&out_dir),
&plecto_control::ResolvedArtifact {
component,
component_signature,
sbom,
sbom_signature,
},
)
.map_err(|e| anyhow::anyhow!("write OCI layout {out_dir}: {e}"))?;
println!("{digest}");
Ok(())
}
pub(crate) fn healthz(rest: Vec<String>) -> anyhow::Result<()> {
let usage = || {
anyhow::anyhow!(
"usage: plecto healthz [--live] [--admin-addr <host:port>] [<manifest.toml>]"
)
};
let mut live = false;
let mut admin_addr: Option<String> = None;
let mut manifest: Option<String> = None;
let mut args = rest.into_iter();
while let Some(arg) = args.next() {
match arg.as_str() {
"--live" => live = true,
"--admin-addr" => admin_addr = Some(args.next().ok_or_else(usage)?),
_ if manifest.is_none() && !arg.starts_with('-') => manifest = Some(arg),
_ => return Err(usage()),
}
}
let addr = match admin_addr {
Some(addr) => addr,
None => {
let path = manifest.ok_or_else(usage)?;
let raw =
std::fs::read_to_string(&path).map_err(|e| anyhow::anyhow!("read {path}: {e}"))?;
let parsed = plecto_control::Manifest::from_toml(&raw)
.map_err(|e| anyhow::anyhow!("parse {path}: {e}"))?;
parsed.observability.admin_addr.ok_or_else(|| {
anyhow::anyhow!(
"{path} sets no [observability] admin_addr — the admin endpoint is off, \
nothing to probe"
)
})?
}
};
let probe_path = if live { "/healthz" } else { "/readyz" };
let status = probe(&addr, probe_path)
.map_err(|e| anyhow::anyhow!("unhealthy: GET {addr}{probe_path}: {e}"))?;
if (200..300).contains(&status) {
println!("healthy: GET {probe_path} -> {status}");
return Ok(());
}
anyhow::bail!("unhealthy: GET {probe_path} -> {status}")
}
fn probe(addr: &str, path: &str) -> anyhow::Result<u16> {
let sock = addr
.to_socket_addrs()
.map_err(|e| anyhow::anyhow!("resolve {addr}: {e}"))?
.next()
.ok_or_else(|| anyhow::anyhow!("resolve {addr}: no address"))?;
let mut stream = TcpStream::connect_timeout(&sock, PROBE_TIMEOUT)?;
stream.set_read_timeout(Some(PROBE_TIMEOUT))?;
stream.set_write_timeout(Some(PROBE_TIMEOUT))?;
stream.write_all(
format!("GET {path} HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n").as_bytes(),
)?;
let mut buf = [0u8; 512];
let mut filled = 0;
while filled < buf.len() {
let n = stream.read(&mut buf[filled..])?;
if n == 0 {
break;
}
filled += n;
if buf[..filled].windows(2).any(|w| w == b"\r\n") {
break;
}
}
let head = str::from_utf8(&buf[..filled]).unwrap_or_default();
let status = head
.split_whitespace()
.nth(1)
.and_then(|code| code.parse::<u16>().ok())
.ok_or_else(|| anyhow::anyhow!("malformed response status line"))?;
Ok(status)
}
pub(crate) fn new_filter(rest: Vec<String>) -> anyhow::Result<()> {
let usage = || anyhow::anyhow!("usage: plecto new-filter --lang <lang> <name>");
let lang_idx = rest.iter().position(|a| a == "--lang");
let lang = lang_idx.and_then(|i| rest.get(i + 1)).ok_or_else(usage)?;
let lang_value_idx = lang_idx.map(|i| i + 1);
let name = rest
.iter()
.enumerate()
.find(|(i, a)| a.as_str() != "--lang" && Some(*i) != lang_value_idx)
.map(|(_, a)| a.as_str())
.ok_or_else(usage)?;
let project_root = std::env::current_dir()
.map_err(|e| anyhow::anyhow!("resolve the current directory: {e}"))?;
crate::new_filter::run(lang, name, &project_root)
}