ostree_ext/container/
deploy.rs1use anyhow::Result;
4use fn_error_context::context;
5use ostree::glib;
6use std::collections::HashSet;
7
8use super::store::{LayeredImageState, gc_image_layers};
9use super::{ImageReference, OstreeImageReference};
10use crate::container::store::PrepareResult;
11use crate::keyfileext::KeyFileExt;
12use crate::sysroot::SysrootLock;
13
14pub const ORIGIN_CONTAINER: &str = "container-image-reference";
16
17pub const STATEROOT_DEFAULT: &str = "default";
20
21#[derive(Debug, Default)]
23#[non_exhaustive]
24pub struct DeployOpts<'a> {
25 pub kargs: Option<&'a [&'a str]>,
27 pub target_imgref: Option<&'a OstreeImageReference>,
36
37 pub proxy_cfg: Option<super::store::ImageProxyConfig>,
39
40 pub no_imgref: bool,
45
46 pub skip_completion: bool,
48
49 pub no_clean: bool,
51}
52
53#[context("Performing deployment")]
57pub async fn deploy(
58 sysroot: &ostree::Sysroot,
59 stateroot: &str,
60 imgref: &OstreeImageReference,
61 options: Option<DeployOpts<'_>>,
62) -> Result<Box<LayeredImageState>> {
63 const DEPLOY_JOURNAL_ID: &str = "9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3";
66
67 tracing::debug!(
68 message_id = DEPLOY_JOURNAL_ID,
69 bootc.image.reference = &imgref.imgref.name,
70 bootc.image.transport = &imgref.imgref.transport.to_string(),
71 bootc.stateroot = stateroot,
72 "Deploying container image to OSTree: {}",
73 imgref
74 );
75
76 let cancellable = ostree::gio::Cancellable::NONE;
77 let options = options.unwrap_or_default();
78 let repo = &sysroot.repo();
79 let merge_deployment = sysroot.merge_deployment(Some(stateroot));
80 let mut imp =
81 super::store::ImageImporter::new(repo, imgref, options.proxy_cfg.unwrap_or_default())
82 .await?;
83 imp.require_bootable();
84 if let Some(target) = options.target_imgref {
85 imp.set_target(target);
86 }
87 if options.no_imgref {
88 imp.set_no_imgref();
89 }
90 let state = match imp.prepare().await? {
91 PrepareResult::AlreadyPresent(r) => r,
92 PrepareResult::Ready(prep) => {
93 if let Some(warning) = prep.deprecated_warning() {
94 crate::cli::print_deprecated_warning(warning).await;
95 }
96
97 imp.import(prep).await?
98 }
99 };
100 let commit = state.merge_commit.as_str();
101 let origin = glib::KeyFile::new();
102 let target_imgref = options.target_imgref.unwrap_or(imgref);
103 origin.set_string("origin", ORIGIN_CONTAINER, &target_imgref.to_string());
104
105 let opts = ostree::SysrootDeployTreeOpts {
106 override_kernel_argv: options.kargs,
107 ..Default::default()
108 };
109
110 if sysroot.booted_deployment().is_some() {
111 sysroot.stage_tree_with_options(
112 Some(stateroot),
113 commit,
114 Some(&origin),
115 merge_deployment.as_ref(),
116 &opts,
117 cancellable,
118 )?;
119 } else {
120 let deployment = &sysroot.deploy_tree_with_options(
121 Some(stateroot),
122 commit,
123 Some(&origin),
124 merge_deployment.as_ref(),
125 Some(&opts),
126 cancellable,
127 )?;
128 let flags = if options.no_clean {
129 ostree::SysrootSimpleWriteDeploymentFlags::NO_CLEAN
130 } else {
131 ostree::SysrootSimpleWriteDeploymentFlags::NONE
132 };
133 sysroot.simple_write_deployment(
134 Some(stateroot),
135 deployment,
136 merge_deployment.as_ref(),
137 flags,
138 cancellable,
139 )?;
140
141 #[cfg(feature = "bootc")]
149 if !options.skip_completion {
150 use bootc_utils::CommandRunExt;
151 use cap_std_ext::cmdext::CapStdExtCommandExt;
152 use ocidir::cap_std::fs::Dir;
153
154 let sysroot_dir = &Dir::reopen_dir(&crate::sysroot::sysroot_fd(sysroot))?;
155
156 let st = std::process::Command::new(std::env::current_exe()?)
159 .args(["internals", "bootc-install-completion", ".", stateroot])
160 .cwd_dir(sysroot_dir.try_clone()?)
161 .lifecycle_bind()
162 .status()?;
163 if !st.success() {
164 anyhow::bail!("Failed to complete bootc install");
165 }
166 }
167
168 if !options.no_clean {
169 sysroot.cleanup(cancellable)?;
170 }
171 }
172
173 Ok(state)
174}
175
176fn deployment_origin_container(
178 deploy: &ostree::Deployment,
179) -> Result<Option<OstreeImageReference>> {
180 let origin = deploy
181 .origin()
182 .map(|o| o.optional_string("origin", ORIGIN_CONTAINER))
183 .transpose()?
184 .flatten();
185 let r = origin
186 .map(|v| OstreeImageReference::try_from(v.as_str()))
187 .transpose()?;
188 Ok(r)
189}
190
191pub fn remove_undeployed_images(sysroot: &SysrootLock) -> Result<Vec<ImageReference>> {
197 let repo = &sysroot.repo();
198 let deployment_origins: Result<HashSet<_>> = sysroot
199 .deployments()
200 .into_iter()
201 .filter_map(|deploy| {
202 deployment_origin_container(&deploy)
203 .map(|v| v.map(|v| v.imgref))
204 .transpose()
205 })
206 .collect();
207 let deployment_origins = deployment_origins?;
208 let all_images = super::store::list_images(&sysroot.repo())?
210 .into_iter()
211 .filter_map(|img| ImageReference::try_from(img.as_str()).ok());
212 let mut removed = Vec::new();
213 for image in all_images {
214 if !deployment_origins.contains(&image) {
215 super::store::remove_image(repo, &image)?;
216 removed.push(image);
217 }
218 }
219 Ok(removed)
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct Pruned {
225 pub n_images: u32,
227 pub n_layers: u32,
229 pub n_objects_pruned: u32,
231 pub objsize: u64,
233}
234
235impl Pruned {
236 pub fn is_empty(&self) -> bool {
238 self.n_images == 0 && self.n_layers == 0 && self.n_objects_pruned == 0
239 }
240}
241
242pub fn prune(sysroot: &SysrootLock) -> Result<Pruned> {
244 let repo = &sysroot.repo();
245 let n_images = remove_undeployed_images(sysroot)?.len().try_into().unwrap();
248 let n_layers = gc_image_layers(repo)?;
250 let (_, n_objects_pruned, objsize) = repo.prune(
252 ostree::RepoPruneFlags::REFS_ONLY,
253 0,
254 ostree::gio::Cancellable::NONE,
255 )?;
256 let n_objects_pruned = u32::try_from(n_objects_pruned).unwrap();
258 Ok(Pruned {
259 n_images,
260 n_layers,
261 n_objects_pruned,
262 objsize,
263 })
264}