ostree_ext/container/
deploy.rs

1//! Perform initial setup for a container image based system root
2
3use std::collections::HashSet;
4
5use anyhow::Result;
6use fn_error_context::context;
7use ostree::glib;
8
9use super::store::{gc_image_layers, LayeredImageState};
10use super::{ImageReference, OstreeImageReference};
11use crate::container::store::PrepareResult;
12use crate::keyfileext::KeyFileExt;
13use crate::sysroot::SysrootLock;
14
15/// The key in the OSTree origin which holds a serialized [`super::OstreeImageReference`].
16pub const ORIGIN_CONTAINER: &str = "container-image-reference";
17
18/// The name of the default stateroot.
19// xref https://github.com/ostreedev/ostree/issues/2794
20pub const STATEROOT_DEFAULT: &str = "default";
21
22/// Options configuring deployment.
23#[derive(Debug, Default)]
24#[non_exhaustive]
25pub struct DeployOpts<'a> {
26    /// Kernel arguments to use.
27    pub kargs: Option<&'a [&'a str]>,
28    /// Target image reference, as distinct from the source.
29    ///
30    /// In many cases, one may want a workflow where a system is provisioned from
31    /// an image with a specific digest (e.g. `quay.io/example/os@sha256:...) for
32    /// reproducibilty.  However, one would want `ostree admin upgrade` to fetch
33    /// `quay.io/example/os:latest`.
34    ///
35    /// To implement this, use this option for the latter `:latest` tag.
36    pub target_imgref: Option<&'a OstreeImageReference>,
37
38    /// Configuration for fetching containers.
39    pub proxy_cfg: Option<super::store::ImageProxyConfig>,
40
41    /// If true, then no image reference will be written; but there will be refs
42    /// for the fetched layers.  This ensures that if the machine is later updated
43    /// to a different container image, the fetch process will reuse shared layers, but
44    /// it will not be necessary to remove the previous image.
45    pub no_imgref: bool,
46
47    /// Do not cleanup deployments
48    pub no_clean: bool,
49}
50
51/// Write a container image to an OSTree deployment.
52///
53/// This API is currently intended for only an initial deployment.
54#[context("Performing deployment")]
55pub async fn deploy(
56    sysroot: &ostree::Sysroot,
57    stateroot: &str,
58    imgref: &OstreeImageReference,
59    options: Option<DeployOpts<'_>>,
60) -> Result<Box<LayeredImageState>> {
61    let cancellable = ostree::gio::Cancellable::NONE;
62    let options = options.unwrap_or_default();
63    let repo = &sysroot.repo();
64    let merge_deployment = sysroot.merge_deployment(Some(stateroot));
65    let mut imp =
66        super::store::ImageImporter::new(repo, imgref, options.proxy_cfg.unwrap_or_default())
67            .await?;
68    imp.require_bootable();
69    if let Some(target) = options.target_imgref {
70        imp.set_target(target);
71    }
72    if options.no_imgref {
73        imp.set_no_imgref();
74    }
75    let state = match imp.prepare().await? {
76        PrepareResult::AlreadyPresent(r) => r,
77        PrepareResult::Ready(prep) => {
78            if let Some(warning) = prep.deprecated_warning() {
79                crate::cli::print_deprecated_warning(warning).await;
80            }
81
82            imp.import(prep).await?
83        }
84    };
85    let commit = state.merge_commit.as_str();
86    let origin = glib::KeyFile::new();
87    let target_imgref = options.target_imgref.unwrap_or(imgref);
88    origin.set_string("origin", ORIGIN_CONTAINER, &target_imgref.to_string());
89
90    let opts = ostree::SysrootDeployTreeOpts {
91        override_kernel_argv: options.kargs,
92        ..Default::default()
93    };
94
95    if sysroot.booted_deployment().is_some() {
96        sysroot.stage_tree_with_options(
97            Some(stateroot),
98            commit,
99            Some(&origin),
100            merge_deployment.as_ref(),
101            &opts,
102            cancellable,
103        )?;
104    } else {
105        let deployment = &sysroot.deploy_tree_with_options(
106            Some(stateroot),
107            commit,
108            Some(&origin),
109            merge_deployment.as_ref(),
110            Some(&opts),
111            cancellable,
112        )?;
113        let flags = if options.no_clean {
114            ostree::SysrootSimpleWriteDeploymentFlags::NO_CLEAN
115        } else {
116            ostree::SysrootSimpleWriteDeploymentFlags::NONE
117        };
118        sysroot.simple_write_deployment(
119            Some(stateroot),
120            deployment,
121            merge_deployment.as_ref(),
122            flags,
123            cancellable,
124        )?;
125        if !options.no_clean {
126            sysroot.cleanup(cancellable)?;
127        }
128    }
129
130    Ok(state)
131}
132
133/// Query the container image reference for a deployment
134fn deployment_origin_container(
135    deploy: &ostree::Deployment,
136) -> Result<Option<OstreeImageReference>> {
137    let origin = deploy
138        .origin()
139        .map(|o| o.optional_string("origin", ORIGIN_CONTAINER))
140        .transpose()?
141        .flatten();
142    let r = origin
143        .map(|v| OstreeImageReference::try_from(v.as_str()))
144        .transpose()?;
145    Ok(r)
146}
147
148/// Remove all container images which are not the target of a deployment.
149/// This acts equivalently to [`super::store::remove_images()`] - the underlying layers
150/// are not pruned.
151///
152/// The set of removed images is returned.
153pub fn remove_undeployed_images(sysroot: &SysrootLock) -> Result<Vec<ImageReference>> {
154    let repo = &sysroot.repo();
155    let deployment_origins: Result<HashSet<_>> = sysroot
156        .deployments()
157        .into_iter()
158        .filter_map(|deploy| {
159            deployment_origin_container(&deploy)
160                .map(|v| v.map(|v| v.imgref))
161                .transpose()
162        })
163        .collect();
164    let deployment_origins = deployment_origins?;
165    // TODO add an API that returns ImageReference instead
166    let all_images = super::store::list_images(&sysroot.repo())?
167        .into_iter()
168        .filter_map(|img| ImageReference::try_from(img.as_str()).ok());
169    let mut removed = Vec::new();
170    for image in all_images {
171        if !deployment_origins.contains(&image) {
172            super::store::remove_image(repo, &image)?;
173            removed.push(image);
174        }
175    }
176    Ok(removed)
177}
178
179/// The result of a prune operation
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct Pruned {
182    /// The number of images that were pruned
183    pub n_images: u32,
184    /// The number of image layers that were pruned
185    pub n_layers: u32,
186    /// The number of OSTree objects that were pruned
187    pub n_objects_pruned: u32,
188    /// The total size of pruned objects
189    pub objsize: u64,
190}
191
192impl Pruned {
193    /// Whether this prune was a no-op (i.e. no images, layers or objects were pruned).
194    pub fn is_empty(&self) -> bool {
195        self.n_images == 0 && self.n_layers == 0 && self.n_objects_pruned == 0
196    }
197}
198
199/// This combines the functionality of [`remove_undeployed_images()`] with [`super::store::gc_image_layers()`].
200pub fn prune(sysroot: &SysrootLock) -> Result<Pruned> {
201    let repo = &sysroot.repo();
202    // Prune container images which are not deployed.
203    // SAFETY: There should never be more than u32 images
204    let n_images = remove_undeployed_images(sysroot)?.len().try_into().unwrap();
205    // Prune unreferenced layer branches.
206    let n_layers = gc_image_layers(repo)?;
207    // Prune the objects in the repo; the above just removed refs (branches).
208    let (_, n_objects_pruned, objsize) = repo.prune(
209        ostree::RepoPruneFlags::REFS_ONLY,
210        0,
211        ostree::gio::Cancellable::NONE,
212    )?;
213    // SAFETY: The number of pruned objects should never be negative
214    let n_objects_pruned = u32::try_from(n_objects_pruned).unwrap();
215    Ok(Pruned {
216        n_images,
217        n_layers,
218        n_objects_pruned,
219        objsize,
220    })
221}