1use std::fs::File;
2use std::io::Read;
3use std::{collections::HashMap, path::PathBuf};
4
5use anyhow::{anyhow, bail, Context, Result};
6use clap::{Parser, Subcommand};
7use nkeys::KeyPairType;
8use provider_archive::ProviderArchive;
9use serde_json::json;
10use tracing::warn;
11use wash_lib::cli::par::{
12 convert_error, create_provider_archive, detect_arch, insert_provider_binary,
13};
14use wash_lib::cli::{extract_keypair, inspect, par, CommandOutput, OutputKind};
15
16const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b];
17
18#[derive(Debug, Clone, Subcommand)]
19pub enum ParCliCommand {
20 #[clap(name = "create")]
22 Create(CreateCommand),
23 #[clap(name = "inspect")]
25 Inspect(InspectCommand),
26 #[clap(name = "insert")]
28 Insert(InsertCommand),
29}
30
31#[derive(Parser, Debug, Clone)]
32pub struct CreateCommand {
33 #[clap(short = 'v', long = "vendor")]
35 vendor: String,
36
37 #[clap(short = 'r', long = "revision")]
39 revision: Option<i32>,
40
41 #[clap(long = "version")]
43 version: Option<String>,
44
45 #[clap(
47 short = 'j',
48 long = "schema",
49 env = "WASH_JSON_SCHEMA",
50 hide_env_values = true
51 )]
52 schema: Option<PathBuf>,
53
54 #[clap(
56 short = 'd',
57 long = "directory",
58 env = "WASH_KEYS",
59 hide_env_values = true
60 )]
61 directory: Option<PathBuf>,
62
63 #[clap(
65 short = 'i',
66 long = "issuer",
67 env = "WASH_ISSUER_KEY",
68 hide_env_values = true
69 )]
70 issuer: Option<String>,
71
72 #[clap(
74 short = 's',
75 long = "subject",
76 env = "WASH_SUBJECT_KEY",
77 hide_env_values = true
78 )]
79 subject: Option<String>,
80
81 #[clap(short = 'n', long = "name")]
83 name: String,
84
85 #[clap(short = 'a', long = "arch", default_value_t = detect_arch())]
87 arch: String,
88
89 #[clap(short = 'b', long = "binary")]
91 binary: String,
92
93 #[clap(long = "destination")]
95 destination: Option<String>,
96
97 #[clap(long = "compress")]
99 compress: bool,
100
101 #[clap(long = "disable-keygen")]
103 disable_keygen: bool,
104
105 #[clap(long = "wit-directory", env = "WIT_DIR")]
107 wit_dir: Option<PathBuf>,
108}
109
110#[derive(Parser, Debug, Clone)]
111pub struct InspectCommand {
112 #[clap(name = "archive")]
114 archive: String,
115
116 #[clap(short = 'd', long = "digest")]
118 digest: Option<String>,
119
120 #[clap(long = "allow-latest")]
122 allow_latest: bool,
123
124 #[clap(
126 short = 'u',
127 long = "user",
128 env = "WASH_REG_USER",
129 hide_env_values = true
130 )]
131 user: Option<String>,
132
133 #[clap(
135 short = 'p',
136 long = "password",
137 env = "WASH_REG_PASSWORD",
138 hide_env_values = true
139 )]
140 password: Option<String>,
141
142 #[clap(long = "insecure")]
144 insecure: bool,
145
146 #[clap(long = "insecure-skip-tls-verify")]
148 pub insecure_skip_tls_verify: bool,
149
150 #[clap(long = "no-cache")]
152 no_cache: bool,
153}
154
155#[derive(Parser, Debug, Clone)]
156pub struct InsertCommand {
157 #[clap(name = "archive")]
159 archive: String,
160
161 #[clap(short = 'a', long = "arch", default_value_t = detect_arch())]
163 arch: String,
164
165 #[clap(short = 'b', long = "binary")]
167 binary: String,
168
169 #[clap(
171 short = 'd',
172 long = "directory",
173 env = "WASH_KEYS",
174 hide_env_values = true
175 )]
176 directory: Option<PathBuf>,
177
178 #[clap(
180 short = 'i',
181 long = "issuer",
182 env = "WASH_ISSUER_KEY",
183 hide_env_values = true
184 )]
185 issuer: Option<String>,
186
187 #[clap(
189 short = 's',
190 long = "subject",
191 env = "WASH_SUBJECT_KEY",
192 hide_env_values = true
193 )]
194 subject: Option<String>,
195
196 #[clap(long = "disable-keygen")]
198 disable_keygen: bool,
199}
200
201impl From<InspectCommand> for inspect::InspectCliCommand {
202 fn from(cmd: InspectCommand) -> Self {
203 inspect::InspectCliCommand {
204 target: cmd.archive,
205 jwt_only: false,
206 wit: false,
207 digest: cmd.digest,
208 allow_latest: cmd.allow_latest,
209 user: cmd.user,
210 password: cmd.password,
211 insecure: cmd.insecure,
212 insecure_skip_tls_verify: cmd.insecure_skip_tls_verify,
213 no_cache: cmd.no_cache,
214 }
215 }
216}
217
218impl From<CreateCommand> for par::ParCreateArgs {
219 fn from(cmd: CreateCommand) -> Self {
220 par::ParCreateArgs {
221 vendor: cmd.vendor,
222 revision: cmd.revision,
223 version: cmd.version,
224 schema: cmd.schema,
225 name: cmd.name,
226 arch: cmd.arch,
227 }
228 }
229}
230
231pub async fn handle_command(
232 command: ParCliCommand,
233 output_kind: OutputKind,
234) -> Result<CommandOutput> {
235 match command {
236 ParCliCommand::Create(cmd) => handle_create(cmd, output_kind).await,
237 ParCliCommand::Inspect(cmd) => {
238 warn!("par inspect will be deprecated in future versions. Use inspect instead.");
239 inspect::handle_command(cmd, output_kind).await
240 }
241 ParCliCommand::Insert(cmd) => handle_insert(cmd, output_kind).await,
242 }
243}
244
245pub async fn handle_create(cmd: CreateCommand, output_kind: OutputKind) -> Result<CommandOutput> {
247 let mut f = File::open(cmd.binary.clone())
248 .with_context(|| format!("failed to load binary [{}]", &cmd.binary))?;
249 let mut lib = Vec::new();
250 f.read_to_end(&mut lib)?;
251
252 let issuer = extract_keypair(
253 cmd.issuer.as_deref(),
254 Some(&cmd.binary),
255 cmd.directory.clone(),
256 KeyPairType::Account,
257 cmd.disable_keygen,
258 output_kind,
259 )?;
260 let subject = extract_keypair(
261 cmd.subject.as_deref(),
262 Some(&cmd.binary),
263 cmd.directory.clone(),
264 KeyPairType::Service,
265 cmd.disable_keygen,
266 output_kind,
267 )?;
268
269 let extension = if cmd.compress { ".par.gz" } else { ".par" };
270 let outfile = match cmd.destination.clone() {
271 Some(path) => path,
272 None => format!(
273 "{}{}",
274 PathBuf::from(&cmd.binary)
275 .file_stem()
276 .unwrap()
277 .to_str()
278 .unwrap(),
279 extension
280 ),
281 };
282
283 let wit_interface_bytes = match cmd.wit_dir.as_ref() {
284 Some(dir) => {
285 let mut resolve = wit_parser::Resolve::default();
286 let (package_id, _paths) = resolve
287 .push_dir(dir)
288 .with_context(|| format!("failed to add WIT directory @ [{}]", dir.display()))?;
289
290 let encoded = wit_component::encode(&resolve, package_id)
291 .context("Failed to encode WIT package")?;
292
293 Some(encoded)
294 }
295 None => None,
296 };
297
298 let compress = cmd.compress;
299 let mut par = create_provider_archive(cmd.into(), &lib, wit_interface_bytes.as_deref())
300 .context("failed to create provider archive with built provider")?;
301 par.write(&outfile, &issuer, &subject, compress)
302 .await
303 .map_err(|e| anyhow!("{e}"))
304 .with_context(|| {
305 format!(
306 "Error writing PAR. Please ensure directory {:?} exists",
307 PathBuf::from(&outfile).parent().unwrap(),
308 )
309 })?;
310
311 let mut map = HashMap::new();
312 map.insert("file".to_string(), json!(outfile));
313 Ok(CommandOutput::new(
314 format!("Successfully created archive {outfile}"),
315 map,
316 ))
317}
318
319pub async fn handle_insert(cmd: InsertCommand, output_kind: OutputKind) -> Result<CommandOutput> {
321 let mut buf = Vec::new();
322 let mut f = File::open(cmd.archive.clone())
323 .with_context(|| format!("failed to load provider archive [{}]", &cmd.archive))?;
324 f.read_to_end(&mut buf)?;
325
326 let mut f = File::open(cmd.binary.clone())
327 .with_context(|| format!("failed to load binary [{}]", &cmd.archive))?;
328 let mut lib = Vec::new();
329 f.read_to_end(&mut lib)?;
330
331 let issuer = extract_keypair(
332 cmd.issuer.as_deref(),
333 Some(&cmd.binary),
334 cmd.directory.clone(),
335 KeyPairType::Account,
336 cmd.disable_keygen,
337 output_kind,
338 )?;
339 let subject = extract_keypair(
340 cmd.subject.as_deref(),
341 Some(&cmd.binary),
342 cmd.directory.clone(),
343 KeyPairType::Service,
344 cmd.disable_keygen,
345 output_kind,
346 )?;
347
348 let mut par = ProviderArchive::try_load(&buf)
349 .await
350 .map_err(convert_error)?;
351
352 par = insert_provider_binary(cmd.arch, &lib, par).await?;
353 par.write(&cmd.archive, &issuer, &subject, is_compressed(&buf)?)
354 .await
355 .map_err(convert_error)?;
356
357 let mut map = HashMap::new();
358 map.insert("file".to_string(), json!(cmd.archive));
359 Ok(CommandOutput::new(
360 format!(
361 "Successfully inserted {} into archive {}",
362 cmd.binary, cmd.archive
363 ),
364 map,
365 ))
366}
367
368fn is_compressed(input: &[u8]) -> Result<bool> {
370 if input.len() < 2 {
371 bail!("Not enough bytes to be a valid PAR file");
372 }
373 Ok(input[0..2] == GZIP_MAGIC)
374}
375
376#[cfg(test)]
377mod test {
378 use super::*;
379
380 #[derive(Parser, Debug)]
381 struct Cmd {
382 #[clap(subcommand)]
383 par: ParCliCommand,
384 }
385
386 #[test]
389 fn test_par_create_comprehensive() {
390 const ISSUER: &str = "SAAJLQZDZO57THPTIIEELEY7FJYOJZQWQD7FF4J67TUYTSCOXTF7R4Y3VY";
391 const SUBJECT: &str = "SVAH7IN6QE6XODCGIIWZQDZ5LNSSS4FNEO6SNHZSSASW4BBBKSZ6KWTKWY";
392 let create_long: Cmd = clap::Parser::try_parse_from([
393 "par",
394 "create",
395 "--arch",
396 "x86_64-testrunner",
397 "--binary",
398 "./testrunner.so",
399 "--name",
400 "CreateTest",
401 "--vendor",
402 "TestRunner",
403 "--destination",
404 "./test.par.gz",
405 "--revision",
406 "1",
407 "--version",
408 "1.11.111",
409 "--directory",
410 "./tests/fixtures",
411 "--issuer",
412 ISSUER,
413 "--subject",
414 SUBJECT,
415 "--disable-keygen",
416 "--compress",
417 "--wit-directory",
418 "./wit",
419 ])
420 .unwrap();
421 match create_long.par {
422 ParCliCommand::Create(CreateCommand {
423 vendor,
424 revision,
425 version,
426 schema,
427 directory,
428 issuer,
429 subject,
430 name,
431 arch,
432 binary,
433 destination,
434 compress,
435 disable_keygen,
436 wit_dir,
437 }) => {
438 assert_eq!(arch, "x86_64-testrunner");
439 assert_eq!(binary, "./testrunner.so");
440 assert_eq!(directory.unwrap(), PathBuf::from("./tests/fixtures"));
441 assert_eq!(issuer.unwrap(), ISSUER);
442 assert_eq!(subject.unwrap(), SUBJECT);
443 assert_eq!(name, "CreateTest");
444 assert_eq!(vendor, "TestRunner");
445 assert_eq!(destination.unwrap(), "./test.par.gz");
446 assert_eq!(revision.unwrap(), 1);
447 assert_eq!(version.unwrap(), "1.11.111");
448 assert_eq!(schema, None);
449 assert!(disable_keygen);
450 assert!(compress);
451 assert_eq!(wit_dir.unwrap(), PathBuf::from("./wit"));
452 }
453 cmd => panic!("par insert constructed incorrect command {cmd:?}"),
454 }
455 let create_short: Cmd = clap::Parser::try_parse_from([
456 "par",
457 "create",
458 "-a",
459 "x86_64-testrunner",
460 "-b",
461 "./testrunner.so",
462 "-n",
463 "CreateTest",
464 "-v",
465 "TestRunner",
466 "--destination",
467 "./test.par.gz",
468 "-r",
469 "1",
470 "--version",
471 "1.11.111",
472 "-d",
473 "./tests/fixtures",
474 "-i",
475 ISSUER,
476 "-s",
477 SUBJECT,
478 "--wit-directory",
479 "./wit",
480 ])
481 .unwrap();
482 match create_short.par {
483 ParCliCommand::Create(CreateCommand {
484 vendor,
485 revision,
486 version,
487 schema,
488 directory,
489 issuer,
490 subject,
491 name,
492 arch,
493 binary,
494 destination,
495 compress,
496 disable_keygen,
497 wit_dir,
498 }) => {
499 assert_eq!(arch, "x86_64-testrunner");
500 assert_eq!(binary, "./testrunner.so");
501 assert_eq!(directory.unwrap(), PathBuf::from("./tests/fixtures"));
502 assert_eq!(issuer.unwrap(), ISSUER);
503 assert_eq!(subject.unwrap(), SUBJECT);
504 assert_eq!(name, "CreateTest");
505 assert_eq!(vendor, "TestRunner");
506 assert_eq!(destination.unwrap(), "./test.par.gz");
507 assert_eq!(revision.unwrap(), 1);
508 assert_eq!(version.unwrap(), "1.11.111");
509 assert_eq!(schema, None);
510 assert!(!disable_keygen);
511 assert!(!compress);
512 assert_eq!(wit_dir.unwrap(), PathBuf::from("./wit"));
513 }
514 cmd => panic!("par insert constructed incorrect command {cmd:?}"),
515 }
516 }
517
518 #[test]
521 fn test_par_insert_comprehensive() {
522 const ISSUER: &str = "SAAJLQZDZO57THPTQLEELEY7FJYOJZQWQD7FF4J67TUYTSCOXTF7R4Y3VY";
523 const SUBJECT: &str = "SVAH7IN6QE6XODCGQAWZQDZ5LNSSS4FNEO6SNHZSSASW4BBBKSZ6KWTKWY";
524 let insert_short: Cmd = clap::Parser::try_parse_from([
525 "par",
526 "insert",
527 "libtest.par.gz",
528 "-a",
529 "x86_64-testrunner",
530 "-b",
531 "./testrunner.so",
532 "-d",
533 "./tests/fixtures",
534 "-i",
535 ISSUER,
536 "-s",
537 SUBJECT,
538 "--disable-keygen",
539 ])
540 .unwrap();
541 match insert_short.par {
542 ParCliCommand::Insert(InsertCommand {
543 archive,
544 arch,
545 binary,
546 directory,
547 issuer,
548 subject,
549 disable_keygen,
550 }) => {
551 assert_eq!(archive, "libtest.par.gz");
552 assert_eq!(arch, "x86_64-testrunner");
553 assert_eq!(binary, "./testrunner.so");
554 assert_eq!(directory.unwrap(), PathBuf::from("./tests/fixtures"));
555 assert_eq!(issuer.unwrap(), ISSUER);
556 assert_eq!(subject.unwrap(), SUBJECT);
557 assert!(disable_keygen);
558 }
559 cmd => panic!("par insert constructed incorrect command {cmd:?}"),
560 }
561 let insert_long: Cmd = clap::Parser::try_parse_from([
562 "par",
563 "insert",
564 "libtest.par.gz",
565 "--arch",
566 "x86_64-testrunner",
567 "--binary",
568 "./testrunner.so",
569 "--directory",
570 "./tests/fixtures",
571 "--issuer",
572 ISSUER,
573 "--subject",
574 SUBJECT,
575 ])
576 .unwrap();
577 match insert_long.par {
578 ParCliCommand::Insert(InsertCommand {
579 archive,
580 arch,
581 binary,
582 directory,
583 issuer,
584 subject,
585 disable_keygen,
586 }) => {
587 assert_eq!(archive, "libtest.par.gz");
588 assert_eq!(arch, "x86_64-testrunner");
589 assert_eq!(binary, "./testrunner.so");
590 assert_eq!(directory.unwrap(), PathBuf::from("./tests/fixtures"));
591 assert_eq!(issuer.unwrap(), ISSUER);
592 assert_eq!(subject.unwrap(), SUBJECT);
593 assert!(!disable_keygen);
594 }
595 cmd => panic!("par insert constructed incorrect command {cmd:?}"),
596 }
597 }
598
599 #[test]
602 fn test_par_inspect_comprehensive() {
603 const LOCAL: &str = "./coolthing.par.gz";
604 const REMOTE: &str = "wasmcloud.azurecr.io/coolthing.par.gz";
605
606 let inspect_long: Cmd = clap::Parser::try_parse_from([
607 "par",
608 "inspect",
609 LOCAL,
610 "--digest",
611 "sha256:blah",
612 "--password",
613 "secret",
614 "--user",
615 "name",
616 "--no-cache",
617 ])
618 .unwrap();
619 match inspect_long.par {
620 ParCliCommand::Inspect(InspectCommand {
621 archive,
622 digest,
623 allow_latest,
624 user,
625 password,
626 insecure,
627 insecure_skip_tls_verify,
628 no_cache,
629 }) => {
630 assert_eq!(archive, LOCAL);
631 assert_eq!(digest.unwrap(), "sha256:blah");
632 assert!(!allow_latest);
633 assert!(!insecure);
634 assert!(!insecure_skip_tls_verify);
635 assert_eq!(user.unwrap(), "name");
636 assert_eq!(password.unwrap(), "secret");
637 assert!(no_cache);
638 }
639 cmd => panic!("par inspect constructed incorrect command {cmd:?}"),
640 }
641 let inspect_short: Cmd = clap::Parser::try_parse_from([
642 "par",
643 "inspect",
644 REMOTE,
645 "-d",
646 "sha256:blah",
647 "-p",
648 "secret",
649 "-u",
650 "name",
651 "--allow-latest",
652 "--insecure",
653 "--no-cache",
654 ])
655 .unwrap();
656 match inspect_short.par {
657 ParCliCommand::Inspect(InspectCommand {
658 archive,
659 digest,
660 allow_latest,
661 user,
662 password,
663 insecure,
664 insecure_skip_tls_verify,
665 no_cache,
666 }) => {
667 assert_eq!(archive, REMOTE);
668 assert_eq!(digest.unwrap(), "sha256:blah");
669 assert!(allow_latest);
670 assert!(insecure);
671 assert!(!insecure_skip_tls_verify);
672 assert_eq!(user.unwrap(), "name");
673 assert_eq!(password.unwrap(), "secret");
674 assert!(no_cache);
675 }
676 cmd => panic!("par inspect constructed incorrect command {cmd:?}"),
677 }
678 }
679}