| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | | use super::{open, ui}; |
| 8 | | use anyhow::{anyhow, bail, Context, Result}; |
| 9 | | use oxy_core::approval::{SignedApproval, Statement}; |
| 10 | | use oxy_core::hash::ObjectRef; |
| 11 | | use serde_json::Value; |
| 12 | |
|
| 13 | |
|
| 14 | |
|
| 15 | 3× | fn run_url(url: &str) -> Result<(String, String, i64, String)> { |
| 16 | 3× | let url = url.trim_end_matches('/'); |
| 17 | 3× | let (repo_url, n) = url |
| 18 | 3× | .rsplit_once("/flow/") |
| 19 | 3× | .ok_or_else(|| anyhow!("{url} is not a run URL (…/OWNER/NAME/flow/N)"))?; |
| 20 | 2× | let number: i64 = n |
| 21 | 2× | .parse() |
| 22 | 2× | .map_err(|_| anyhow!("{url} is not a run URL (…/OWNER/NAME/flow/N)"))?; |
| 23 | 1× | let (rest, name) = repo_url |
| 24 | 1× | .rsplit_once('/') |
| 25 | 1× | .ok_or_else(|| anyhow!("{url} is not a run URL"))?; |
| 26 | 1× | let (base, owner) = rest |
| 27 | 1× | .rsplit_once('/') |
| 28 | 1× | .ok_or_else(|| anyhow!("{url} is not a run URL"))?; |
| 29 | 1× | Ok(( |
| 30 | 1× | base.to_string(), |
| 31 | 1× | format!("{owner}/{name}"), |
| 32 | 1× | number, |
| 33 | 1× | repo_url.to_string(), |
| 34 | 1× | )) |
| 35 | 3× | } |
| 36 | |
|
| 37 | 0× | fn to_hex(b: &[u8]) -> String { |
| 38 | 0× | b.iter().map(|x| format!("{x:02x}")).collect() |
| 39 | 0× | } |
| 40 | |
|
| 41 | 0× | fn oref(v: &Value, key: &str) -> Result<ObjectRef> { |
| 42 | 0× | v.get(key) |
| 43 | 0× | .and_then(Value::as_str) |
| 44 | 0× | .and_then(ObjectRef::from_hex) |
| 45 | 0× | .ok_or_else(|| anyhow!("the hub's statement has no valid {key}")) |
| 46 | 0× | } |
| 47 | |
|
| 48 | 0× | pub fn seal(url: &str, job: &str, reject: bool, as_json: bool) -> Result<i32> { |
| 49 | 0× | let repo = open()?; |
| 50 | 0× | let (base, slug, number, repo_url) = run_url(url)?; |
| 51 | 0× | let options = repo.options_for_url(&repo_url); |
| 52 | 0× | let api = format!("{base}/api/v1/repos/{slug}/flow/runs/{number}/jobs/{job}/approval"); |
| 53 | 0× | let body = oxy_core::protocol::http_get(&api, &options).map_err(|e| anyhow!("{e}"))?; |
| 54 | 0× | let v: Value = serde_json::from_slice(&body).context("the hub did not answer with JSON")?; |
| 55 | 0× | let repo_hex = v.get("repo").and_then(Value::as_str).unwrap_or(""); |
| 56 | 0× | let mine = to_hex(&repo.id()?); |
| 57 | 0× | if repo_hex != mine { |
| 58 | 0× | bail!("run #{number} belongs to repository {repo_hex}, not this checkout's ({mine}): run oxy seal --flow from a checkout of {slug}"); |
| 59 | 0× | } |
| 60 | 0× | let mut repo_id = [0u8; 16]; |
| 61 | 0× | for (i, b) in repo_id.iter_mut().enumerate() { |
| 62 | 0× | *b = u8::from_str_radix(&repo_hex[i * 2..i * 2 + 2], 16)?; |
| 63 | | } |
| 64 | 0× | let inputs = v |
| 65 | 0× | .get("inputs") |
| 66 | 0× | .and_then(Value::as_array) |
| 67 | 0× | .map(|a| { |
| 68 | 0× | a.iter() |
| 69 | 0× | .filter_map(|x| x.as_str().and_then(ObjectRef::from_hex)) |
| 70 | 0× | .collect::<Vec<_>>() |
| 71 | 0× | }) |
| 72 | 0× | .unwrap_or_default(); |
| 73 | 0× | let statement = Statement { |
| 74 | 0× | repo: repo_id, |
| 75 | 0× | run: v |
| 76 | 0× | .get("run") |
| 77 | 0× | .and_then(Value::as_str) |
| 78 | 0× | .unwrap_or(url) |
| 79 | 0× | .to_string(), |
| 80 | 0× | job: job.to_string(), |
| 81 | 0× | job_hash: oref(&v, "job_hash")?, |
| 82 | 0× | state: oref(&v, "state")?, |
| 83 | 0× | inputs, |
| 84 | 0× | environment: v |
| 85 | 0× | .get("environment") |
| 86 | 0× | .and_then(Value::as_str) |
| 87 | 0× | .unwrap_or("") |
| 88 | 0× | .to_string(), |
| 89 | 0× | approve: !reject, |
| 90 | | }; |
| 91 | 0× | let (key, sk) = repo |
| 92 | 0× | .secret_key()? |
| 93 | 0× | .ok_or_else(|| anyhow!("no signing key: create one with `oxy identity new`"))?; |
| 94 | 0× | let me = repo.identity(&key)?; |
| 95 | 0× | let signed = SignedApproval::sign(&statement, &me, &sk); |
| 96 | 0× | let post = format!("{base}/api/v1/repos/{slug}/flow/runs/{number}/jobs/{job}/seal"); |
| 97 | 0× | let payload = serde_json::json!({ "signed": to_hex(&signed.encode()) }).to_string(); |
| 98 | 0× | let (code, answer) = oxy_core::protocol::http_json("POST", &post, Some(&payload), &options) |
| 99 | 0× | .map_err(|e| anyhow!("{e}"))?; |
| 100 | 0× | if !(200..300).contains(&code) { |
| 101 | 0× | let why = serde_json::from_str::<Value>(&answer) |
| 102 | 0× | .ok() |
| 103 | 0× | .and_then(|v| v.get("error").and_then(Value::as_str).map(String::from)); |
| 104 | 0× | bail!( |
| 105 | | "the hub refused the seal (HTTP {code}): {}", |
| 106 | 0× | why.unwrap_or(answer) |
| 107 | | ); |
| 108 | 0× | } |
| 109 | 0× | if as_json { |
| 110 | 0× | println!("{answer}"); |
| 111 | 0× | } else { |
| 112 | 0× | let verb = if reject { "Rejected" } else { "Approved" }; |
| 113 | 0× | println!( |
| 114 | | "{} {job} of run #{number} with key {}", |
| 115 | 0× | ui::done(verb), |
| 116 | 0× | &key.to_hex()[..12] |
| 117 | | ); |
| 118 | 0× | println!( |
| 119 | | " state {} · job {}", |
| 120 | 0× | &statement.state.to_hex()[..12], |
| 121 | 0× | &statement.job_hash.to_hex()[..12] |
| 122 | | ); |
| 123 | 0× | for i in &statement.inputs { |
| 124 | 0× | println!(" takes {}", &i.to_hex()[..12]); |
| 125 | 0× | } |
| 126 | 0× | if !statement.environment.is_empty() { |
| 127 | 0× | println!(" for environment {}", statement.environment); |
| 128 | 0× | } |
| 129 | | } |
| 130 | 0× | Ok(0) |
| 131 | 0× | } |
| 132 | |
|
| 133 | | #[cfg(test)] |
| 134 | | mod tests { |
| 135 | | #[test] |
| 136 | 1× | fn run_urls_are_read() { |
| 137 | 1× | let (base, slug, n, repo) = |
| 138 | 1× | super::run_url("https://oxygenhub.io/andymccall/oxygenhub-gcp/flow/8/").unwrap(); |
| 139 | 1× | assert_eq!( |
| 140 | 1× | (base.as_str(), slug.as_str(), n, repo.as_str()), |
| 141 | | ( |
| 142 | | "https://oxygenhub.io", |
| 143 | | "andymccall/oxygenhub-gcp", |
| 144 | | 8, |
| 145 | | "https://oxygenhub.io/andymccall/oxygenhub-gcp" |
| 146 | | ) |
| 147 | | ); |
| 148 | 1× | assert!(super::run_url("https://oxygenhub.io/andymccall/x/proposal/3").is_err()); |
| 149 | 1× | assert!(super::run_url("https://oxygenhub.io/andymccall/x/flow/abc").is_err()); |
| 150 | 1× | } |
| 151 | | } |