Skip to content

Commit e7d2f3b

Browse files
author
Jonathan D.A. Jewell
committed
feat: add http timeouts/retries, config search, and integration tests
1 parent eb4d615 commit e7d2f3b

12 files changed

Lines changed: 176 additions & 31 deletions

File tree

crates/opm-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
1515

1616
[dev-dependencies]
1717
assert_cmd = "2"
18+
mockito = "1"
19+
tempfile = "3"

crates/opm-cli/src/clients/checky_monkey.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::Result;
44

55
use crate::clients::http;
66

7-
pub fn analyze(path: &str, base_url: &str, token: Option<&str>) -> Result<()> {
7+
pub fn analyze(path: &str, base_url: &str, token: Option<&str>, opts: &http::HttpOptions) -> Result<()> {
88
let body = format!("{\"path\":\"{path}\"}");
9-
http::post_json(base_url, "/analyze", token, &body)
9+
http::post_json(base_url, "/analyze", token, &body, opts)
1010
}

crates/opm-cli/src/clients/cicd_hyper_a.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::Result;
44

55
use crate::clients::http;
66

7-
pub fn publish(path: &str, base_url: &str, token: Option<&str>) -> Result<()> {
7+
pub fn publish(path: &str, base_url: &str, token: Option<&str>, opts: &http::HttpOptions) -> Result<()> {
88
let body = format!("{\"path\":\"{path}\"}");
9-
http::post_json(base_url, "/publish", token, &body)
9+
http::post_json(base_url, "/publish", token, &body, opts)
1010
}

crates/opm-cli/src/clients/claim_forge.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::Result;
44

55
use crate::clients::http;
66

7-
pub fn attest(path: &str, base_url: &str, token: Option<&str>) -> Result<()> {
8-
let body = format!("{{\"path\":\"{path}\"}}");
9-
http::post_json(base_url, "/attest", token, &body)
7+
pub fn attest(path: &str, base_url: &str, token: Option<&str>, opts: &http::HttpOptions) -> Result<()> {
8+
let body = format!("{\"path\":\"{path}\"}");
9+
http::post_json(base_url, "/attest", token, &body, opts)
1010
}

crates/opm-cli/src/clients/http.rs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@
33
use anyhow::{Context, Result};
44
use reqwest::blocking::Client;
55
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
6+
use std::time::Duration;
67

7-
pub fn post_json(base: &str, path: &str, token: Option<&str>, body: &str) -> Result<()> {
8-
let client = Client::new();
8+
#[derive(Debug, Clone)]
9+
pub struct HttpOptions {
10+
pub timeout_ms: u64,
11+
pub retries: u32,
12+
pub backoff_ms: u64,
13+
}
14+
15+
pub fn post_json(base: &str, path: &str, token: Option<&str>, body: &str, opts: &HttpOptions) -> Result<()> {
16+
let client = Client::builder()
17+
.timeout(Duration::from_millis(opts.timeout_ms))
18+
.build()
19+
.context("http client")?;
920
let mut headers = HeaderMap::new();
1021
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
1122

@@ -15,16 +26,29 @@ pub fn post_json(base: &str, path: &str, token: Option<&str>, body: &str) -> Res
1526
}
1627

1728
let url = format!("{}{}", base.trim_end_matches('/'), path);
18-
let resp = client
19-
.post(url)
20-
.headers(headers)
21-
.body(body.to_string())
22-
.send()
23-
.context("opm http post")?;
29+
let attempts = opts.retries.saturating_add(1);
30+
for attempt in 0..attempts {
31+
let resp = client
32+
.post(url.clone())
33+
.headers(headers.clone())
34+
.body(body.to_string())
35+
.send();
2436

25-
if !resp.status().is_success() {
26-
return Err(anyhow::anyhow!("http error: {}", resp.status()));
27-
}
37+
match resp {
38+
Ok(resp) if resp.status().is_success() => return Ok(()),
39+
Ok(resp) => {
40+
if attempt + 1 == attempts {
41+
return Err(anyhow::anyhow!("http error: {}", resp.status()));
42+
}
43+
}
44+
Err(err) => {
45+
if attempt + 1 == attempts {
46+
return Err(anyhow::anyhow!("http error: {err}"));
47+
}
48+
}
49+
}
2850

51+
std::thread::sleep(Duration::from_millis(opts.backoff_ms * (attempt as u64 + 1)));
52+
}
2953
Ok(())
3054
}

crates/opm-cli/src/clients/oikos.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::Result;
44

55
use crate::clients::http;
66

7-
pub fn score(package: &str, base_url: &str, token: Option<&str>) -> Result<()> {
7+
pub fn score(package: &str, base_url: &str, token: Option<&str>, opts: &http::HttpOptions) -> Result<()> {
88
let body = format!("{\"package\":\"{package}\"}");
9-
http::post_json(base_url, "/score", token, &body)
9+
http::post_json(base_url, "/score", token, &body, opts)
1010
}

crates/opm-cli/src/clients/palimpsest_license.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::Result;
44

55
use crate::clients::http;
66

7-
pub fn audit(path: &str, base_url: &str, token: Option<&str>) -> Result<()> {
7+
pub fn audit(path: &str, base_url: &str, token: Option<&str>, opts: &http::HttpOptions) -> Result<()> {
88
let body = format!("{\"path\":\"{path}\"}");
9-
http::post_json(base_url, "/audit", token, &body)
9+
http::post_json(base_url, "/audit", token, &body, opts)
1010
}

crates/opm-cli/src/config.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ use serde::Deserialize;
44
use std::fs;
55
use std::path::Path;
66

7+
#[derive(Debug, Deserialize, Clone)]
8+
#[serde(default)]
9+
pub struct HttpConfig {
10+
pub timeout_ms: u64,
11+
pub retries: u32,
12+
pub backoff_ms: u64,
13+
}
14+
15+
impl Default for HttpConfig {
16+
fn default() -> Self {
17+
Self {
18+
timeout_ms: 3000,
19+
retries: 2,
20+
backoff_ms: 200,
21+
}
22+
}
23+
}
24+
725
#[derive(Debug, Deserialize, Clone)]
826
pub struct ServiceConfig {
927
pub base_url: String,
@@ -12,6 +30,8 @@ pub struct ServiceConfig {
1230

1331
#[derive(Debug, Deserialize, Clone)]
1432
pub struct OpmConfig {
33+
#[serde(default)]
34+
pub http: HttpConfig,
1535
pub claim_forge: ServiceConfig,
1636
pub checky_monkey: ServiceConfig,
1737
pub palimpsest_license: ServiceConfig,
@@ -22,6 +42,7 @@ pub struct OpmConfig {
2242
impl OpmConfig {
2343
pub fn example() -> Self {
2444
Self {
45+
http: HttpConfig::default(),
2546
claim_forge: ServiceConfig {
2647
base_url: "http://127.0.0.1:7001".to_string(),
2748
token: None,
@@ -50,4 +71,24 @@ impl OpmConfig {
5071
let cfg = toml::from_str(&data)?;
5172
Ok(cfg)
5273
}
74+
75+
pub fn load() -> anyhow::Result<Self> {
76+
if let Ok(path) = std::env::var("OPM_CONFIG") {
77+
return Self::load_from(path);
78+
}
79+
80+
let local = Path::new("opm.toml");
81+
if local.exists() {
82+
return Self::load_from(local);
83+
}
84+
85+
if let Ok(home) = std::env::var("HOME") {
86+
let path = Path::new(&home).join(".config/opm/opm.toml");
87+
if path.exists() {
88+
return Self::load_from(path);
89+
}
90+
}
91+
92+
Err(anyhow::anyhow!("opm config not found"))
93+
}
5394
}

crates/opm-cli/src/wiring.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,30 @@ use crate::config::OpmConfig;
77

88
pub fn publish(path: &str) -> Result<()> {
99
println!("opm publish (stub) for {path}");
10-
let cfg = OpmConfig::load_from("opm.toml").unwrap_or_else(|_| OpmConfig::example());
11-
clients::claim_forge::attest(path, &cfg.claim_forge.base_url, cfg.claim_forge.token.as_deref())?;
12-
clients::checky_monkey::analyze(path, &cfg.checky_monkey.base_url, cfg.checky_monkey.token.as_deref())?;
13-
clients::palimpsest_license::audit(path, &cfg.palimpsest_license.base_url, cfg.palimpsest_license.token.as_deref())?;
14-
clients::cicd_hyper_a::publish(path, &cfg.cicd_hyper_a.base_url, cfg.cicd_hyper_a.token.as_deref())?;
10+
let cfg = OpmConfig::load().unwrap_or_else(|_| OpmConfig::example());
11+
let opts = clients::http::HttpOptions {
12+
timeout_ms: cfg.http.timeout_ms,
13+
retries: cfg.http.retries,
14+
backoff_ms: cfg.http.backoff_ms,
15+
};
16+
clients::claim_forge::attest(path, &cfg.claim_forge.base_url, cfg.claim_forge.token.as_deref(), &opts)?;
17+
clients::checky_monkey::analyze(path, &cfg.checky_monkey.base_url, cfg.checky_monkey.token.as_deref(), &opts)?;
18+
clients::palimpsest_license::audit(path, &cfg.palimpsest_license.base_url, cfg.palimpsest_license.token.as_deref(), &opts)?;
19+
clients::cicd_hyper_a::publish(path, &cfg.cicd_hyper_a.base_url, cfg.cicd_hyper_a.token.as_deref(), &opts)?;
1520
Ok(())
1621
}
1722

1823
pub fn audit(package: &str) -> Result<()> {
1924
println!("opm audit (stub) for {package}");
20-
let cfg = OpmConfig::load_from("opm.toml").unwrap_or_else(|_| OpmConfig::example());
21-
clients::claim_forge::attest(package, &cfg.claim_forge.base_url, cfg.claim_forge.token.as_deref())?;
22-
clients::checky_monkey::analyze(package, &cfg.checky_monkey.base_url, cfg.checky_monkey.token.as_deref())?;
23-
clients::oikos::score(package, &cfg.oikos.base_url, cfg.oikos.token.as_deref())?;
25+
let cfg = OpmConfig::load().unwrap_or_else(|_| OpmConfig::example());
26+
let opts = clients::http::HttpOptions {
27+
timeout_ms: cfg.http.timeout_ms,
28+
retries: cfg.http.retries,
29+
backoff_ms: cfg.http.backoff_ms,
30+
};
31+
clients::claim_forge::attest(package, &cfg.claim_forge.base_url, cfg.claim_forge.token.as_deref(), &opts)?;
32+
clients::checky_monkey::analyze(package, &cfg.checky_monkey.base_url, cfg.checky_monkey.token.as_deref(), &opts)?;
33+
clients::oikos::score(package, &cfg.oikos.base_url, cfg.oikos.token.as_deref(), &opts)?;
2434
Ok(())
2535
}
2636

crates/opm-cli/tests/audit_flow.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: PMPL-1.0
2+
3+
use assert_cmd::Command;
4+
use mockito::Server;
5+
use std::io::Write;
6+
use tempfile::NamedTempFile;
7+
8+
#[test]
9+
fn audit_hits_pipeline_endpoints() {
10+
let mut server = Server::new();
11+
12+
let _attest = server.mock("POST", "/attest").with_status(200).create();
13+
let _analyze = server.mock("POST", "/analyze").with_status(200).create();
14+
let _score = server.mock("POST", "/score").with_status(200).create();
15+
16+
let mut file = NamedTempFile::new().expect("tmp config");
17+
let cfg = format!(
18+
"[http]\nretries = 0\n\n[claim_forge]\nbase_url = \"{}\"\n\n[checky_monkey]\nbase_url = \"{}\"\n\n[palimpsest_license]\nbase_url = \"{}\"\n\n[cicd_hyper_a]\nbase_url = \"{}\"\n\n[oikos]\nbase_url = \"{}\"\n",
19+
server.url(),
20+
server.url(),
21+
server.url(),
22+
server.url(),
23+
server.url()
24+
);
25+
file.write_all(cfg.as_bytes()).expect("write config");
26+
27+
let mut cmd = Command::cargo_bin("opm").expect("binary builds");
28+
cmd.env("OPM_CONFIG", file.path());
29+
cmd.arg("audit").arg("example-lib");
30+
cmd.assert().success();
31+
}

0 commit comments

Comments
 (0)