bootc_lib/bootc_composefs/
state.rs

1use std::io::Write;
2use std::os::unix::fs::symlink;
3use std::path::Path;
4use std::{fs::create_dir_all, process::Command};
5
6use anyhow::{Context, Result};
7use bootc_initramfs_setup::overlay_transient;
8use bootc_kernel_cmdline::utf8::Cmdline;
9use bootc_mount::tempmount::TempMount;
10use bootc_utils::CommandRunExt;
11use camino::Utf8PathBuf;
12use cap_std_ext::cap_std::ambient_authority;
13use cap_std_ext::cap_std::fs::{Dir, Permissions, PermissionsExt};
14use cap_std_ext::dirext::CapStdExtDirExt;
15use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
16use fn_error_context::context;
17
18use ostree_ext::container::deploy::ORIGIN_CONTAINER;
19use rustix::{
20    fs::{open, Mode, OFlags},
21    path::Arg,
22};
23
24use crate::bootc_composefs::boot::BootType;
25use crate::bootc_composefs::repo::get_imgref;
26use crate::bootc_composefs::status::{get_sorted_type1_boot_entries, ImgConfigManifest};
27use crate::parsers::bls_config::BLSConfigType;
28use crate::store::{BootedComposefs, Storage};
29use crate::{
30    composefs_consts::{
31        COMPOSEFS_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR,
32        ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, SHARED_VAR_PATH,
33        STATE_DIR_RELATIVE,
34    },
35    parsers::bls_config::BLSConfig,
36    spec::ImageReference,
37    utils::path_relative_to,
38};
39
40pub(crate) fn get_booted_bls(boot_dir: &Dir) -> Result<BLSConfig> {
41    let cmdline = Cmdline::from_proc()?;
42    let booted = cmdline
43        .find(COMPOSEFS_CMDLINE)
44        .ok_or_else(|| anyhow::anyhow!("Failed to find composefs parameter in kernel cmdline"))?;
45
46    let sorted_entries = get_sorted_type1_boot_entries(boot_dir, true)?;
47
48    for entry in sorted_entries {
49        match &entry.cfg_type {
50            BLSConfigType::EFI { efi } => {
51                let composefs_param_value = booted.value().ok_or_else(|| {
52                    anyhow::anyhow!("Failed to get composefs kernel cmdline value")
53                })?;
54
55                if efi.as_str().contains(composefs_param_value) {
56                    return Ok(entry);
57                }
58            }
59
60            BLSConfigType::NonEFI { options, .. } => {
61                let Some(opts) = options else {
62                    anyhow::bail!("options not found in bls config")
63                };
64
65                let opts = Cmdline::from(opts);
66
67                if opts.iter().any(|v| v == booted) {
68                    return Ok(entry);
69                }
70            }
71
72            BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config type"),
73        };
74    }
75
76    Err(anyhow::anyhow!("Booted BLS not found"))
77}
78
79/// Mounts an EROFS image and copies the pristine /etc and /var to the deployment's /etc and /var.
80/// Only copies /var for initial installation of deployments (non-staged deployments)
81#[context("Initializing /etc and /var for state")]
82pub(crate) fn initialize_state(
83    sysroot_path: &Utf8PathBuf,
84    erofs_id: &String,
85    state_path: &Utf8PathBuf,
86    initialize_var: bool,
87) -> Result<()> {
88    let sysroot_fd = open(
89        sysroot_path.as_std_path(),
90        OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
91        Mode::empty(),
92    )
93    .context("Opening sysroot")?;
94
95    let composefs_fd = bootc_initramfs_setup::mount_composefs_image(&sysroot_fd, &erofs_id, false)?;
96
97    let tempdir = TempMount::mount_fd(composefs_fd)?;
98
99    // TODO: Replace this with a function to cap_std_ext
100    if initialize_var {
101        Command::new("cp")
102            .args([
103                "-a",
104                "--remove-destination",
105                &format!("{}/var/.", tempdir.dir.path().as_str()?),
106                &format!("{state_path}/var/."),
107            ])
108            .run_capture_stderr()?;
109    }
110
111    let cp_ret = Command::new("cp")
112        .args([
113            "-a",
114            "--remove-destination",
115            &format!("{}/etc/.", tempdir.dir.path().as_str()?),
116            &format!("{state_path}/etc/."),
117        ])
118        .run_capture_stderr();
119
120    cp_ret
121}
122
123/// Adds or updates the provided key/value pairs in the .origin file of the deployment pointed to
124/// by the `deployment_id`
125fn add_update_in_origin(
126    storage: &Storage,
127    deployment_id: &str,
128    section: &str,
129    kv_pairs: &[(&str, &str)],
130) -> Result<()> {
131    let path = Path::new(STATE_DIR_RELATIVE).join(deployment_id);
132
133    let state_dir = storage
134        .physical_root
135        .open_dir(path)
136        .context("Opening state dir")?;
137
138    let origin_filename = format!("{deployment_id}.origin");
139
140    let origin_file = state_dir
141        .read_to_string(&origin_filename)
142        .context("Reading origin file")?;
143
144    let mut ini =
145        tini::Ini::from_string(&origin_file).context("Failed to parse file origin file as ini")?;
146
147    for (key, value) in kv_pairs {
148        ini = ini.section(section).item(*key, *value);
149    }
150
151    state_dir
152        .atomic_replace_with(origin_filename, move |f| -> std::io::Result<_> {
153            f.write_all(ini.to_string().as_bytes())?;
154            f.flush()?;
155
156            let perms = Permissions::from_mode(0o644);
157            f.get_mut().as_file_mut().set_permissions(perms)?;
158
159            Ok(())
160        })
161        .context("Writing to origin file")?;
162
163    Ok(())
164}
165
166/// Updates the currently booted image's target imgref
167pub(crate) fn update_target_imgref_in_origin(
168    storage: &Storage,
169    booted_cfs: &BootedComposefs,
170    imgref: &ImageReference,
171) -> Result<()> {
172    add_update_in_origin(
173        storage,
174        booted_cfs.cmdline.digest.as_ref(),
175        "origin",
176        &[(
177            ORIGIN_CONTAINER,
178            &format!(
179                "ostree-unverified-image:{}",
180                get_imgref(&imgref.transport, &imgref.image)
181            ),
182        )],
183    )
184}
185
186pub(crate) fn update_boot_digest_in_origin(
187    storage: &Storage,
188    digest: &str,
189    boot_digest: &str,
190) -> Result<()> {
191    add_update_in_origin(
192        storage,
193        digest,
194        ORIGIN_KEY_BOOT,
195        &[(ORIGIN_KEY_BOOT_DIGEST, boot_digest)],
196    )
197}
198
199/// Creates and populates the composefs state directory for a deployment.
200///
201/// This function sets up the state directory structure and configuration files
202/// needed for a composefs deployment. It creates the deployment state directory,
203/// copies configuration, sets up the shared `/var` directory, and writes metadata
204/// files including the origin configuration and image information.
205///
206/// # Arguments
207///
208/// * `root_path`         - The root filesystem path (typically `/sysroot`)
209/// * `deployment_id`     - Unique SHA512 hash identifier for this deployment
210/// * `imgref`            - Container image reference for the deployment
211/// * `staged`            - Whether this is a staged deployment (writes to transient state dir)
212/// * `boot_type`         - Boot loader type (`Bls` or `Uki`)
213/// * `boot_digest`       - Optional boot digest for verification
214/// * `container_details` - Container manifest and config used to create this deployment
215///
216/// # State Directory Structure
217///
218/// Creates the following structure under `/sysroot/state/deploy/{deployment_id}/`:
219/// * `etc/`                    - Copy of system configuration files
220/// * `var`                     - Symlink to shared `/var` directory
221/// * `{deployment_id}.origin`  - OSTree-style origin configuration
222/// * `{deployment_id}.imginfo` - Container image manifest and config as JSON
223///
224/// For staged deployments, also writes to `/run/composefs/staged-deployment`.
225#[context("Writing composefs state")]
226pub(crate) async fn write_composefs_state(
227    root_path: &Utf8PathBuf,
228    deployment_id: &Sha512HashValue,
229    target_imgref: &ImageReference,
230    staged: bool,
231    boot_type: BootType,
232    boot_digest: String,
233    container_details: &ImgConfigManifest,
234) -> Result<()> {
235    let state_path = root_path
236        .join(STATE_DIR_RELATIVE)
237        .join(deployment_id.to_hex());
238
239    create_dir_all(state_path.join("etc"))?;
240
241    let actual_var_path = root_path.join(SHARED_VAR_PATH);
242    create_dir_all(&actual_var_path)?;
243
244    symlink(
245        path_relative_to(state_path.as_std_path(), actual_var_path.as_std_path())
246            .context("Getting var symlink path")?,
247        state_path.join("var"),
248    )
249    .context("Failed to create symlink for /var")?;
250
251    initialize_state(&root_path, &deployment_id.to_hex(), &state_path, !staged)?;
252
253    let ImageReference {
254        image: image_name,
255        transport,
256        ..
257    } = &target_imgref;
258
259    let imgref = get_imgref(&transport, &image_name);
260
261    let mut config = tini::Ini::new().section("origin").item(
262        ORIGIN_CONTAINER,
263        // TODO (Johan-Liebert1): The image won't always be unverified
264        format!("ostree-unverified-image:{imgref}"),
265    );
266
267    config = config
268        .section(ORIGIN_KEY_BOOT)
269        .item(ORIGIN_KEY_BOOT_TYPE, boot_type);
270
271    config = config
272        .section(ORIGIN_KEY_BOOT)
273        .item(ORIGIN_KEY_BOOT_DIGEST, boot_digest);
274
275    let state_dir =
276        Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?;
277
278    // NOTE: This is only supposed to be temporary until we decide on where to store
279    // the container manifest/config
280    state_dir
281        .atomic_write(
282            format!("{}.imginfo", deployment_id.to_hex()),
283            serde_json::to_vec(&container_details)?,
284        )
285        .context("Failed to write to .imginfo file")?;
286
287    state_dir
288        .atomic_write(
289            format!("{}.origin", deployment_id.to_hex()),
290            config.to_string().as_bytes(),
291        )
292        .context("Failed to write to .origin file")?;
293
294    if staged {
295        std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR)
296            .with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
297
298        let staged_depl_dir =
299            Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
300                .with_context(|| format!("Opening {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
301
302        staged_depl_dir
303            .atomic_write(
304                COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
305                deployment_id.to_hex().as_bytes(),
306            )
307            .with_context(|| format!("Writing to {COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"))?;
308    }
309
310    Ok(())
311}
312
313pub(crate) fn composefs_usr_overlay() -> Result<()> {
314    let usr = Dir::open_ambient_dir("/usr", ambient_authority()).context("Opening /usr")?;
315    let is_usr_mounted = usr
316        .is_mountpoint(".")
317        .context("Failed to get mount details for /usr")?;
318
319    let is_usr_mounted =
320        is_usr_mounted.ok_or_else(|| anyhow::anyhow!("Failed to get mountinfo"))?;
321
322    if is_usr_mounted {
323        println!("A writeable overlayfs is already mounted on /usr");
324        return Ok(());
325    }
326
327    // Get the mode from the underlying /usr directory
328    let usr_metadata = usr.metadata(".").context("Getting /usr metadata")?;
329    let usr_mode = Mode::from_raw_mode(usr_metadata.permissions().mode());
330
331    overlay_transient(usr, Some(usr_mode))?;
332
333    println!("A writeable overlayfs is now mounted on /usr");
334    println!("All changes there will be discarded on reboot.");
335
336    Ok(())
337}