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