bootc_lib/bootc_composefs/
repo.rs

1use fn_error_context::context;
2use std::sync::Arc;
3
4use anyhow::{Context, Result};
5
6use cfsctl::composefs;
7use cfsctl::composefs_boot;
8use cfsctl::composefs_oci;
9use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
10use composefs_boot::{BootOps, bootloader::BootEntry as ComposefsBootEntry};
11use composefs_oci::{
12    PullResult, image::create_filesystem as create_composefs_filesystem, pull as composefs_oci_pull,
13};
14
15use ostree_ext::container::ImageReference as OstreeExtImgRef;
16
17use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
18
19use crate::install::{RootSetup, State};
20
21pub(crate) fn open_composefs_repo(rootfs_dir: &Dir) -> Result<crate::store::ComposefsRepository> {
22    crate::store::ComposefsRepository::open_path(rootfs_dir, "composefs")
23        .context("Failed to open composefs repository")
24}
25
26pub(crate) async fn initialize_composefs_repository(
27    state: &State,
28    root_setup: &RootSetup,
29    allow_missing_fsverity: bool,
30) -> Result<PullResult<Sha512HashValue>> {
31    const COMPOSEFS_REPO_INIT_JOURNAL_ID: &str = "5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9";
32
33    let rootfs_dir = &root_setup.physical_root;
34    let image_name = &state.source.imageref.name;
35    let transport = &state.source.imageref.transport;
36
37    tracing::info!(
38        message_id = COMPOSEFS_REPO_INIT_JOURNAL_ID,
39        bootc.operation = "repository_init",
40        bootc.source_image = %image_name,
41        bootc.transport = %transport,
42        bootc.allow_missing_fsverity = allow_missing_fsverity,
43        "Initializing composefs repository for image {}:{}",
44        transport,
45        image_name
46    );
47
48    crate::store::ensure_composefs_dir(rootfs_dir)?;
49
50    let mut repo = open_composefs_repo(rootfs_dir)?;
51    repo.set_insecure(allow_missing_fsverity);
52
53    let OstreeExtImgRef {
54        name: image_name,
55        transport,
56    } = &state.source.imageref;
57
58    let mut config = crate::deploy::new_proxy_config();
59    ostree_ext::container::merge_default_container_proxy_opts(&mut config)?;
60
61    // transport's display is already of type "<transport_type>:"
62    composefs_oci_pull(
63        &Arc::new(repo),
64        &format!("{transport}{image_name}"),
65        None,
66        Some(config),
67    )
68    .await
69}
70
71/// skopeo (in composefs-rs) doesn't understand "registry:"
72/// This function will convert it to "docker://" and return the image ref
73///
74/// Ex
75/// docker://quay.io/some-image
76/// containers-storage:some-image
77/// docker-daemon:some-image-id
78pub(crate) fn get_imgref(transport: &str, image: &str) -> String {
79    let img = image.strip_prefix(":").unwrap_or(&image);
80    let transport = transport.strip_suffix(":").unwrap_or(&transport);
81
82    if transport == "registry" || transport == "docker://" {
83        format!("docker://{img}")
84    } else if transport == "docker-daemon" {
85        format!("docker-daemon:{img}")
86    } else {
87        format!("{transport}:{img}")
88    }
89}
90
91/// Pulls the `image` from `transport` into a composefs repository at /sysroot
92/// Checks for boot entries in the image and returns them
93#[context("Pulling composefs repository")]
94pub(crate) async fn pull_composefs_repo(
95    transport: &String,
96    image: &String,
97    allow_missing_fsverity: bool,
98) -> Result<(
99    crate::store::ComposefsRepository,
100    Vec<ComposefsBootEntry<Sha512HashValue>>,
101    Sha512HashValue,
102    crate::store::ComposefsFilesystem,
103)> {
104    const COMPOSEFS_PULL_JOURNAL_ID: &str = "4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8";
105
106    tracing::info!(
107        message_id = COMPOSEFS_PULL_JOURNAL_ID,
108        bootc.operation = "pull",
109        bootc.source_image = image,
110        bootc.transport = transport,
111        bootc.allow_missing_fsverity = allow_missing_fsverity,
112        "Pulling composefs image {}:{}",
113        transport,
114        image
115    );
116
117    let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?;
118
119    let mut repo = open_composefs_repo(&rootfs_dir).context("Opening composefs repo")?;
120    repo.set_insecure(allow_missing_fsverity);
121
122    let final_imgref = get_imgref(transport, image);
123
124    tracing::debug!("Image to pull {final_imgref}");
125
126    let mut config = crate::deploy::new_proxy_config();
127    ostree_ext::container::merge_default_container_proxy_opts(&mut config)?;
128
129    let pull_result = composefs_oci_pull(&Arc::new(repo), &final_imgref, None, Some(config))
130        .await
131        .context("Pulling composefs repo")?;
132
133    tracing::info!(
134        message_id = COMPOSEFS_PULL_JOURNAL_ID,
135        id = pull_result.config_digest,
136        verity = pull_result.config_verity.to_hex(),
137        "Pulled image into repository"
138    );
139
140    let mut repo = open_composefs_repo(&rootfs_dir)?;
141    repo.set_insecure(allow_missing_fsverity);
142
143    let mut fs: crate::store::ComposefsFilesystem =
144        create_composefs_filesystem(&repo, &pull_result.config_digest, None)
145            .context("Failed to create composefs filesystem")?;
146
147    let entries = fs.transform_for_boot(&repo)?;
148    let id = fs.commit_image(&repo, None)?;
149
150    Ok((repo, entries, id, fs))
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    const IMAGE_NAME: &str = "quay.io/example/image:latest";
158
159    #[test]
160    fn test_get_imgref_registry_transport() {
161        assert_eq!(
162            get_imgref("registry:", IMAGE_NAME),
163            format!("docker://{IMAGE_NAME}")
164        );
165    }
166
167    #[test]
168    fn test_get_imgref_containers_storage() {
169        assert_eq!(
170            get_imgref("containers-storage", IMAGE_NAME),
171            format!("containers-storage:{IMAGE_NAME}")
172        );
173
174        assert_eq!(
175            get_imgref("containers-storage:", IMAGE_NAME),
176            format!("containers-storage:{IMAGE_NAME}")
177        );
178    }
179
180    #[test]
181    fn test_get_imgref_edge_cases() {
182        assert_eq!(
183            get_imgref("registry", IMAGE_NAME),
184            format!("docker://{IMAGE_NAME}")
185        );
186    }
187
188    #[test]
189    fn test_get_imgref_docker_daemon_transport() {
190        assert_eq!(
191            get_imgref("docker-daemon", IMAGE_NAME),
192            format!("docker-daemon:{IMAGE_NAME}")
193        );
194    }
195}