isolated_ota_env/
lib.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#![allow(clippy::let_unit_value)]
6
7use anyhow::{Context, Error};
8use async_trait::async_trait;
9use fidl::endpoints::{ClientEnd, DiscoverableProtocolMarker, Proxy, ServerEnd};
10use fidl_fuchsia_io::DirectoryProxy;
11use fidl_fuchsia_paver::PaverRequestStream;
12use fidl_fuchsia_pkg_ext::RepositoryConfigs;
13use fuchsia_component::server::ServiceFs;
14use fuchsia_component_test::LocalComponentHandles;
15use fuchsia_merkle::Hash;
16use fuchsia_pkg_testing::serve::ServedRepository;
17use fuchsia_pkg_testing::{Package, RepositoryBuilder};
18use fuchsia_sync::Mutex;
19use futures::prelude::*;
20use isolated_ota::{OmahaConfig, UpdateUrlSource};
21use mock_omaha_server::{
22    OmahaResponse, OmahaServer, OmahaServerBuilder, ResponseAndMetadata, ResponseMap,
23};
24use mock_paver::{MockPaverService, MockPaverServiceBuilder};
25use std::collections::{BTreeMap, BTreeSet};
26use std::io::Write;
27use std::str::FromStr;
28use std::sync::Arc;
29use tempfile::TempDir;
30use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
31
32pub const GLOBAL_SSL_CERTS_PATH: &str = "/config/ssl";
33const EMPTY_REPO_PATH: &str = "/pkg/empty-repo";
34const TEST_CERTS_PATH: &str = "/pkg/data/ssl";
35const TEST_REPO_URL: &str = "fuchsia-pkg://integration.test.fuchsia.com";
36
37pub enum OmahaState {
38    /// Don't use Omaha for this update, instead use the provided, or default, update URL.
39    Disabled(Option<url::Url>),
40    /// Set up an Omaha server automatically.
41    Auto(OmahaResponse),
42    /// Pass the given OmahaConfig to Omaha.
43    Manual(OmahaConfig),
44}
45
46pub struct TestParams {
47    pub blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
48    pub board: String,
49    pub channel: String,
50    pub expected_blobfs_contents: BTreeSet<Hash>,
51    pub paver: Arc<MockPaverService>,
52    pub repo_config_dir: TempDir,
53    pub ssl_certs: DirectoryProxy,
54    pub update_merkle: Hash,
55    pub version: String,
56    pub update_url_source: UpdateUrlSource,
57    pub paver_connector: ClientEnd<fio::DirectoryMarker>,
58}
59
60/// Connects the local component to a mock paver.
61///
62/// Unlike other mocks, the `fuchsia.paver.Paver` is serviced by [`isolated_ota_env::TestEnv`], so
63/// this function proxies to the given `paver_dir_proxy` which is expected to host a
64/// file named "fuchsia.paver.Paver" which implements the `fuchsia.paver.Paver` FIDL protocol.
65pub async fn expose_mock_paver(
66    handles: LocalComponentHandles,
67    paver_dir_proxy: fio::DirectoryProxy,
68) -> Result<(), Error> {
69    let mut fs = ServiceFs::new();
70
71    fs.dir("svc").add_service_connector(
72        move |server_end: ServerEnd<fidl_fuchsia_paver::PaverMarker>| {
73            fdio::service_connect_at(
74                paver_dir_proxy.as_channel().as_ref(),
75                &format!("/{}", fidl_fuchsia_paver::PaverMarker::PROTOCOL_NAME),
76                server_end.into_channel(),
77            )
78            .expect("failed to connect to paver service node");
79        },
80    );
81
82    fs.serve_connection(handles.outgoing_dir).expect("failed to serve paver fs connection");
83    fs.collect::<()>().await;
84    Ok(())
85}
86
87#[async_trait(?Send)]
88pub trait TestExecutor<R> {
89    async fn run(&self, params: TestParams) -> R;
90}
91
92pub struct TestEnvBuilder<R> {
93    blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
94    board: String,
95    channel: String,
96    omaha: OmahaState,
97    packages: Vec<Package>,
98    paver: MockPaverServiceBuilder,
99    repo_config: Option<RepositoryConfigs>,
100    version: String,
101    test_executor: Option<Box<dyn TestExecutor<R>>>,
102    // The zbi and optional vbmeta contents.
103    fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
104    // The zbi and optional vbmeta contents of the recovery partition.
105    recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
106    firmware_images: BTreeMap<String, Vec<u8>>,
107}
108
109impl<R> TestEnvBuilder<R> {
110    #[allow(clippy::new_without_default)]
111    pub fn new() -> Self {
112        TestEnvBuilder {
113            blobfs: None,
114            board: "test-board".to_owned(),
115            channel: "test".to_owned(),
116            omaha: OmahaState::Disabled(Some(format!("{TEST_REPO_URL}/update").parse().unwrap())),
117            packages: vec![],
118            paver: MockPaverServiceBuilder::new(),
119            repo_config: None,
120            version: "0.1.2.3".to_owned(),
121            test_executor: None,
122            fuchsia_image: None,
123            recovery_image: None,
124            firmware_images: BTreeMap::new(),
125        }
126    }
127
128    /// Add a package to the repository generated by this TestEnvBuilder.
129    /// The package will also be listed in the generated update package
130    /// so that it will be downloaded as part of the OTA.
131    pub fn add_package(mut self, pkg: Package) -> Self {
132        self.packages.push(pkg);
133        self
134    }
135
136    pub fn blobfs(mut self, client: ClientEnd<fio::DirectoryMarker>) -> Self {
137        self.blobfs = Some(client);
138        self
139    }
140
141    /// Provide a TUF repository configuration to the package resolver.
142    /// This will override the repository that the builder would otherwise generate.
143    pub fn repo_config(mut self, repo: RepositoryConfigs) -> Self {
144        self.repo_config = Some(repo);
145        self
146    }
147
148    /// Enable/disable Omaha. OmahaState::Auto will automatically set up an Omaha server and tell
149    /// the updater to use it.
150    pub fn omaha_state(mut self, state: OmahaState) -> Self {
151        self.omaha = state;
152        self
153    }
154
155    /// Mutate the MockPaverServiecBuilder used by this TestEnvBuilder.
156    pub fn paver<F>(mut self, func: F) -> Self
157    where
158        F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
159    {
160        self.paver = func(self.paver);
161        self
162    }
163
164    pub fn test_executor(mut self, executor: Box<dyn TestExecutor<R>>) -> Self {
165        self.test_executor = Some(executor);
166        self
167    }
168
169    /// The zbi and optional vbmeta images to write.
170    pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
171        assert_eq!(self.fuchsia_image, None);
172        self.fuchsia_image = Some((zbi, vbmeta));
173        self
174    }
175
176    /// The zbi and optional vbmeta images to write to the recovery partition.
177    pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
178        assert_eq!(self.recovery_image, None);
179        self.recovery_image = Some((zbi, vbmeta));
180        self
181    }
182
183    /// A firmware image to write.
184    pub fn firmware_image(mut self, type_: String, content: Vec<u8>) -> Self {
185        assert_eq!(self.firmware_images.insert(type_, content), None);
186        self
187    }
188
189    /// Turn this |TestEnvBuilder| into a |TestEnv|
190    pub async fn build(mut self) -> Result<TestEnv<R>, Error> {
191        let (repo_config, served_repo, ssl_certs, expected_blobfs_contents, merkle) =
192            if let Some(repo_config) = self.repo_config {
193                // Use the provided repo config. Assume that this means we'll actually want to use
194                // real SSL certificates, and that we don't need to host our own repository.
195                (
196                    repo_config,
197                    None,
198                    fuchsia_fs::directory::open_in_namespace(
199                        GLOBAL_SSL_CERTS_PATH,
200                        fio::PERM_READABLE,
201                    )
202                    .unwrap(),
203                    BTreeSet::new(),
204                    Hash::from_str(
205                        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
206                    )
207                    .expect("make merkle"),
208                )
209            } else {
210                // If no repo config was specified, host a repo containing the provided packages,
211                // and an update package containing given images + all packages in the repo.
212                let mut update =
213                    fuchsia_pkg_testing::UpdatePackageBuilder::new(TEST_REPO_URL.parse().unwrap())
214                        .packages(
215                            self.packages
216                                .iter()
217                                .map(|p| {
218                                    fuchsia_url::PinnedAbsolutePackageUrl::new(
219                                        TEST_REPO_URL.parse().unwrap(),
220                                        p.name().clone(),
221                                        None,
222                                        *p.hash(),
223                                    )
224                                })
225                                .collect::<Vec<_>>(),
226                        )
227                        .firmware_images(self.firmware_images);
228                if let Some((zbi, vbmeta)) = self.fuchsia_image {
229                    update = update.fuchsia_image(zbi, vbmeta);
230                }
231                if let Some((zbi, vbmeta)) = self.recovery_image {
232                    update = update.recovery_image(zbi, vbmeta);
233                }
234                let (update, images) = update.build().await;
235
236                // Do not include the images package, system-updater triggers GC after resolving it.
237                let expected_blobfs_contents = self
238                    .packages
239                    .iter()
240                    .chain([update.as_package()])
241                    .flat_map(|p| p.list_blobs())
242                    .collect();
243
244                let repo = Arc::new(
245                    self.packages
246                        .iter()
247                        .chain([update.as_package(), &images])
248                        .fold(
249                            RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
250                                .add_package(update.as_package()),
251                            |repo, package| repo.add_package(package),
252                        )
253                        .build()
254                        .await
255                        .expect("build repo"),
256                );
257
258                let served_repo = Arc::clone(&repo).server().start().expect("serve repo");
259                let config = RepositoryConfigs::Version1(vec![
260                    served_repo.make_repo_config(TEST_REPO_URL.parse().expect("make repo config")),
261                ]);
262
263                let update_merkle = *update.as_package().hash();
264                // Add the update package to the list of packages, so that TestResult::check_packages
265                // will expect to see the update package's blobs in blobfs.
266                let mut packages = vec![update.into_package()];
267                packages.append(&mut self.packages);
268                (
269                    config,
270                    Some(served_repo),
271                    fuchsia_fs::directory::open_in_namespace(TEST_CERTS_PATH, fio::PERM_READABLE)
272                        .unwrap(),
273                    expected_blobfs_contents,
274                    update_merkle,
275                )
276            };
277
278        let dir = tempfile::tempdir()?;
279        let mut path = dir.path().to_owned();
280        path.push("repo_config.json");
281        let path = path.as_path();
282        let mut file =
283            std::io::BufWriter::new(std::fs::File::create(path).context("creating file")?);
284        serde_json::to_writer(&mut file, &repo_config).unwrap();
285        file.flush().unwrap();
286
287        Ok(TestEnv {
288            blobfs: self.blobfs,
289            board: self.board,
290            channel: self.channel,
291            omaha: self.omaha,
292            expected_blobfs_contents,
293            paver: Arc::new(self.paver.build()),
294            _repo: served_repo,
295            repo_config_dir: dir,
296            ssl_certs,
297            update_merkle: merkle,
298            version: self.version,
299            test_executor: self.test_executor.expect("test executor must be set"),
300        })
301    }
302}
303
304pub struct TestEnv<R> {
305    blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
306    channel: String,
307    omaha: OmahaState,
308    expected_blobfs_contents: BTreeSet<Hash>,
309    paver: Arc<MockPaverService>,
310    _repo: Option<ServedRepository>,
311    repo_config_dir: tempfile::TempDir,
312    ssl_certs: DirectoryProxy,
313    update_merkle: Hash,
314    board: String,
315    version: String,
316    test_executor: Box<dyn TestExecutor<R>>,
317}
318
319impl<R> TestEnv<R> {
320    async fn start_omaha(omaha: OmahaState, merkle: Hash) -> Result<UpdateUrlSource, Error> {
321        match omaha {
322            OmahaState::Disabled(url) => Ok(match url {
323                Some(url) => UpdateUrlSource::UpdateUrl(url),
324                None => UpdateUrlSource::UseDefault,
325            }),
326            OmahaState::Manual(cfg) => Ok(UpdateUrlSource::OmahaConfig(cfg)),
327            OmahaState::Auto(response) => {
328                // Amend the default struct with the expected package hash
329                let mut response = ResponseAndMetadata { response, ..Default::default() };
330                let p: Vec<_> = response.package_name.split("?hash=").collect();
331                assert_eq!(p.len(), 2);
332                response.package_name = format!("{}?hash={}", p[0], merkle);
333                let server = OmahaServerBuilder::default()
334                    .responses_by_appid(
335                        vec![("integration-test-appid".to_string(), response)]
336                            .into_iter()
337                            .collect::<ResponseMap>(),
338                    )
339                    .build()
340                    .unwrap();
341                let addr = OmahaServer::start_and_detach(Arc::new(Mutex::new(server)), None)
342                    .await
343                    .context("Starting omaha server")?;
344                let config =
345                    OmahaConfig { app_id: "integration-test-appid".to_owned(), server_url: addr };
346
347                Ok(UpdateUrlSource::OmahaConfig(config))
348            }
349        }
350    }
351
352    /// Run the update, consuming this |TestEnv| and returning a |TestResult|.
353    pub async fn run(self) -> R {
354        let update_url_source = TestEnv::<R>::start_omaha(self.omaha, self.update_merkle)
355            .await
356            .expect("Starting Omaha server");
357
358        let mut service_fs = ServiceFs::new();
359        let paver_clone = Arc::clone(&self.paver);
360        service_fs.add_fidl_service(move |stream: PaverRequestStream| {
361            fasync::Task::spawn(
362                Arc::clone(&paver_clone)
363                    .run_paver_service(stream)
364                    .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
365            )
366            .detach();
367        });
368
369        let (client, server) =
370            fidl::endpoints::create_endpoints::<fidl_fuchsia_io::DirectoryMarker>();
371        service_fs
372            .serve_connection(server.into_channel().into())
373            .expect("Failed to serve connection");
374        fasync::Task::spawn(service_fs.collect()).detach();
375
376        let params = TestParams {
377            blobfs: self.blobfs,
378            board: self.board,
379            channel: self.channel,
380            expected_blobfs_contents: self.expected_blobfs_contents,
381            paver: self.paver,
382            repo_config_dir: self.repo_config_dir,
383            ssl_certs: self.ssl_certs,
384            update_merkle: self.update_merkle,
385            version: self.version,
386            update_url_source,
387            paver_connector: client,
388        };
389
390        self.test_executor.run(params).await
391    }
392}