Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ In case you have an alternate root mounted at `/mnt/squash` from an external sou
sudo REPART_COPY_SOURCE=/mnt/rootfs readymade
```

For playbook-driven installs using the filesystem `Copy` provisioner, `copy_source` may also be
an OCI image reference instead of a path. If the value starts with a Podman transport prefix such
as `containers-storage:`, `docker://`, `oci:`, or `oci-archive:`, Readymade will create a
temporary container with Podman, mount it, copy the mounted root filesystem into the target, then
clean the temporary container up afterwards.

```json
{
"filesystem_provisioner": {
"module": "Copy",
"copy_source": "containers-storage:registry.example.org/example/os:latest"
}
}
```

> [!NOTE]
> If Readymade is built as a debug build, it will dump the installation state and the systemd-repart output to `/tmp/` for debugging purposes.

Expand Down
135 changes: 119 additions & 16 deletions crates/libreadymade/backend/provisioners/filesystem/copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,67 @@ use crate::{

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Copy {
pub copy_source: PathBuf,
/// Either a local path, a mountable image file, or an OCI image reference with
/// a Podman-recognized transport prefix such as `containers-storage:`.
pub copy_source: String,
}

const OCI_COPY_SOURCE_PREFIXES: [&str; 4] =
["containers-storage:", "docker://", "oci:", "oci-archive:"];

fn is_oci_copy_source(copy_source: &str) -> bool {
OCI_COPY_SOURCE_PREFIXES
.iter()
.any(|prefix| copy_source.starts_with(prefix))
}

#[tracing::instrument]
fn podman_stdout(args: &[&str], action: &str) -> Result<String> {
tracing::trace!(?args, "running `podman {action}`");
let mut cmd = Command::new("podman");
cmd.args(args);

// hack: do this to allow tracing to see the full command string
tracing::trace!(?cmd, "executing command");
let output = cmd
.output()
.wrap_err_with(|| format!("failed to run `podman {action}`"))?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
bail!("`podman {action}` failed: {stderr}");
}

let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if stdout.is_empty() {
bail!("`podman {action}` returned empty stdout");
}

Ok(stdout)
}

fn cleanup_podman_container(container_id: &str) {
if let Err(err) = Command::new("podman")
.args(["umount", container_id])
.status()
{
tracing::warn!(
?err,
?container_id,
"podman umount cleanup command failed to execute"
);
}

if let Err(err) = Command::new("podman")
.args(["rm", "-f", container_id])
.status()
{
tracing::warn!(
?err,
?container_id,
"podman rm cleanup command failed to execute"
);
}
}

impl FileSystemProvisionerModule for Copy {
Expand All @@ -24,29 +84,72 @@ impl FileSystemProvisionerModule for Copy {
}
};

let copy_source = &self.copy_source;
let copy_source = self.copy_source.trim();
if copy_source.is_empty() {
bail!("copy_source cannot be empty");
}

tracing::trace!(?copy_source, ?destroot);
if copy_source.is_file() {
// XXX: we should be using consistent paths, maybe a const? -ci
const MOUNT_PATH: &str = "/mnt/rdmsqsh";
tracing::warn!("Copy source is a file, treating as an image to mount");
if is_oci_copy_source(copy_source) {
tracing::debug!("Copy source is an OCI reference");
crate::stage!(extracting "Extracting files" {
tracing::trace!(?MOUNT_PATH, "Mounting disk image");
let return_code = Command::new("mount").arg(copy_source).arg(MOUNT_PATH).status()?.code();
if return_code.is_none_or(|return_code| return_code != 0) {
bail!("mount command returns rc={return_code:?}");
}
tracing::info!(copy_source, "Copy source is an OCI image, mounting with podman");
let container_id = podman_stdout(&["create", copy_source], "create")?;
scopeguard::defer! {
_ = Command::new("umount").arg(MOUNT_PATH).status();
cleanup_podman_container(&container_id);
}
copy_dir(MOUNT_PATH, destroot)?;

let mount_path = podman_stdout(&["mount", &container_id], "mount")?;
copy_dir(&mount_path, destroot)?;
});
} else {
crate::stage!(copying "Copying files" {
copy_dir(&copy_source, destroot)?;
});
let copy_source = PathBuf::from(copy_source);
if copy_source.is_file() {
// XXX: we should be using consistent paths, maybe a const? -ci
const MOUNT_PATH: &str = "/mnt/rdmsqsh";
tracing::warn!("Copy source is a file, treating as an image to mount");
crate::stage!(extracting "Extracting files" {
tracing::trace!(?MOUNT_PATH, "Mounting disk image");
std::fs::create_dir_all(MOUNT_PATH)?;
let return_code = Command::new("mount")
.arg(&copy_source)
.arg(MOUNT_PATH)
.status()?
.code();
if return_code.is_none_or(|return_code| return_code != 0) {
bail!("mount command returns rc={return_code:?}");
}
scopeguard::defer! {
_ = Command::new("umount").arg(MOUNT_PATH).status();
}
copy_dir(MOUNT_PATH, destroot)?;
});
} else {
crate::stage!(copying "Copying files" {
copy_dir(&copy_source, destroot)?;
});
}
}

Ok(())
}
}

#[cfg(test)]
mod tests {
use super::is_oci_copy_source;

#[test]
fn detects_oci_copy_sources() {
assert!(is_oci_copy_source(
"containers-storage:registry.example.org/example/os:latest"
));
assert!(is_oci_copy_source("docker://quay.io/fyralabs/os:latest"));
}

#[test]
fn ignores_plain_paths() {
assert!(!is_oci_copy_source("/mnt/install-root"));
assert!(!is_oci_copy_source("./install.img"));
}
}
2 changes: 1 addition & 1 deletion crates/libreadymade/backend/provisioners/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ pub mod disk;
pub mod filesystem;

pub use disk::DiskProvisioner;
pub use filesystem::FileSystemProvisioner;
pub use filesystem::FileSystemProvisioner;
2 changes: 1 addition & 1 deletion crates/libreadymade/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pub use color_eyre::{Result, Section};
pub use itertools::Itertools;
pub use serde::{Deserialize, Serialize};

pub use crate::backend::mounts::{Mount, Mounts, EncryptionOption, CryptData};
pub use crate::backend::mounts::{CryptData, EncryptionOption, Mount, Mounts};
pub use std::path::Component;
pub use std::path::Path;
pub use std::path::PathBuf;
Expand Down
2 changes: 1 addition & 1 deletion crates/readymade-playbook/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
use color_eyre::Result;
use libreadymade::playbook::Playbook;
use std::fs;
use tracing_subscriber::{EnvFilter, fmt, prelude::*};

fn main() -> Result<()> {
color_eyre::install()?;
Expand Down
Loading