| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | | use super::json; |
| 5 | | use super::open; |
| 6 | | use super::ui; |
| 7 | | use anyhow::{bail, Result}; |
| 8 | | use oxy_core::receipt::SignedReceipt; |
| 9 | | use oxy_core::repo::ReceiptInfo; |
| 10 | |
|
| 11 | 1× | fn status(problem: Option<&str>, revoked: bool) -> String { |
| 12 | 1× | match (problem, revoked) { |
| 13 | 1× | (Some(p), _) => ui::err(format!("{} {p}", ui::glyph("✘", "x"))), |
| 14 | 0× | (None, true) => ui::warn(format!("{} runner key revoked", ui::glyph("!", "!"))), |
| 15 | 0× | (None, false) => ui::ok(format!("{} signed", ui::glyph("✔", "v"))), |
| 16 | | } |
| 17 | 1× | } |
| 18 | |
|
| 19 | 1× | fn line(sr: &SignedReceipt, problem: Option<&str>, revoked: bool) -> String { |
| 20 | 1× | let r = &sr.receipt; |
| 21 | 1× | let result = if r.passed() { |
| 22 | 0× | ui::ok(format!("{} passed", ui::glyph("●", "*"))) |
| 23 | | } else { |
| 24 | 1× | ui::err(format!("{} exit {}", ui::glyph("●", "*"), r.exit_code)) |
| 25 | | }; |
| 26 | 1× | format!( |
| 27 | | "{} {}{}{}{}{}", |
| 28 | | result, |
| 29 | 1× | ui::subject(&r.job_name), |
| 30 | 1× | ui::sep() + &ui::dim(format!("{:.1} s", r.duration_ms as f64 / 1000.0)), |
| 31 | 1× | ui::sep() + &ui::author(&sr.runner.name), |
| 32 | 1× | ui::sep() + &status(problem, revoked), |
| 33 | 1× | ui::sep() + &ui::dim(ui::ago(sr.attest.hlc.wall_ms)), |
| 34 | | ) |
| 35 | 1× | } |
| 36 | |
|
| 37 | 3× | fn to_json( |
| 38 | 3× | attest: Option<String>, |
| 39 | 3× | sr: &SignedReceipt, |
| 40 | 3× | problem: Option<&str>, |
| 41 | 3× | revoked: bool, |
| 42 | 3× | ) -> String { |
| 43 | 3× | let r = &sr.receipt; |
| 44 | 6× | let refs = |v: &[oxy_core::hash::ObjectRef]| json::arr(v.iter().map(|h| json::s(&h.to_hex()))); |
| 45 | 3× | json::obj([ |
| 46 | 3× | ("attest", json::opt(attest.as_ref(), |a| json::s(a))), |
| 47 | 3× | ("receipt", json::s(&r.hash().to_hex())), |
| 48 | 3× | ("state", json::s(&sr.state().to_hex())), |
| 49 | 3× | ("run", json::s(sr.run())), |
| 50 | 3× | ("job", json::s(&r.job_name)), |
| 51 | 3× | ("exit_code", json::n(r.exit_code)), |
| 52 | 3× | ("duration_ms", json::n(r.duration_ms)), |
| 53 | 3× | ("pipeline", json::s(&r.pipeline_hash.to_hex())), |
| 54 | 3× | ("scope", json::s(&r.scope_tree_hash.to_hex())), |
| 55 | 3× | ("environment", json::s(&r.environment.to_hex())), |
| 56 | 3× | ("inputs", refs(&r.inputs)), |
| 57 | 3× | ("outputs", refs(&r.outputs)), |
| 58 | 3× | ("runner", json::s(&sr.runner.hash().to_hex())), |
| 59 | 3× | ("runner_name", json::s(&sr.runner.name)), |
| 60 | 3× | ("made_ms", json::n(sr.attest.hlc.wall_ms)), |
| 61 | 3× | ("valid", json::b(problem.is_none())), |
| 62 | 3× | ("problem", json::opt(problem, json::s)), |
| 63 | 3× | ("revoked", json::b(revoked)), |
| 64 | | ]) |
| 65 | 3× | } |
| 66 | |
|
| 67 | 1× | fn detail(sr: &SignedReceipt, problem: Option<&str>, revoked: bool) { |
| 68 | 1× | let r = &sr.receipt; |
| 69 | 1× | println!("{}", line(sr, problem, revoked)); |
| 70 | 1× | println!( |
| 71 | | "{}", |
| 72 | 1× | ui::kv("run", if sr.run().is_empty() { "-" } else { sr.run() }) |
| 73 | | ); |
| 74 | 1× | println!( |
| 75 | | "{}", |
| 76 | 1× | ui::kv("state", ui::hash(format!("#{}", sr.state().short(12)))) |
| 77 | | ); |
| 78 | 1× | println!( |
| 79 | | "{}", |
| 80 | 1× | ui::kv("receipt", ui::hash(format!("#{}", r.hash().short(12)))) |
| 81 | | ); |
| 82 | 1× | println!( |
| 83 | | "{}", |
| 84 | 1× | ui::kv("pipeline", ui::hash(r.pipeline_hash.short(12))) |
| 85 | | ); |
| 86 | 1× | println!("{}", ui::kv("scope", ui::hash(r.scope_tree_hash.short(12)))); |
| 87 | 1× | println!( |
| 88 | | "{}", |
| 89 | 1× | ui::kv("environment", ui::hash(r.environment.short(12))) |
| 90 | | ); |
| 91 | 1× | println!( |
| 92 | | "{}", |
| 93 | 1× | ui::kv( |
| 94 | 1× | "runner", |
| 95 | 1× | format!("{} {}", sr.runner.name, ui::dim(sr.runner.hash().short(12))) |
| 96 | | ) |
| 97 | | ); |
| 98 | 2× | for (what, v) in [("input", &r.inputs), ("output", &r.outputs)] { |
| 99 | 2× | for h in v.iter() { |
| 100 | 0× | println!("{}", ui::kv(what, ui::hash(h.short(12)))); |
| 101 | 0× | } |
| 102 | | } |
| 103 | 1× | } |
| 104 | |
|
| 105 | |
|
| 106 | 2× | pub fn list(rev: Option<&str>, as_json: bool) -> Result<i32> { |
| 107 | 2× | let repo = open()?; |
| 108 | 2× | let heads = match rev { |
| 109 | 0× | Some(r) => repo.resolve_heads(r)?, |
| 110 | 2× | None => repo.bookmark_heads()?, |
| 111 | | }; |
| 112 | 2× | let found = repo.receipts(&heads)?; |
| 113 | 2× | if as_json { |
| 114 | 2× | println!( |
| 115 | | "{}", |
| 116 | 2× | json::arr(found.iter().map(|i| to_json( |
| 117 | 1× | Some(i.attest.to_hex()), |
| 118 | 1× | &i.signed, |
| 119 | 1× | i.problem.as_deref(), |
| 120 | 1× | i.revoked |
| 121 | | ))) |
| 122 | | ); |
| 123 | 2× | return Ok(0); |
| 124 | 0× | } |
| 125 | 0× | if found.is_empty() { |
| 126 | 0× | println!("{}", ui::dim("No receipts for this state.")); |
| 127 | 0× | } |
| 128 | 0× | for i in &found { |
| 129 | 0× | println!( |
| 130 | 0× | "{} {}", |
| 131 | 0× | ui::hash(format!("#{}", i.attest.short(12))), |
| 132 | 0× | line(&i.signed, i.problem.as_deref(), i.revoked) |
| 133 | 0× | ); |
| 134 | 0× | } |
| 135 | 0× | Ok(0) |
| 136 | 2× | } |
| 137 | |
|
| 138 | 2× | fn find(repo: &oxy_core::repo::Repo, prefix: &str, rev: Option<&str>) -> Result<ReceiptInfo> { |
| 139 | 2× | let heads = match rev { |
| 140 | 0× | Some(r) => repo.resolve_heads(r)?, |
| 141 | 2× | None => repo.bookmark_heads()?, |
| 142 | | }; |
| 143 | 2× | let p = prefix.trim_start_matches('#').to_ascii_lowercase(); |
| 144 | 2× | let mut hits: Vec<ReceiptInfo> = repo |
| 145 | 2× | .receipts(&heads)? |
| 146 | 2× | .into_iter() |
| 147 | 2× | .filter(|i| i.attest.to_hex().starts_with(&p)) |
| 148 | 2× | .collect(); |
| 149 | 2× | match hits.len() { |
| 150 | 2× | 1 => Ok(hits.remove(0)), |
| 151 | 0× | 0 => bail!("no receipt #{p} for this state; `oxy receipt list` shows them"), |
| 152 | 0× | n => bail!("#{p} matches {n} receipts; give more of the hash"), |
| 153 | | } |
| 154 | 2× | } |
| 155 | |
|
| 156 | |
|
| 157 | 1× | pub fn show(prefix: &str, rev: Option<&str>, as_json: bool) -> Result<i32> { |
| 158 | 1× | let repo = open()?; |
| 159 | 1× | let i = find(&repo, prefix, rev)?; |
| 160 | 1× | if as_json { |
| 161 | 1× | println!( |
| 162 | 1× | "{}", |
| 163 | 1× | to_json( |
| 164 | 1× | Some(i.attest.to_hex()), |
| 165 | 1× | &i.signed, |
| 166 | 1× | i.problem.as_deref(), |
| 167 | 1× | i.revoked |
| 168 | 1× | ) |
| 169 | 1× | ); |
| 170 | 1× | } else { |
| 171 | 0× | detail(&i.signed, i.problem.as_deref(), i.revoked); |
| 172 | 0× | } |
| 173 | 1× | Ok(if i.problem.is_some() { 1 } else { 0 }) |
| 174 | 1× | } |
| 175 | |
|
| 176 | |
|
| 177 | 1× | pub fn export(prefix: &str, file: &str, rev: Option<&str>) -> Result<i32> { |
| 178 | 1× | let repo = open()?; |
| 179 | 1× | let i = find(&repo, prefix, rev)?; |
| 180 | 1× | std::fs::write(file, i.signed.encode())?; |
| 181 | 1× | println!( |
| 182 | | "{} {} {}", |
| 183 | 1× | ui::done("Exported receipt"), |
| 184 | 1× | ui::hash(format!("#{}", i.attest.short(12))), |
| 185 | 1× | ui::path(file) |
| 186 | | ); |
| 187 | 1× | Ok(0) |
| 188 | 1× | } |
| 189 | |
|
| 190 | 4× | fn read(file: &str) -> Result<SignedReceipt> { |
| 191 | 4× | let bytes = std::fs::read(file).map_err(|e| anyhow::anyhow!("{file}: {e}"))?; |
| 192 | 4× | SignedReceipt::decode(&bytes).map_err(|e| anyhow::anyhow!("{file}: not a signed receipt: {e}")) |
| 193 | 4× | } |
| 194 | |
|
| 195 | |
|
| 196 | 2× | pub fn verify(file: &str, as_json: bool) -> Result<i32> { |
| 197 | 2× | let sr = read(file)?; |
| 198 | 2× | let problem = sr.verify().err().map(|e| e.to_string()); |
| 199 | 2× | if as_json { |
| 200 | 1× | println!("{}", to_json(None, &sr, problem.as_deref(), false)); |
| 201 | 1× | } else { |
| 202 | 1× | detail(&sr, problem.as_deref(), false); |
| 203 | 1× | } |
| 204 | 2× | Ok(if problem.is_some() { 1 } else { 0 }) |
| 205 | 2× | } |
| 206 | |
|
| 207 | |
|
| 208 | 2× | pub fn import(file: &str) -> Result<i32> { |
| 209 | 2× | let repo = open()?; |
| 210 | 2× | let sr = read(file)?; |
| 211 | 2× | let r = repo.add_receipt(&sr)?; |
| 212 | 1× | println!( |
| 213 | | "{} {} {} {} {}", |
| 214 | 1× | ui::done("Kept receipt"), |
| 215 | 1× | ui::hash(format!("#{}", r.short(12))), |
| 216 | 1× | ui::subject(&sr.receipt.job_name), |
| 217 | 1× | ui::dim("by"), |
| 218 | 1× | ui::author(&sr.runner.name) |
| 219 | | ); |
| 220 | 1× | Ok(0) |
| 221 | 2× | } |