Skip to main content

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