diff --git a/garnet/go/src/amber/BUILD.gn b/garnet/go/src/amber/BUILD.gn index 916a5f20aa7b785bd532b4bb42d0297d65b47689..bcce302f8a687c27c8fdeb644873bb2b81c650e8 100644 --- a/garnet/go/src/amber/BUILD.gn +++ b/garnet/go/src/amber/BUILD.gn @@ -36,6 +36,7 @@ go_library("lib") { "//garnet/public/lib/syslog/go/src/syslog", "//sdk/fidl/fuchsia.amber($go_toolchain)", "//sdk/fidl/fuchsia.pkg($go_toolchain)", + "//sdk/fidl/fuchsia.pkg.rewrite($go_toolchain)", "//sdk/fidl/fuchsia.sys($go_toolchain)", "//zircon/public/fidl/fuchsia-cobalt($go_toolchain)", "//zircon/public/fidl/fuchsia-mem($go_toolchain)", diff --git a/garnet/go/src/amber/amberctl/amberctl.go b/garnet/go/src/amber/amberctl/amberctl.go index 7b301e927f309bb825518713af426d1e1300cc7b..13981fd742caca3e3597e0c5bb3471559e66e094 100644 --- a/garnet/go/src/amber/amberctl/amberctl.go +++ b/garnet/go/src/amber/amberctl/amberctl.go @@ -28,6 +28,7 @@ import ( "fidl/fuchsia/amber" fuchsiaio "fidl/fuchsia/io" "fidl/fuchsia/pkg" + "fidl/fuchsia/pkg/rewrite" ) const usage = `usage: %s <command> [opts] @@ -101,27 +102,185 @@ func doTest(pxy *amber.ControlInterface) error { return nil } -func connectToAmber(ctx *context.Context) (*amber.ControlInterface, amber.ControlInterfaceRequest) { +type Services struct { + amber *amber.ControlInterface + resolver *pkg.PackageResolverInterface + repoMgr *pkg.RepositoryManagerInterface + rewriteEngine *rewrite.EngineInterface +} + +func connectToAmber(ctx *context.Context) *amber.ControlInterface { req, pxy, err := amber.NewControlInterfaceRequest() if err != nil { panic(err) } ctx.ConnectToEnvService(req) - return pxy, req + return pxy } -func connectToPackageResolver(ctx *context.Context) (*pkg.PackageResolverInterface, pkg.PackageResolverInterfaceRequest) { +func connectToPackageResolver(ctx *context.Context) *pkg.PackageResolverInterface { req, pxy, err := pkg.NewPackageResolverInterfaceRequest() if err != nil { panic(err) } ctx.ConnectToEnvService(req) - return pxy, req + return pxy } -func addSource(a *amber.ControlInterface) error { - var cfg amber.SourceConfig +func connectToRepositoryManager(ctx *context.Context) *pkg.RepositoryManagerInterface { + req, pxy, err := pkg.NewRepositoryManagerInterfaceRequest() + if err != nil { + panic(err) + } + ctx.ConnectToEnvService(req) + return pxy +} + +func connectToRewriteEngine(ctx *context.Context) *rewrite.EngineInterface { + req, pxy, err := rewrite.NewEngineInterfaceRequest() + if err != nil { + panic(err) + } + ctx.ConnectToEnvService(req) + return pxy +} + +// upgradeSourceConfig attempts to upgrade an amber.SourceConfig into a pkg.RepositoryConfig +// +// The two config formats are incompatible in various ways: +// +// * repo configs cannot be disabled. amberctl will attempt to preserve a config's disabled bit by +// not configuring a rewrite rule for the source. +// +// * repo configs do not support oauth, network client config options, or polling frequency +// overrides. If present, these options are discarded. +// +// * repo config mirrors do not accept different URLs for the TUF repo and the blobs. Any custom +// blob URL is discarded. +func upgradeSourceConfig(cfg amber.SourceConfig) pkg.RepositoryConfig { + repoCfg := pkg.RepositoryConfig{ + RepoUrl: repoUrlForId(cfg.Id), + RepoUrlPresent: true, + } + + mirror := pkg.MirrorConfig{ + MirrorUrl: cfg.RepoUrl, + MirrorUrlPresent: true, + Subscribe: cfg.Auto, + SubscribePresent: true, + } + if cfg.BlobKey != nil { + var blobKey pkg.RepositoryBlobKey + blobKey.SetAesKey(cfg.BlobKey.Data[:]) + mirror.SetBlobKey(blobKey) + } + repoCfg.SetMirrors([]pkg.MirrorConfig{mirror}) + + for _, key := range cfg.RootKeys { + if key.Type != "ed25519" { + continue + } + + var rootKey pkg.RepositoryKeyConfig + bytes, err := hex.DecodeString(key.Value) + if err != nil { + continue + } + rootKey.SetEd25519Key(bytes) + + repoCfg.RootKeys = append(repoCfg.RootKeys, rootKey) + repoCfg.RootKeysPresent = true + } + + return repoCfg +} + +func repoUrlForId(id string) string { + return fmt.Sprintf("fuchsia-pkg://%s", id) +} + +func rewriteRuleForId(id string) rewrite.Rule { + var rule rewrite.Rule + rule.SetLiteral(rewrite.LiteralRule{ + HostMatch: "fuchsia.com", + HostReplacement: id, + PathPrefixMatch: "/", + PathPrefixReplacement: "/", + }) + return rule +} + +func replaceDynamicRewriteRules(rewriteEngine *rewrite.EngineInterface, rule rewrite.Rule) error { + return doRewriteRuleEditTransaction(rewriteEngine, func(transaction *rewrite.EditTransactionInterface) error { + if err := transaction.ResetAll(); err != nil { + return fmt.Errorf("fuchsia.pkg.rewrite.EditTransaction.ResetAll IPC encountered an error: %s", err) + } + + s, err := transaction.Add(rule) + if err != nil { + return fmt.Errorf("fuchsia.pkg.rewrite.EditTransaction.Add IPC encountered an error: %s", err) + } + status := zx.Status(s) + if status != zx.ErrOk { + return fmt.Errorf("unable to add rewrite rule: %s", status) + } + + return nil + }) +} + +func removeAllDynamicRewriteRules(rewriteEngine *rewrite.EngineInterface) error { + return doRewriteRuleEditTransaction(rewriteEngine, func(transaction *rewrite.EditTransactionInterface) error { + if err := transaction.ResetAll(); err != nil { + return fmt.Errorf("fuchsia.pkg.rewrite.EditTransaction.ResetAll IPC encountered an error: %s", err) + } + + return nil + }) +} +// doRewriteRuleEditTransaction executes a rewrite rule edit transaction using +// the provided callback, retrying on data races a few times before giving up. +func doRewriteRuleEditTransaction(rewriteEngine *rewrite.EngineInterface, cb func(*rewrite.EditTransactionInterface) error) error { + for i := 0; i < 10; i++ { + err, status := func() (error, zx.Status) { + var status zx.Status + req, transaction, err := rewrite.NewEditTransactionInterfaceRequest() + if err != nil { + return fmt.Errorf("creating edit transaction: %s", err), status + } + defer transaction.Close() + if err := rewriteEngine.StartEditTransaction(req); err != nil { + return fmt.Errorf("fuchsia.pkg.rewrite.Engine IPC encountered an error: %s", err), status + } + + if err := cb(transaction); err != nil { + return err, status + } + + s, err := transaction.Commit() + if err != nil { + return fmt.Errorf("fuchsia.pkg.rewrite.EditTransaction.Commit IPC encountered an error: %s", err), status + } + return nil, zx.Status(s) + }() + if err != nil { + return err + } + switch status { + case zx.ErrOk: + return nil + case zx.ErrUnavailable: + continue + default: + return fmt.Errorf("unexpected error while committing rewrite rule transaction: %s", status) + } + } + + return fmt.Errorf("unable to commit rewrite rule changes") +} + +func addSource(services Services) error { if len(*pkgFile) == 0 { return fmt.Errorf("a url or file path (via -f) are required") } @@ -179,6 +338,7 @@ func addSource(a *amber.ControlInterface) error { source = f } + var cfg amber.SourceConfig if err := json.NewDecoder(source).Decode(&cfg); err != nil { return fmt.Errorf("failed to parse source config: %v", err) } @@ -206,16 +366,38 @@ func addSource(a *amber.ControlInterface) error { cfg.BlobRepoUrl = filepath.Join(cfg.RepoUrl, "blobs") } - added, err := a.AddSrc(cfg) + added, err := services.amber.AddSrc(cfg) if err != nil { - return fmt.Errorf("IPC encountered an error: %s", err) + return fmt.Errorf("fuchsia.amber.Control IPC encountered an error: %s", err) } if !added { return fmt.Errorf("request arguments properly formatted, but possibly otherwise invalid") } if isSourceConfigEnabled(&cfg) && !*nonExclusive { - if err := disableAllSources(a, cfg.Id); err != nil { + if err := disableAllSources(services.amber, cfg.Id); err != nil { + return err + } + } + + repoCfg := upgradeSourceConfig(cfg) + s, err := services.repoMgr.Add(repoCfg) + if err != nil { + return fmt.Errorf("fuchsia.pkg.RepositoryManager IPC encountered an error: %s", err) + } + status := zx.Status(s) + if !(status == zx.ErrOk || status == zx.ErrAlreadyExists) { + return fmt.Errorf("unable to register source with RepositoryManager: %s", status) + } + + // Nothing currently registers sources in a disabled state, but make a best effort attempt + // to try to prevent the source from being used anyway by only configuring a mapping of + // fuchsia.com to this source if it is enabled. Note that this doesn't prevent resolving a + // package using this config's id explicitly or calling an amber source config + // "fuchsia.com". + if isSourceConfigEnabled(&cfg) { + rule := rewriteRuleForId(cfg.Id) + if err := replaceDynamicRewriteRules(services.rewriteEngine, rule); err != nil { return err } } @@ -223,19 +405,19 @@ func addSource(a *amber.ControlInterface) error { return nil } -func rmSource(a *amber.ControlInterface) error { +func rmSource(services Services) error { name := strings.TrimSpace(*name) if name == "" { return fmt.Errorf("no source id provided") } - status, err := a.RemoveSrc(name) + status, err := services.amber.RemoveSrc(name) if err != nil { - return fmt.Errorf("IPC encountered an error: %s", err) + return fmt.Errorf("fuchsia.amber.Control IPC encountered an error: %s", err) } switch status { case amber.StatusOk: - return nil + break case amber.StatusErrNotFound: return fmt.Errorf("Source not found") case amber.StatusErr: @@ -243,6 +425,25 @@ func rmSource(a *amber.ControlInterface) error { default: return fmt.Errorf("Unexpected status: %v", status) } + + // Since modifications to amber.Control, RepositoryManager, and rewrite.Engine aren't + // atomic and amberctl could be interrupted or encounter an error during any step, + // unregister the rewrite rule before removing the repo config to prevent a dangling + // rewrite rule to a repo that no longer exists. + if err := removeAllDynamicRewriteRules(services.rewriteEngine); err != nil { + return err + } + + s, err := services.repoMgr.Remove(repoUrlForId(name)) + if err != nil { + return fmt.Errorf("fuchsia.pkg.RepositoryManager IPC encountered an error: %s", err) + } + zxStatus := zx.Status(s) + if !(zxStatus == zx.ErrOk || zxStatus == zx.ErrNotFound) { + return fmt.Errorf("unable to remove source from RepositoryManager: %s", zxStatus) + } + + return nil } func getUp(r *pkg.PackageResolverInterface) error { @@ -322,10 +523,10 @@ func disableAllSources(a *amber.ControlInterface, except string) error { return nil } -func do(amberProxy *amber.ControlInterface, resolverProxy *pkg.PackageResolverInterface) int { +func do(services Services) int { switch os.Args[1] { case "get_up": - if err := getUp(resolverProxy); err != nil { + if err := getUp(services.resolver); err != nil { log.Printf("error getting an update: %s", err) return 1 } @@ -334,12 +535,12 @@ func do(amberProxy *amber.ControlInterface, resolverProxy *pkg.PackageResolverIn log.Printf("no blob id provided") return 1 } - if err := amberProxy.GetBlob(*blobID); err != nil { + if err := services.amber.GetBlob(*blobID); err != nil { log.Printf("error requesting blob fetch: %s", err) return 1 } case "add_src": - if err := addSource(amberProxy); err != nil { + if err := addSource(services); err != nil { log.Printf("error adding source: %s", err) if _, ok := err.(ErrGetFile); ok { return 2 @@ -348,12 +549,12 @@ func do(amberProxy *amber.ControlInterface, resolverProxy *pkg.PackageResolverIn } } case "rm_src": - if err := rmSource(amberProxy); err != nil { + if err := rmSource(services); err != nil { log.Printf("error removing source: %s", err) return 1 } case "list_srcs": - if err := listSources(amberProxy); err != nil { + if err := listSources(services.amber); err != nil { log.Printf("error listing sources: %s", err) return 1 } @@ -361,12 +562,12 @@ func do(amberProxy *amber.ControlInterface, resolverProxy *pkg.PackageResolverIn log.Printf("%q not yet supported\n", os.Args[1]) return 1 case "test": - if err := doTest(amberProxy); err != nil { + if err := doTest(services.amber); err != nil { log.Printf("error testing connection to amber: %s", err) return 1 } case "system_update": - configured, err := amberProxy.CheckForSystemUpdate() + configured, err := services.amber.CheckForSystemUpdate() if err != nil { log.Printf("error checking for system update: %s", err) return 1 @@ -382,14 +583,19 @@ func do(amberProxy *amber.ControlInterface, resolverProxy *pkg.PackageResolverIn log.Printf("Error enabling source: no source id provided") return 1 } - err := setSourceEnablement(amberProxy, *name, true) + err := setSourceEnablement(services.amber, *name, true) if err != nil { log.Printf("Error enabling source: %s", err) return 1 } + err = replaceDynamicRewriteRules(services.rewriteEngine, rewriteRuleForId(*name)) + if err != nil { + log.Printf("Error configuring rewrite rules: %s", err) + return 1 + } fmt.Printf("Source %q enabled\n", *name) if !*nonExclusive { - if err := disableAllSources(amberProxy, *name); err != nil { + if err := disableAllSources(services.amber, *name); err != nil { log.Printf("Error disabling sources: %s", err) return 1 } @@ -399,14 +605,19 @@ func do(amberProxy *amber.ControlInterface, resolverProxy *pkg.PackageResolverIn log.Printf("Error disabling source: no source id provided") return 1 } - err := setSourceEnablement(amberProxy, *name, false) + err := setSourceEnablement(services.amber, *name, false) if err != nil { log.Printf("Error disabling source: %s", err) return 1 } + err = removeAllDynamicRewriteRules(services.rewriteEngine) + if err != nil { + log.Printf("Error configuring rewrite rules: %s", err) + return 1 + } fmt.Printf("Source %q disabled\n", *name) case "gc": - err := amberProxy.Gc() + err := services.amber.Gc() if err != nil { log.Printf("Error collecting garbage: %s", err) return 1 @@ -462,13 +673,21 @@ func Main() { ctx := context.CreateFromStartupInfo() - amberProxy, _ := connectToAmber(ctx) - defer amberProxy.Close() + var services Services + + services.amber = connectToAmber(ctx) + defer services.amber.Close() + + services.resolver = connectToPackageResolver(ctx) + defer services.resolver.Close() + + services.repoMgr = connectToRepositoryManager(ctx) + defer services.repoMgr.Close() - resolverProxy, _ := connectToPackageResolver(ctx) - defer resolverProxy.Close() + services.rewriteEngine = connectToRewriteEngine(ctx) + defer services.rewriteEngine.Close() - os.Exit(do(amberProxy, resolverProxy)) + os.Exit(do(services)) } type ErrDaemon string diff --git a/garnet/go/src/amber/meta/amberctl.cmx b/garnet/go/src/amber/meta/amberctl.cmx index ba9393cf941b54c078e1476667fdb1c328077b15..4bc953cfb13b0cc15bc30e6b571cc0c7e8b2905e 100644 --- a/garnet/go/src/amber/meta/amberctl.cmx +++ b/garnet/go/src/amber/meta/amberctl.cmx @@ -7,7 +7,9 @@ "fuchsia.amber.Control", "fuchsia.logger.LogSink", "fuchsia.net.SocketProvider", - "fuchsia.pkg.PackageResolver" + "fuchsia.pkg.PackageResolver", + "fuchsia.pkg.RepositoryManager", + "fuchsia.pkg.rewrite.Engine" ] } } diff --git a/garnet/lib/rust/fidl_fuchsia_pkg_ext/src/repo.rs b/garnet/lib/rust/fidl_fuchsia_pkg_ext/src/repo.rs index 83b783284f13806dd820f13c56c114c5ad04178a..59ce8884268776969173f4379ee39eb73b023694 100644 --- a/garnet/lib/rust/fidl_fuchsia_pkg_ext/src/repo.rs +++ b/garnet/lib/rust/fidl_fuchsia_pkg_ext/src/repo.rs @@ -8,18 +8,18 @@ use { fuchsia_uri::pkg_uri::{PkgUri, RepoUri}, serde_derive::{Deserialize, Serialize}, std::convert::TryFrom, - std::mem, + std::{fmt, mem}, }; /// Convenience wrapper for the FIDL RepositoryKeyConfig type -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase", tag = "type", content = "value", deny_unknown_fields)] pub enum RepositoryKey { Ed25519(#[serde(with = "hex_serde")] Vec<u8>), } /// Convenience wrapper for the FIDL RepositoryBlobConfig type -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase", tag = "type", content = "value", deny_unknown_fields)] pub enum RepositoryBlobKey { Aes(#[serde(with = "hex_serde")] Vec<u8>), @@ -40,17 +40,27 @@ impl MirrorConfig { } /// Convenience wrapper for generating [MirrorConfig] values. +#[derive(Clone, Debug)] pub struct MirrorConfigBuilder { config: MirrorConfig, } impl MirrorConfigBuilder { - pub fn new(mirror_url: String) -> Self { + pub fn new(mirror_url: impl Into<String>) -> Self { MirrorConfigBuilder { - config: MirrorConfig { mirror_url: mirror_url, subscribe: false, blob_key: None }, + config: MirrorConfig { + mirror_url: mirror_url.into(), + subscribe: false, + blob_key: None, + }, } } + pub fn mirror_url(mut self, mirror_url: impl Into<String>) -> Self { + self.config.mirror_url = mirror_url.into(); + self + } + pub fn subscribe(mut self, subscribe: bool) -> Self { self.config.subscribe = subscribe; self @@ -66,6 +76,12 @@ impl MirrorConfigBuilder { } } +impl Into<MirrorConfig> for MirrorConfigBuilder { + fn into(self) -> MirrorConfig { + self.build() + } +} + impl TryFrom<fidl::MirrorConfig> for MirrorConfig { type Error = RepositoryParseError; fn try_from(other: fidl::MirrorConfig) -> Result<Self, RepositoryParseError> { @@ -169,6 +185,7 @@ impl Into<fidl::RepositoryConfig> for RepositoryConfig { } /// Convenience wrapper for generating [RepositoryConfig] values. +#[derive(Clone, Debug)] pub struct RepositoryConfigBuilder { config: RepositoryConfig, } @@ -177,7 +194,7 @@ impl RepositoryConfigBuilder { pub fn new(repo_url: RepoUri) -> Self { RepositoryConfigBuilder { config: RepositoryConfig { - repo_url: repo_url, + repo_url, root_keys: vec![], mirrors: vec![], update_package_uri: None, @@ -185,13 +202,18 @@ impl RepositoryConfigBuilder { } } + pub fn repo_url(mut self, repo_url: RepoUri) -> Self { + self.config.repo_url = repo_url; + self + } + pub fn add_root_key(mut self, key: RepositoryKey) -> Self { self.config.root_keys.push(key); self } - pub fn add_mirror(mut self, mirror: MirrorConfig) -> Self { - self.config.mirrors.push(mirror); + pub fn add_mirror(mut self, mirror: impl Into<MirrorConfig>) -> Self { + self.config.mirrors.push(mirror.into()); self } @@ -205,6 +227,12 @@ impl RepositoryConfigBuilder { } } +impl Into<RepositoryConfig> for RepositoryConfigBuilder { + fn into(self) -> RepositoryConfig { + self.build() + } +} + /// Wraper for serializing repository configs to the on-disk JSON format. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(tag = "version", content = "content", deny_unknown_fields)] @@ -231,6 +259,13 @@ impl Into<fidl::RepositoryKeyConfig> for RepositoryKey { } } +impl fmt::Debug for RepositoryKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let RepositoryKey::Ed25519(ref value) = self; + f.debug_tuple("Ed25519").field(&hex::encode(value)).finish() + } +} + impl TryFrom<fidl::RepositoryBlobKey> for RepositoryBlobKey { type Error = RepositoryParseError; fn try_from(id: fidl::RepositoryBlobKey) -> Result<Self, RepositoryParseError> { @@ -249,6 +284,13 @@ impl Into<fidl::RepositoryBlobKey> for RepositoryBlobKey { } } +impl fmt::Debug for RepositoryBlobKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let RepositoryBlobKey::Aes(ref value) = self; + f.debug_tuple("Aes").field(&hex::encode(value)).finish() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/garnet/lib/rust/fuchsia_uri_rewrite/src/rule.rs b/garnet/lib/rust/fuchsia_uri_rewrite/src/rule.rs index 3a17a326196d9383c273db22a6aa42d917b2694c..e61d14c5d3fe34914f95c5d061724361b91e91f2 100644 --- a/garnet/lib/rust/fuchsia_uri_rewrite/src/rule.rs +++ b/garnet/lib/rust/fuchsia_uri_rewrite/src/rule.rs @@ -30,11 +30,16 @@ pub enum RuleConfig { impl Rule { /// Creates a new `Rule`. pub fn new( - host_match: String, - host_replacement: String, - path_prefix_match: String, - path_prefix_replacement: String, + host_match: impl Into<String>, + host_replacement: impl Into<String>, + path_prefix_match: impl Into<String>, + path_prefix_replacement: impl Into<String>, ) -> Result<Self, RuleParseError> { + let host_match = host_match.into(); + let host_replacement = host_replacement.into(); + let path_prefix_match = path_prefix_match.into(); + let path_prefix_replacement = path_prefix_replacement.into(); + fn validate_host(s: &str) -> Result<(), RuleParseError> { PkgUri::new_repository(s.to_owned()).map_err(|_err| RuleParseError::InvalidHost)?; Ok(()) @@ -165,13 +170,8 @@ mod serde_tests { macro_rules! rule { ($host_match:expr => $host_replacement:expr, $path_prefix_match:expr => $path_prefix_replacement:expr) => { - Rule::new( - $host_match.to_owned(), - $host_replacement.to_owned(), - $path_prefix_match.to_owned(), - $path_prefix_replacement.to_owned(), - ) - .unwrap() + Rule::new($host_match, $host_replacement, $path_prefix_match, $path_prefix_replacement) + .unwrap() }; } @@ -322,19 +322,19 @@ mod rule_tests { #[test] fn $test_name() { let error = Rule::new( - $host_match.to_owned(), - $host_replacement.to_owned(), - $path_prefix_match.to_owned(), - $path_prefix_replacement.to_owned() + $host_match, + $host_replacement, + $path_prefix_match, + $path_prefix_replacement, ) .expect_err("should have failed to parse"); assert_eq!(error, $error); let error = Rule::new( - $host_replacement.to_owned(), - $host_match.to_owned(), - $path_prefix_replacement.to_owned(), - $path_prefix_match.to_owned() + $host_replacement, + $host_match, + $path_prefix_replacement, + $path_prefix_match, ) .expect_err("should have failed to parse"); assert_eq!(error, $error); diff --git a/garnet/tests/amberctl/BUILD.gn b/garnet/tests/amberctl/BUILD.gn index 0faf5ec4ed6a5d0dfe0052d89cc5f00a8d41332b..59df86f799b7f61717419e7ec33d9f509dc3bf89 100644 --- a/garnet/tests/amberctl/BUILD.gn +++ b/garnet/tests/amberctl/BUILD.gn @@ -13,10 +13,15 @@ rustc_library("driver") { with_unit_tests = true deps = [ + "//garnet/lib/rust/fidl_fuchsia_pkg_ext", + "//garnet/lib/rust/fuchsia_uri", + "//garnet/lib/rust/fuchsia_uri_rewrite", "//garnet/public/lib/fidl/rust/fidl", "//garnet/public/rust/fuchsia-async", "//garnet/public/rust/fuchsia-component", "//sdk/fidl/fuchsia.amber:fuchsia.amber-rustc", + "//sdk/fidl/fuchsia.pkg:fuchsia.pkg-rustc", + "//sdk/fidl/fuchsia.pkg.rewrite:fuchsia.pkg.rewrite-rustc", "//sdk/fidl/fuchsia.sys:fuchsia.sys-rustc", "//third_party/rust_crates:failure", "//third_party/rust_crates:hex", diff --git a/garnet/tests/amberctl/src/lib.rs b/garnet/tests/amberctl/src/lib.rs index c23983c89d539d01df422729708ade90d359ae73..e8938d8b7f61a72972db79dbad2d32179b42ab8f 100644 --- a/garnet/tests/amberctl/src/lib.rs +++ b/garnet/tests/amberctl/src/lib.rs @@ -6,13 +6,23 @@ #![cfg(test)] use { + failure::Error, fidl_fuchsia_amber::{ControlMarker as AmberMarker, ControlProxy as AmberProxy}, + fidl_fuchsia_pkg::{RepositoryManagerMarker, RepositoryManagerProxy}, + fidl_fuchsia_pkg_ext::{ + MirrorConfigBuilder, RepositoryConfig, RepositoryConfigBuilder, RepositoryKey, + }, + fidl_fuchsia_pkg_rewrite::{ + EngineMarker as RewriteEngineMarker, EngineProxy as RewriteEngineProxy, + }, fidl_fuchsia_sys::TerminationReason, fuchsia_async as fasync, fuchsia_component::{ client::{App, AppBuilder, Stdio}, server::{NestedEnvironment, ServiceFs}, }, + fuchsia_uri::pkg_uri::RepoUri, + fuchsia_uri_rewrite::Rule, futures::prelude::*, std::{convert::TryInto, fs::File}, }; @@ -23,6 +33,10 @@ use types::SourceConfigBuilder; const ROOT_KEY_1: &str = "be0b983f7396da675c40c6b93e47fced7c1e9ea8a32a1fe952ba8f519760b307"; const ROOT_KEY_2: &str = "00112233445566778899aabbccddeeffffeeddccbbaa99887766554433221100"; +fn amberctl() -> AppBuilder { + AppBuilder::new("fuchsia-pkg://fuchsia.com/amberctl-tests#meta/amberctl.cmx".to_owned()) +} + struct Mounts { misc: tempfile::TempDir, data_amber: tempfile::TempDir, @@ -39,11 +53,14 @@ impl Mounts { struct Proxies { amber: AmberProxy, + repo_manager: RepositoryManagerProxy, + rewrite_engine: RewriteEngineProxy, } struct TestEnv { _amber: App, - mounts: Mounts, + _pkg_resolver: App, + _mounts: Mounts, env: NestedEnvironment, proxies: Proxies, } @@ -69,43 +86,67 @@ impl TestEnv { ) .expect("/data/amber to mount"); + let mut pkg_resolver = AppBuilder::new( + "fuchsia-pkg://fuchsia.com/pkg_resolver#meta/pkg_resolver.cmx".to_owned(), + ); + let mut fs = ServiceFs::new(); - fs.add_proxy_service_to::<AmberMarker, _>(amber.directory_request().unwrap().clone()); + fs.add_proxy_service_to::<AmberMarker, _>(amber.directory_request().unwrap().clone()) + .add_proxy_service_to::<RepositoryManagerMarker, _>( + pkg_resolver.directory_request().unwrap().clone(), + ) + .add_proxy_service_to::<RewriteEngineMarker, _>( + pkg_resolver.directory_request().unwrap().clone(), + ); + let env = fs .create_salted_nested_environment("amberctl_env") .expect("nested environment to create successfully"); fasync::spawn(fs.collect()); let amber = amber.spawn(env.launcher()).expect("amber to launch"); + let pkg_resolver = pkg_resolver.spawn(env.launcher()).expect("amber to launch"); let amber_proxy = env.connect_to_service::<AmberMarker>().expect("connect to amber"); + let repo_manager_proxy = env + .connect_to_service::<RepositoryManagerMarker>() + .expect("connect to repository manager"); + let rewrite_engine_proxy = + env.connect_to_service::<RewriteEngineMarker>().expect("connect to rewrite engine"); - Self { _amber: amber, mounts, env, proxies: Proxies { amber: amber_proxy } } + Self { + _amber: amber, + _pkg_resolver: pkg_resolver, + _mounts: mounts, + env, + proxies: Proxies { + amber: amber_proxy, + repo_manager: repo_manager_proxy, + rewrite_engine: rewrite_engine_proxy, + }, + } } - /// Tear down the test environment, retaining the state directories. - fn into_mounts(self) -> Mounts { - self.mounts - } + async fn _run_amberctl(&self, builder: AppBuilder) { + let fut = + builder.stderr(Stdio::Inherit).output(self.env.launcher()).expect("amberctl to launch"); + let output = await!(fut).expect("amberctl to run"); - /// Re-create the test environment, re-using the existing temporary state directories. - fn restart(self) -> Self { - Self::new_with_mounts(self.into_mounts()) + assert_eq!(output.exit_status.reason(), TerminationReason::Exited); + assert!( + output.exit_status.success(), + "amberctl exited with {}\nSTDOUT\n{}\nSTDOUT", + output.exit_status.code(), + String::from_utf8_lossy(&output.stdout), + ); } - async fn run_amberctl<'a>(&'a self, args: &'a [&'a str]) { - let fut = AppBuilder::new( - "fuchsia-pkg://fuchsia.com/amberctl-tests#meta/amberctl.cmx".to_owned(), - ) - .args(args.into_iter().map(|s| *s)) - .add_dir_to_namespace( - "/sources".to_string(), - File::open("/pkg/data/sources").expect("/pkg/data/sources to exist"), - ) - .expect("/sources to mount") - .stderr(Stdio::Inherit) - .output(self.env.launcher()) - .expect("amberctl to launch"); + async fn run_amberctl<'a>(&'a self, args: &'a [impl std::fmt::Debug + AsRef<str>]) { + let fut = amberctl() + .args(args.into_iter().map(|s| s.as_ref())) + .stderr(Stdio::Inherit) + .output(self.env.launcher()) + .expect("amberctl to launch"); let output = await!(fut).expect("amberctl to run"); assert_eq!(output.exit_status.reason(), TerminationReason::Exited); @@ -118,6 +159,32 @@ impl TestEnv { ); } + async fn run_amberctl_add_static_src(&self, name: &'static str) { + await!(self._run_amberctl( + amberctl() + .add_dir_to_namespace( + "/configs".to_string(), + File::open("/pkg/data/sources").expect("/pkg/data/sources to exist"), + ) + .expect("static /configs to mount") + .args(["add_src", "-f"].into_iter().cloned()) + .arg(format!("/configs/{}", name)) + )); + } + + async fn run_amberctl_add_src(&self, source: types::SourceConfig) { + let mut config_file = tempfile::tempfile().expect("temp config file to create"); + serde_json::to_writer(&mut config_file, &source).expect("source config to serialize"); + + await!(self._run_amberctl( + amberctl() + .add_dir_to_namespace("/configs/test.json".to_string(), config_file) + .expect("static /configs to mount") + // Run amberctl in non-exclusive mode so it doesn't disable existing source configs + .args(["add_src", "-x", "-f", "/configs/test.json"].iter().map(|s| *s)) + )); + } + async fn amber_list_sources(&self) -> Vec<types::SourceConfig> { let sources = await!(self.proxies.amber.list_srcs()).unwrap(); @@ -130,51 +197,90 @@ impl TestEnv { sources.sort_unstable(); sources } + + async fn resolver_list_repos(&self) -> Vec<RepositoryConfig> { + let (iterator, iterator_server_end) = fidl::endpoints::create_proxy().unwrap(); + self.proxies.repo_manager.list(iterator_server_end).unwrap(); + await!(collect_iterator(|| iterator.next())).unwrap() + } + + async fn rewrite_engine_list_rules(&self) -> Vec<Rule> { + let (iterator, iterator_server_end) = fidl::endpoints::create_proxy().unwrap(); + self.proxies.rewrite_engine.list(iterator_server_end).unwrap(); + await!(collect_iterator(|| iterator.next())).unwrap() + } +} + +async fn collect_iterator<F, E, I, O>(mut next: impl FnMut() -> F) -> Result<Vec<O>, Error> +where + F: Future<Output = Result<Vec<I>, fidl::Error>>, + I: TryInto<O, Error = E>, + Error: From<E>, +{ + let mut res = Vec::new(); + loop { + let more = await!(next())?; + if more.is_empty() { + break; + } + res.extend(more.into_iter().map(|cfg| cfg.try_into()).collect::<Result<Vec<_>, _>>()?); + } + Ok(res) } struct SourceConfigGenerator { - builder: SourceConfigBuilder, - root_id: String, - root_url: String, + id_prefix: String, n: usize, } impl SourceConfigGenerator { - fn new(builder: SourceConfigBuilder) -> Self { - let config = builder.clone().build(); - Self { - root_id: config.id().to_owned(), - root_url: config.repo_url().to_owned(), - builder, - n: 0, - } + fn new(id_prefix: impl Into<String>) -> Self { + Self { id_prefix: id_prefix.into(), n: 0 } } } impl Iterator for SourceConfigGenerator { - type Item = types::SourceConfigBuilder; + type Item = (types::SourceConfigBuilder, RepositoryConfigBuilder); fn next(&mut self) -> Option<Self::Item> { - let id = format!("{}{:02}", &self.root_id, self.n); - let url = format!("{}/{:02}", &self.root_url, self.n); + let id = format!("{}{:02}", &self.id_prefix, self.n); + let repo_url = format!("fuchsia-pkg://{}", &id); + let mirror_url = format!("http://example.com/{}", &id); self.n += 1; - Some(self.builder.clone().id(id).repo_url(url)) + Some(( + SourceConfigBuilder::new(id) + .repo_url(mirror_url.clone()) + .add_root_key(ROOT_KEY_1) + .auto(true), + RepositoryConfigBuilder::new(RepoUri::parse(&repo_url).unwrap()) + .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_1).unwrap())) + .add_mirror(MirrorConfigBuilder::new(mirror_url).subscribe(true)), + )) } } +fn make_test_repo_config() -> RepositoryConfig { + RepositoryConfigBuilder::new("fuchsia-pkg://test".parse().unwrap()) + .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_1).unwrap())) + .add_mirror(MirrorConfigBuilder::new("http://example.com").subscribe(true)) + .build() +} + #[fasync::run_singlethreaded(test)] -async fn test_amber_starts_with_no_sources() { +async fn test_services_start_with_no_config() { let env = TestEnv::new(); assert_eq!(await!(env.amber_list_sources()), vec![]); + assert_eq!(await!(env.resolver_list_repos()), vec![]); + assert_eq!(await!(env.rewrite_engine_list_rules()), vec![]); } #[fasync::run_singlethreaded(test)] async fn test_add_src() { let env = TestEnv::new(); - await!(env.run_amberctl(&["add_src", "-f", "/sources/test.json"])); + await!(env.run_amberctl_add_static_src("test.json")); let cfg_test = SourceConfigBuilder::new("test") .repo_url("http://example.com") @@ -183,44 +289,43 @@ async fn test_add_src() { .add_root_key(ROOT_KEY_1) .build(); - assert_eq!(await!(env.amber_list_sources()), vec![cfg_test.clone()]); - - // Ensure source configs persist across service restarts - let env = env.restart(); assert_eq!(await!(env.amber_list_sources()), vec![cfg_test]); + assert_eq!(await!(env.resolver_list_repos()), vec![make_test_repo_config()]); + assert_eq!( + await!(env.rewrite_engine_list_rules()), + vec![Rule::new("fuchsia.com", "test", "/", "/").unwrap()] + ); } #[fasync::run_singlethreaded(test)] async fn test_add_src_disables_other_sources() { let env = TestEnv::new(); - let configs = SourceConfigGenerator::new( - SourceConfigBuilder::new("test") - .repo_url("http://example.com") - .rate_period(60) - .auto(true) - .add_root_key(ROOT_KEY_1), - ) - .take(3) - .collect::<Vec<_>>(); - - for config in &configs { - assert_eq!( - await!(env.proxies.amber.add_src(&mut config.clone().build().into())).unwrap(), - true - ); + let configs = SourceConfigGenerator::new("testgen").take(3).collect::<Vec<_>>(); + + for (config, _) in &configs { + await!(env.run_amberctl_add_src(config.clone().build().into())); } - await!(env.run_amberctl(&["add_src", "-f", "/sources/test.json"])); + await!(env.run_amberctl_add_static_src("test.json")); - let mut configs = - configs.into_iter().map(|builder| builder.enabled(false).build()).collect::<Vec<_>>(); + let mut source_configs = vec![]; + let mut repo_configs = vec![make_test_repo_config()]; + for (source_config, repo_config) in configs { + source_configs.push(source_config.enabled(false).build()); + repo_configs.push(repo_config.build()); + } let test_config = serde_json::from_reader(File::open("/pkg/data/sources/test.json").unwrap()).unwrap(); - configs.push(test_config); - configs.sort_unstable(); + source_configs.push(test_config); + source_configs.sort_unstable(); - assert_eq!(await!(env.amber_list_sources()), configs); + assert_eq!(await!(env.amber_list_sources()), source_configs); + assert_eq!(await!(env.resolver_list_repos()), repo_configs); + assert_eq!( + await!(env.rewrite_engine_list_rules()), + vec![Rule::new("fuchsia.com", "test", "/", "/").unwrap()] + ); } #[fasync::run_singlethreaded(test)] @@ -239,30 +344,55 @@ async fn test_rm_src() { .add_root_key(ROOT_KEY_2) .build(); - assert_eq!(await!(env.proxies.amber.add_src(&mut cfg_a.clone().into())).unwrap(), true); - assert_eq!(await!(env.proxies.amber.add_src(&mut cfg_b.clone().into())).unwrap(), true); - - await!(env.run_amberctl(&["rm_src", "-n", "b"])); - assert_eq!(await!(env.amber_list_sources()), vec![cfg_a]); + await!(env.run_amberctl_add_src(cfg_a.clone().into())); + await!(env.run_amberctl_add_src(cfg_b.clone().into())); await!(env.run_amberctl(&["rm_src", "-n", "a"])); + assert_eq!(await!(env.amber_list_sources()), vec![cfg_b]); + assert_eq!( + await!(env.resolver_list_repos()), + vec![RepositoryConfigBuilder::new("fuchsia-pkg://b".parse().unwrap()) + .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_2).unwrap())) + .add_mirror(MirrorConfigBuilder::new("http://example.com/b")) + .build()] + ); + // rm_src removes all rules, so no source remains enabled. + assert_eq!(await!(env.rewrite_engine_list_rules()), vec![]); + + await!(env.run_amberctl(&["rm_src", "-n", "b"])); assert_eq!(await!(env.amber_list_sources()), vec![]); + assert_eq!(await!(env.resolver_list_repos()), vec![]); + assert_eq!(await!(env.rewrite_engine_list_rules()), vec![]); } #[fasync::run_singlethreaded(test)] async fn test_enable_src() { let env = TestEnv::new(); - let cfg = SourceConfigBuilder::new("test") + let source = SourceConfigBuilder::new("test") .repo_url("http://example.com") .enabled(false) .add_root_key(ROOT_KEY_1); - assert_eq!(await!(env.proxies.amber.add_src(&mut cfg.clone().build().into())).unwrap(), true); + let repo = RepositoryConfigBuilder::new("fuchsia-pkg://test".parse().unwrap()) + .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_1).unwrap())) + .add_mirror(MirrorConfigBuilder::new("http://example.com")) + .build(); + + await!(env.run_amberctl_add_src(source.clone().build().into())); + + assert_eq!(await!(env.resolver_list_repos()), vec![repo.clone()]); + // Adding a disabled source does not add a rewrite rule for it. + assert_eq!(await!(env.rewrite_engine_list_rules()), vec![]); await!(env.run_amberctl(&["enable_src", "-n", "test"])); - assert_eq!(await!(env.amber_list_sources()), vec![cfg.enabled(true).build()]); + assert_eq!(await!(env.amber_list_sources()), vec![source.enabled(true).build()]); + assert_eq!(await!(env.resolver_list_repos()), vec![repo]); + assert_eq!( + await!(env.rewrite_engine_list_rules()), + vec![Rule::new("fuchsia.com", "test", "/", "/").unwrap()] + ); } #[fasync::run_singlethreaded(test)] @@ -270,33 +400,39 @@ async fn test_enable_src_disables_other_sources() { let env = TestEnv::new(); // add some enabled sources - let mut gen = SourceConfigGenerator::new( - SourceConfigBuilder::new("test").repo_url("http://example.com").add_root_key(ROOT_KEY_1), - ); + let mut gen = SourceConfigGenerator::new("test"); let configs = gen.by_ref().take(3).collect::<Vec<_>>(); - for config in &configs { - assert_eq!( - await!(env.proxies.amber.add_src(&mut config.clone().build().into())).unwrap(), - true - ); + for (config, _) in &configs { + await!(env.run_amberctl_add_src(config.clone().build().into())); } - // add an initially disabled source. - let config = gen.next().unwrap().enabled(false); + // add an initially disabled source + let (config, repo) = gen.next().unwrap(); + let config = config.enabled(false); let c = config.clone().build(); let id = c.id().to_owned(); - assert_eq!(await!(env.proxies.amber.add_src(&mut c.into())).unwrap(), true); + await!(env.run_amberctl_add_src(c.into())); // enable that source let args = ["enable_src", "-n", &id]; await!(env.run_amberctl(&args)); // verify the enabled sources are now disabled and the disabled source is now enabled - let mut configs = - configs.into_iter().map(|builder| builder.enabled(false).build()).collect::<Vec<_>>(); - configs.push(config.enabled(true).build()); - configs.sort_unstable(); - assert_eq!(await!(env.amber_list_sources()), configs); + let mut source_configs = vec![]; + let mut repo_configs = vec![]; + for (source_config, repo_config) in configs { + source_configs.push(source_config.enabled(false).build()); + repo_configs.push(repo_config.build()); + } + source_configs.push(config.enabled(true).build()); + repo_configs.push(repo.build()); + source_configs.sort_unstable(); + assert_eq!(await!(env.amber_list_sources()), source_configs); + assert_eq!(await!(env.resolver_list_repos()), repo_configs); + assert_eq!( + await!(env.rewrite_engine_list_rules()), + vec![Rule::new("fuchsia.com", id, "/", "/").unwrap()] + ); } #[fasync::run_singlethreaded(test)] @@ -313,8 +449,8 @@ async fn test_disable_src() { .rate_period(60) .add_root_key(ROOT_KEY_2); - assert_eq!(await!(env.proxies.amber.add_src(&mut cfg_a.clone().build().into())).unwrap(), true); - assert_eq!(await!(env.proxies.amber.add_src(&mut cfg_b.clone().build().into())).unwrap(), true); + await!(env.run_amberctl_add_src(cfg_a.clone().build().into())); + await!(env.run_amberctl_add_src(cfg_b.clone().build().into())); await!(env.run_amberctl(&["disable_src", "-n", "a"])); @@ -322,4 +458,19 @@ async fn test_disable_src() { await!(env.amber_list_sources()), vec![cfg_a.enabled(false).build(), cfg_b.enabled(true).build().into(),] ); + assert_eq!( + await!(env.resolver_list_repos()), + vec![ + RepositoryConfigBuilder::new("fuchsia-pkg://a".parse().unwrap()) + .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_1).unwrap())) + .add_mirror(MirrorConfigBuilder::new("http://example.com/a")) + .build(), + RepositoryConfigBuilder::new("fuchsia-pkg://b".parse().unwrap()) + .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_2).unwrap())) + .add_mirror(MirrorConfigBuilder::new("http://example.com/b")) + .build(), + ] + ); + // disabling any source clears all rewrite rules. + assert_eq!(await!(env.rewrite_engine_list_rules()), vec![]); } diff --git a/garnet/tests/amberctl/src/types.rs b/garnet/tests/amberctl/src/types.rs index cbf00faec8ca244399e166dfc67bdd143417df22..eedbbf7b8fc031b5da14dd20b9169fdca97f15e3 100644 --- a/garnet/tests/amberctl/src/types.rs +++ b/garnet/tests/amberctl/src/types.rs @@ -35,11 +35,6 @@ impl SourceConfigBuilder { } } - pub fn id(mut self, value: impl Into<String>) -> Self { - self.config.id = value.into(); - self - } - pub fn repo_url(mut self, value: impl Into<String>) -> Self { self.config.repo_url = value.into(); self.config.blob_repo_url = format!("{}/blobs", self.config.repo_url); @@ -98,9 +93,6 @@ impl SourceConfig { pub fn id(&self) -> &str { self.id.as_str() } - pub fn repo_url(&self) -> &str { - self.repo_url.as_str() - } } impl Into<fidl::SourceConfig> for SourceConfig {