-
Notifications
You must be signed in to change notification settings - Fork 222
Expand file tree
/
Copy pathrepo.rs
More file actions
355 lines (308 loc) · 14 KB
/
Copy pathrepo.rs
File metadata and controls
355 lines (308 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
use fn_error_context::context;
use std::sync::Arc;
use anyhow::{Context, Result};
use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
use composefs_boot::bootloader::{BootEntry as ComposefsBootEntry, get_boot_resources};
use composefs_ctl::composefs;
use composefs_ctl::composefs_boot;
use composefs_ctl::composefs_oci;
use composefs_oci::{
LocalFetchOpt, PullOptions, PullResult,
image::create_filesystem as create_composefs_filesystem, tag_image,
};
use ostree_ext::containers_image_proxy;
use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
use crate::composefs_consts::BOOTC_TAG_PREFIX;
use crate::install::{RootSetup, State};
use crate::lsm;
use crate::podstorage::CStorage;
/// Create a composefs OCI tag name for the given manifest digest.
///
/// Returns a tag like `localhost/bootc-sha256:abc...` which acts as a GC root
/// in the composefs repository, keeping the manifest, config, and all layer
/// splitstreams alive.
pub(crate) fn bootc_tag_for_manifest(manifest_digest: &str) -> String {
format!("{BOOTC_TAG_PREFIX}{manifest_digest}")
}
pub(crate) fn open_composefs_repo(rootfs_dir: &Dir) -> Result<crate::store::ComposefsRepository> {
crate::store::ComposefsRepository::open_path(rootfs_dir, "composefs")
.context("Failed to open composefs repository")
}
pub(crate) async fn initialize_composefs_repository(
state: &State,
root_setup: &RootSetup,
allow_missing_fsverity: bool,
use_unified: bool,
local_fetch: LocalFetchOpt,
) -> Result<PullResult<Sha512HashValue>> {
const COMPOSEFS_REPO_INIT_JOURNAL_ID: &str = "5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9";
let rootfs_dir = &root_setup.physical_root;
let image_name = &state.source.imageref.name;
let transport = &state.source.imageref.transport;
tracing::info!(
message_id = COMPOSEFS_REPO_INIT_JOURNAL_ID,
bootc.operation = "repository_init",
bootc.source_image = %image_name,
bootc.transport = %transport,
bootc.allow_missing_fsverity = allow_missing_fsverity,
bootc.unified_storage = use_unified,
"Initializing composefs repository for image {}:{}",
transport,
image_name
);
crate::store::ensure_composefs_dir(rootfs_dir)?;
let (mut repo, _created) = crate::store::ComposefsRepository::init_path(
rootfs_dir,
"composefs",
composefs::fsverity::Algorithm::SHA512,
!allow_missing_fsverity,
)
.context("Failed to initialize composefs repository")?;
if allow_missing_fsverity {
repo.set_insecure();
}
let imgref: containers_image_proxy::ImageReference = state
.source
.imageref
.to_string()
.as_str()
.try_into()
.context("Parsing source image reference")?;
// Ensure the compatibility symlink ostree/bootc -> ../composefs/bootc
// exists. This is needed for LBI and (when unified storage is enabled)
// for containers-storage under composefs/bootc/storage. The existing
// /usr/lib/bootc/storage symlink and all runtime code using
// ostree/bootc/storage depend on this link.
crate::store::ensure_composefs_bootc_link(rootfs_dir)?;
let repo = Arc::new(repo);
let pull_result = if use_unified {
// Unified path: first into containers-storage on the target
// rootfs, then cstor zero-copy into composefs. This ensures the image
// is available for `podman run` from first boot.
let sepolicy = state.load_policy()?;
let run = Dir::open_ambient_dir("/run", ambient_authority())?;
let imgstore = CStorage::create(rootfs_dir, &run, sepolicy.as_ref())?;
let storage_path = root_setup.physical_root_path.join(CStorage::subpath());
let r = pull_composefs_unified(&imgstore, storage_path.as_str(), &repo, &imgref, local_fetch).await?;
// SELinux-label the containers-storage now that all pulls are done.
imgstore
.ensure_labeled()
.context("SELinux labeling of containers-storage")?;
r
} else {
// Direct path: pull directly into composefs via skopeo, without
// containers-storage as intermediary.
pull_composefs_direct(&repo, &imgref).await?
};
// Tag the manifest as a bootc-owned GC root.
let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
tag_image(&*repo, &pull_result.manifest_digest, &tag)
.context("Tagging pulled image as bootc GC root")?;
tracing::info!(
message_id = COMPOSEFS_REPO_INIT_JOURNAL_ID,
bootc.operation = "repository_init",
bootc.manifest_digest = %pull_result.manifest_digest,
bootc.manifest_verity = pull_result.manifest_verity.to_hex(),
bootc.config_digest = %pull_result.config_digest,
bootc.config_verity = pull_result.config_verity.to_hex(),
bootc.tag = tag,
"Pulled image into composefs repository",
);
Ok(pull_result)
}
/// Result of pulling a composefs repository, including the OCI manifest digest
/// needed to reconstruct image metadata from the local composefs repo.
pub(crate) struct PullRepoResult {
pub(crate) repo: crate::store::ComposefsRepository,
pub(crate) entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
pub(crate) id: Sha512HashValue,
/// The OCI manifest content digest (e.g. "sha256:abc...")
pub(crate) manifest_digest: String,
}
/// Pull an image directly into the composefs repository via skopeo.
///
/// This is the default path: the image is fetched directly from the source
/// transport (registry, oci directory, etc.) into the composefs repo without
/// going through containers-storage first.
async fn pull_composefs_direct(
repo: &Arc<crate::store::ComposefsRepository>,
imgref: &containers_image_proxy::ImageReference,
) -> Result<PullResult<Sha512HashValue>> {
let imgref_str = imgref.to_string();
tracing::info!("Direct pull: fetching {imgref_str} into composefs repository");
let pull_result = composefs_oci::pull(repo, &imgref_str, None, PullOptions::default())
.await
.context("Pulling image into composefs repository")?;
Ok(pull_result)
}
/// Pull an image via unified storage: first into bootc-owned containers-storage,
/// then from there into the composefs repository via cstor (zero-copy
/// reflink/hardlink).
///
/// The caller provides:
/// - `imgstore`: the bootc-owned `CStorage` instance (may be on an arbitrary
/// mount point during install, or under `/sysroot` during upgrade)
/// - `storage_path`: the absolute filesystem path to that containers-storage
/// directory, so cstor and skopeo can find it (e.g.
/// `/mnt/sysroot/ostree/bootc/storage` during install, or
/// `/sysroot/ostree/bootc/storage` during upgrade)
///
/// This ensures the image is available in containers-storage for `podman run`
/// while also populating the composefs repo for booting.
async fn pull_composefs_unified(
imgstore: &CStorage,
storage_path: &str,
repo: &Arc<crate::store::ComposefsRepository>,
imgref: &containers_image_proxy::ImageReference,
local_fetch: LocalFetchOpt,
) -> Result<PullResult<Sha512HashValue>> {
let image = &imgref.name;
// Stage 1: get the image into bootc-owned containers-storage.
if imgref.transport == containers_image_proxy::Transport::ContainerStorage {
// The image is in a containers-storage instance — either the default
// /var/lib/containers/storage or an additional image store advertised
// via STORAGE_OPTS (e.g. the bcvk virtiofs mount).
tracing::info!("Unified pull: copying {image} from host containers-storage");
imgstore
.pull_from_containers_storage(image)
.await
.context("Copying image from host containers-storage into bootc storage")?;
} else {
// For registry (docker://), oci:, docker-daemon:, etc. — pull
// via the native podman API with streaming progress display.
let pull_ref = imgref.to_string();
tracing::info!("Unified pull: fetching {pull_ref} into containers-storage");
imgstore
.pull_with_progress(&pull_ref)
.await
.context("Pulling image into bootc containers-storage")?;
}
// Stage 2: import full OCI structure (layers + config + manifest) from
// containers-storage into composefs via cstor (zero-copy reflink/hardlink).
let cstor_imgref_str = format!("containers-storage:{image}");
tracing::info!("Unified pull: importing from {cstor_imgref_str} (zero-copy)");
let storage = std::path::Path::new(storage_path);
let pull_opts = PullOptions {
// The image is already in bootc-owned containers-storage at this point
// (placed there by Stage 1 of the unified pull). CopyMode controls
// whether a fallback to byte copies is acceptable:
// ZeroCopy → fail if reflinks unavailable (storage.unified = "enabled")
// IfPossible → byte-copy fallback ok (storage.unified = "enabled-with-copy")
local_fetch,
storage_root: Some(storage),
..Default::default()
};
let pull_result = composefs_oci::pull(repo, &cstor_imgref_str, None, pull_opts)
.await
.context("Importing from containers-storage into composefs")?;
Ok(pull_result)
}
/// Pulls an image into a composefs repository at /sysroot.
///
/// When `use_unified` is true, the image is first pulled into bootc-owned
/// containers-storage (so it's available for `podman run`), then imported
/// from there into the composefs repo via zero-copy reflinks.
///
/// When `use_unified` is false (the default), the image is pulled directly
/// into the composefs repo via skopeo.
///
/// Checks for boot entries in the image and returns them.
#[context("Pulling composefs repository")]
pub(crate) async fn pull_composefs_repo(
spec_imgref: &crate::spec::ImageReference,
allow_missing_fsverity: bool,
use_unified: bool,
local_fetch: LocalFetchOpt,
) -> Result<PullRepoResult> {
const COMPOSEFS_PULL_JOURNAL_ID: &str = "4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8";
let imgref = spec_imgref.to_image_proxy_ref()?;
tracing::info!(
message_id = COMPOSEFS_PULL_JOURNAL_ID,
bootc.operation = "pull",
bootc.source_image = &spec_imgref.image,
bootc.transport = %imgref.transport,
bootc.allow_missing_fsverity = allow_missing_fsverity,
bootc.unified_storage = use_unified,
"Pulling composefs image {imgref}",
);
let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?;
let mut repo = open_composefs_repo(&rootfs_dir).context("Opening composefs repo")?;
if allow_missing_fsverity {
repo.set_insecure();
}
let repo = Arc::new(repo);
// Upgrade any old-format OCI images before pulling. Old bootc
// (composefs-rs ≤ 2203e8f) did not add IMAGE_REF_KEY to config
// splitstreams, so the new GC's tag-based stream walk cannot reach
// their layer objects. upgrade_repo() rewrites those config
// splitstreams in place before we add a new deployment, ensuring all
// existing deployments are GC-safe. It is idempotent and fast when
// images are already in the current format.
let upgrade_result =
composefs_oci::upgrade_repo(&repo).context("Upgrading old-format OCI images")?;
if upgrade_result.upgraded > 0 {
tracing::info!(
"Upgraded {} old-format OCI image(s) to current format",
upgrade_result.upgraded
);
}
let pull_result = if use_unified {
// Create bootc-owned containers-storage on the rootfs.
// Load SELinux policy from the running system so newly pulled layers
// get the correct container_var_lib_t labels.
let root = Dir::open_ambient_dir("/", ambient_authority())?;
let sepolicy = lsm::new_sepolicy_at(&root)?;
let run = Dir::open_ambient_dir("/run", ambient_authority())?;
let imgstore = CStorage::create(&rootfs_dir, &run, sepolicy.as_ref())?;
let storage_path = format!("/sysroot/{}", CStorage::subpath());
pull_composefs_unified(&imgstore, &storage_path, &repo, &imgref, local_fetch).await?
} else {
pull_composefs_direct(&repo, &imgref).await?
};
// Tag the manifest as a bootc-owned GC root.
let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
tag_image(&*repo, &pull_result.manifest_digest, &tag)
.context("Tagging pulled image as bootc GC root")?;
tracing::info!(
message_id = COMPOSEFS_PULL_JOURNAL_ID,
bootc.operation = "pull",
bootc.manifest_digest = %pull_result.manifest_digest,
bootc.manifest_verity = pull_result.manifest_verity.to_hex(),
bootc.config_digest = %pull_result.config_digest,
bootc.config_verity = pull_result.config_verity.to_hex(),
bootc.tag = tag,
"Pulled image into composefs repository",
);
// Generate the bootable EROFS image (idempotent).
let id = composefs_oci::generate_boot_image(&repo, &pull_result.manifest_digest)
.context("Generating bootable EROFS image")?;
// Get boot entries from the OCI filesystem (untransformed).
let fs = create_composefs_filesystem(&*repo, &pull_result.config_digest, None)
.context("Creating composefs filesystem for boot entry discovery")?;
let entries =
get_boot_resources(&fs, &*repo).context("Extracting boot entries from OCI image")?;
// Unwrap the Arc to get the owned repo back.
let mut repo = Arc::try_unwrap(repo).map_err(|_| {
anyhow::anyhow!("BUG: Arc<Repository> still has other references after pull completed")
})?;
if allow_missing_fsverity {
repo.set_insecure();
}
Ok(PullRepoResult {
repo,
entries,
id,
manifest_digest: pull_result.manifest_digest.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bootc_tag_for_manifest() {
let digest = "sha256:abc123def456";
let tag = bootc_tag_for_manifest(digest);
assert_eq!(tag, "localhost/bootc-sha256:abc123def456");
assert!(tag.starts_with(BOOTC_TAG_PREFIX));
}
}