blob: 9a0b88888fd840dc7a8a2300550bee2b607f5679 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::error::Error, argh, argh::FromArgs, fidl_fuchsia_pkg::ExperimentToggle as Experiment,
fidl_fuchsia_pkg_ext::BlobId, fidl_fuchsia_pkg_rewrite_ext::RuleConfig, serde_json,
std::path::PathBuf,
};
#[derive(FromArgs, Debug, PartialEq)]
/// Various operations on packages, package repositories, and the package cache.
pub struct Args {
#[argh(subcommand)]
pub command: Command,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum Command {
Resolve(ResolveCommand),
Open(OpenCommand),
Repo(RepoCommand),
Rule(RuleCommand),
Experiment(ExperimentCommand),
Gc(GcCommand),
GetHash(GetHashCommand),
PkgStatus(PkgStatusCommand),
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "resolve")]
/// Resolve a package.
pub struct ResolveCommand {
#[argh(positional)]
pub pkg_url: String,
#[argh(positional)]
pub selectors: Vec<String>,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "open")]
/// Open a package by merkle root.
pub struct OpenCommand {
#[argh(positional)]
pub meta_far_blob_id: BlobId,
#[argh(positional)]
pub selectors: Vec<String>,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(
subcommand,
name = "repo",
note = "A fuchsia package URL contains a repository hostname to identify the package's source.\n",
note = "Example repository hostnames are:\n",
note = " fuchsia.com",
note = " mycorp.com\n",
note = "Without any arguments the command outputs the list of configured repository URLs.\n",
note = "Note that repo commands expect the full repository URL, not just the hostname, e.g:",
note = "$ pkgctl repo rm fuchsia-pkg://mycorp.com"
)]
/// Manage one or more known repositories.
pub struct RepoCommand {
/// verbose output
#[argh(switch, short = 'v')]
pub verbose: bool,
#[argh(subcommand)]
pub subcommand: Option<RepoSubCommand>,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum RepoSubCommand {
Add(RepoAddCommand),
Remove(RepoRemoveCommand),
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "add")]
/// Add a source repository.
pub struct RepoAddCommand {
#[argh(subcommand)]
pub subcommand: RepoAddSubCommand,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum RepoAddSubCommand {
File(RepoAddFileCommand),
Url(RepoAddUrlCommand),
}
#[derive(Debug, PartialEq)]
pub enum RepoConfigFormat {
Version1,
Version2,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "file")]
/// Add a respository config from a local file, in JSON format, which contains the different repository metadata and URLs.
pub struct RepoAddFileCommand {
/// the expected config.json file format version.
#[argh(
option,
short = 'f',
default = "RepoConfigFormat::Version2",
from_str_fn(repo_config_format)
)]
pub format: RepoConfigFormat,
/// name of the source (a name from the URL will be derived if not provided).
#[argh(option, short = 'n')]
pub name: Option<String>,
/// respository config file, in JSON format, which contains the different repository metadata and URLs.
#[argh(positional)]
pub file: PathBuf,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "url")]
/// Add a respository config via http, in JSON format, which contains the different repository metadata and URLs.
pub struct RepoAddUrlCommand {
/// the expected config.json file format version.
#[argh(
option,
short = 'f',
default = "RepoConfigFormat::Version2",
from_str_fn(repo_config_format)
)]
pub format: RepoConfigFormat,
/// name of the source (a name from the URL will be derived if not provided).
#[argh(option, short = 'n')]
pub name: Option<String>,
/// http(s) URL pointing to a respository config file, in JSON format, which contains the different repository metadata and URLs.
#[argh(positional)]
pub repo_url: String,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "rm")]
/// Remove a configured source repository.
pub struct RepoRemoveCommand {
#[argh(positional)]
pub repo_url: String,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "rule")]
/// Manage URL rewrite rules applied to package URLs during package resolution.
pub struct RuleCommand {
#[argh(subcommand)]
pub subcommand: RuleSubCommand,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum RuleSubCommand {
Clear(RuleClearCommand),
DumpDynamic(RuleDumpDynamicCommand),
List(RuleListCommand),
Replace(RuleReplaceCommand),
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "clear")]
/// Clear all URL rewrite rules.
pub struct RuleClearCommand {}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "list")]
/// List all URL rewrite rules.
pub struct RuleListCommand {}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "dump-dynamic")]
/// Dumps all dynamic rewrite rules.
pub struct RuleDumpDynamicCommand {}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "replace")]
/// Replace all dynamic rules with the provided rules.
pub struct RuleReplaceCommand {
#[argh(subcommand)]
pub subcommand: RuleReplaceSubCommand,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum RuleReplaceSubCommand {
File(RuleReplaceFileCommand),
Json(RuleReplaceJsonCommand),
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "file")]
/// Replace all rewrite rules with ones specified in a file
pub struct RuleReplaceFileCommand {
#[argh(positional)]
pub file: PathBuf,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "json")]
/// Replace all rewrite rules with JSON from the command line
pub struct RuleReplaceJsonCommand {
#[argh(positional, from_str_fn(parse_rule_config))]
pub config: RuleConfig,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(
subcommand,
name = "experiment",
note = "Experiments may be added or removed over time and should not be considered stable.",
note = "Known experiments:",
note = " lightbulb no-op experiment"
)]
/// Manage runtime experiment states.
pub struct ExperimentCommand {
#[argh(subcommand)]
pub subcommand: ExperimentSubCommand,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum ExperimentSubCommand {
Enable(ExperimentEnableCommand),
Disable(ExperimentDisableCommand),
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "enable")]
/// Enable the given experiment.
pub struct ExperimentEnableCommand {
#[argh(positional, from_str_fn(parse_experiment_id))]
pub experiment: Experiment,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "disable")]
/// Disable the given experiment.
pub struct ExperimentDisableCommand {
#[argh(positional, from_str_fn(parse_experiment_id))]
pub experiment: Experiment,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(
subcommand,
name = "gc",
note = "This deletes any cached packages that are not present in the static and dynamic index.",
note = "Any blobs associated with these packages will be removed if they are not referenced by another component or package.",
note = "The static index currently is located at /system/data/static_packages, but this location is likely to change.",
note = "The dynamic index is dynamically calculated, and cannot easily be queried at this time."
)]
/// Trigger a manual garbage collection of the package cache.
pub struct GcCommand {}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "get-hash")]
/// Get the hash of a package.
pub struct GetHashCommand {
#[argh(positional)]
pub pkg_url: String,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(
subcommand,
name = "pkg-status",
note = "Exit codes:",
note = " 0 - pkg in tuf repo and on disk",
note = " 2 - pkg in tuf repo but not on disk",
note = " 3 - pkg not in tuf repo",
note = " 1 - any other misc application error"
)]
/// Determine if a pkg is in a registered tuf repo and/or on disk.
pub struct PkgStatusCommand {
#[argh(positional)]
pub pkg_url: String,
}
fn parse_experiment_id(experiment: &str) -> Result<Experiment, String> {
match experiment {
"lightbulb" => Ok(Experiment::Lightbulb),
experiment => Err(Error::ExperimentId(experiment.to_owned()).to_string()),
}
}
fn parse_rule_config(config: &str) -> Result<RuleConfig, String> {
serde_json::from_str(&config).map_err(|e| e.to_string())
}
fn repo_config_format(value: &str) -> Result<RepoConfigFormat, String> {
match value {
"1" => Ok(RepoConfigFormat::Version1),
"2" => Ok(RepoConfigFormat::Version2),
_ => Err(format!("unknown format {:?}", value)),
}
}
#[cfg(test)]
mod tests {
use {super::*, matches::assert_matches};
const REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
const CONFIG_JSON: &str = r#"{"version": "1", "content": []}"#;
const CMD_NAME: &'static [&'static str] = &["pkgctl"];
#[test]
fn resolve() {
fn check(args: &[&str], expected_pkg_url: &str, expected_selectors: &[String]) {
assert_eq!(
Args::from_args(CMD_NAME, args),
Ok(Args {
command: Command::Resolve(ResolveCommand {
pkg_url: expected_pkg_url.to_string(),
selectors: expected_selectors.into_iter().cloned().collect()
})
})
);
}
let url = "fuchsia-pkg://fuchsia.com/foo/bar";
check(&["resolve", url], url, &[]);
check(
&["resolve", url, "selector1", "selector2"],
url,
&["selector1".to_string(), "selector2".to_string()],
);
}
#[test]
fn open() {
fn check(args: &[&str], expected_blob_id: &str, expected_selectors: &[String]) {
assert_eq!(
Args::from_args(CMD_NAME, args),
Ok(Args {
command: Command::Open(OpenCommand {
meta_far_blob_id: expected_blob_id.parse().unwrap(),
selectors: expected_selectors.into_iter().cloned().collect()
})
})
)
}
let blob_id = "1111111111111111111111111111111111111111111111111111111111111111";
check(&["open", blob_id], blob_id, &[]);
check(
&["open", blob_id, "selector1", "selector2"],
blob_id,
&["selector1".to_string(), "selector2".to_string()],
);
}
#[test]
fn open_reject_malformed_blobs() {
match Args::from_args(CMD_NAME, &["open", "bad_id"]) {
Err(argh::EarlyExit { output: _, status: _ }) => {}
result => panic!("unexpected result {:?}", result),
}
}
#[test]
fn repo() {
fn check(args: &[&str], expected: RepoCommand) {
assert_eq!(
Args::from_args(CMD_NAME, args),
Ok(Args { command: Command::Repo(expected) })
)
}
check(&["repo"], RepoCommand { verbose: false, subcommand: None });
check(&["repo", "-v"], RepoCommand { verbose: true, subcommand: None });
check(&["repo", "--verbose"], RepoCommand { verbose: true, subcommand: None });
check(
&["repo", "add", "file", "foo"],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
subcommand: RepoAddSubCommand::File(RepoAddFileCommand {
format: RepoConfigFormat::Version2,
name: None,
file: "foo".into(),
}),
})),
},
);
check(
&["repo", "add", "file", "-f", "1", "foo"],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
subcommand: RepoAddSubCommand::File(RepoAddFileCommand {
format: RepoConfigFormat::Version1,
name: None,
file: "foo".into(),
}),
})),
},
);
check(
&["repo", "add", "file", "-n", "devhost", "foo"],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
subcommand: RepoAddSubCommand::File(RepoAddFileCommand {
format: RepoConfigFormat::Version2,
name: Some("devhost".to_string()),
file: "foo".into(),
}),
})),
},
);
check(
&["repo", "add", "url", "-n", "devhost", "http://foo.tld/fuchsia/config.json"],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
subcommand: RepoAddSubCommand::Url(RepoAddUrlCommand {
format: RepoConfigFormat::Version2,
name: Some("devhost".to_string()),
repo_url: "http://foo.tld/fuchsia/config.json".into(),
}),
})),
},
);
check(
&["repo", "rm", REPO_URL],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Remove(RepoRemoveCommand {
repo_url: REPO_URL.to_string(),
})),
},
);
}
#[test]
fn rule() {
fn check(args: &[&str], expected: RuleCommand) {
match Args::from_args(CMD_NAME, args).unwrap() {
Args { command: Command::Rule(cmd) } => {
assert_eq!(cmd, expected);
}
result => panic!("unexpected result {:?}", result),
}
}
check(
&["rule", "list"],
RuleCommand { subcommand: RuleSubCommand::List(RuleListCommand {}) },
);
check(
&["rule", "clear"],
RuleCommand { subcommand: RuleSubCommand::Clear(RuleClearCommand {}) },
);
check(
&["rule", "dump-dynamic"],
RuleCommand { subcommand: RuleSubCommand::DumpDynamic(RuleDumpDynamicCommand {}) },
);
check(
&["rule", "replace", "file", "foo"],
RuleCommand {
subcommand: RuleSubCommand::Replace(RuleReplaceCommand {
subcommand: RuleReplaceSubCommand::File(RuleReplaceFileCommand {
file: "foo".into(),
}),
}),
},
);
check(
&["rule", "replace", "json", CONFIG_JSON],
RuleCommand {
subcommand: RuleSubCommand::Replace(RuleReplaceCommand {
subcommand: RuleReplaceSubCommand::Json(RuleReplaceJsonCommand {
config: RuleConfig::Version1(vec![]),
}),
}),
},
);
}
#[test]
fn rule_replace_json_rejects_malformed_json() {
assert_matches!(
Args::from_args(CMD_NAME, &["rule", "replace", "json", "{"]),
Err(argh::EarlyExit { output: _, status: _ })
);
}
#[test]
fn experiment_ok() {
assert_eq!(
Args::from_args(CMD_NAME, &["experiment", "enable", "lightbulb"]).unwrap(),
Args {
command: Command::Experiment(ExperimentCommand {
subcommand: ExperimentSubCommand::Enable(ExperimentEnableCommand {
experiment: Experiment::Lightbulb
})
})
}
);
assert_eq!(
Args::from_args(CMD_NAME, &["experiment", "disable", "lightbulb"]).unwrap(),
Args {
command: Command::Experiment(ExperimentCommand {
subcommand: ExperimentSubCommand::Disable(ExperimentDisableCommand {
experiment: Experiment::Lightbulb
})
})
}
);
}
#[test]
fn experiment_unknown() {
assert_matches!(
Args::from_args(CMD_NAME, &["experiment", "enable", "unknown"]),
Err(argh::EarlyExit { output, status: Err(()) }) if output.contains("unknown")
);
assert_matches!(
Args::from_args(CMD_NAME, &["experiment", "disable", "unknown"]),
Err(argh::EarlyExit { output, status: Err(()) }) if output.contains("unknown")
);
}
#[test]
fn gc() {
match Args::from_args(CMD_NAME, &["gc"]).unwrap() {
Args { command: Command::Gc(GcCommand {}) } => {}
result => panic!("unexpected result {:?}", result),
}
}
#[test]
fn get_hash() {
let url = "fuchsia-pkg://fuchsia.com/foo/bar";
match Args::from_args(CMD_NAME, &["get-hash", url]).unwrap() {
Args { command: Command::GetHash(GetHashCommand { pkg_url }) } if pkg_url == url => {}
result => panic!("unexpected result {:?}", result),
}
}
#[test]
fn pkg_status() {
let url = "fuchsia-pkg://fuchsia.com/foo/bar";
match Args::from_args(CMD_NAME, &["pkg-status", url]).unwrap() {
Args { command: Command::PkgStatus(PkgStatusCommand { pkg_url }) }
if pkg_url == url => {}
result => panic!("unexpected result {:?}", result),
}
}
}