Skip to content
Draft
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ jobs:
tar -xzvf ../../install.tar
sudo podman build -v $(pwd)/usr/bin:/ci -t localhost/builder -f Containerfile.builder
sudo ./test.sh
sudo ./test-oci.sh
build-c9s:
name: "Build (c9s)"
runs-on: ubuntu-latest
Expand Down
140 changes: 104 additions & 36 deletions rust/src/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ pub(crate) struct BuildChunkedOCIOpts {
#[clap(long, required_unless_present = "rootfs")]
from: Option<String>,

/// Change the transport for the `--from` argument; typically this is either
/// `oci` or `registry`. A key use case for this is when you need
/// to avoid requiring privileges to perform mounts when
/// invoking this command. Operating on `registry` or `oci`
/// can be done inside a default podman/docker container image.
#[clap(long, value_parser = crate::parse_transport, default_value = "containers-storage")]
from_transport: ostree_container::Transport,

/// If set, configure the output OCI image to be a bootc container.
/// At the current time this option is required.
#[clap(long, required = true)]
Expand Down Expand Up @@ -250,12 +258,27 @@ pub(crate) struct CommitToContainerRootfsOpts {
dest: Utf8PathBuf,
}

enum FileSource {
Rootfs(Utf8PathBuf),
Podman(Mount),
OciOrRegistry,
}

impl FileSource {
fn open_dir(&self) -> Result<Option<Dir>> {
let rootfs = match &self {
FileSource::Rootfs(p) => p.as_path(),
FileSource::Podman(mnt) => mnt.path(),
FileSource::OciOrRegistry => return Ok(None),
};
Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())
.with_context(|| format!("Opening {}", rootfs))
.map(Some)
}
}

impl BuildChunkedOCIOpts {
pub(crate) fn run(self) -> Result<()> {
enum FileSource {
Rootfs(Utf8PathBuf),
Podman(Mount),
}
let rootfs_source = if let Some(rootfs) = self.rootfs {
FileSource::Rootfs(rootfs)
} else {
Expand All @@ -267,28 +290,80 @@ impl BuildChunkedOCIOpts {
// Note that this would all be a lot saner with a composefs-native container storage
// as we could cleanly operate on that, asking c/storage to synthesize one for us.
// crate::containers_storage::reexec_if_needed()?;
FileSource::Podman(Mount::new_for_image(image)?)
};
let rootfs = match &rootfs_source {
FileSource::Rootfs(p) => p.as_path(),
FileSource::Podman(mnt) => mnt.path(),
if matches!(
self.from_transport,
ostree_container::Transport::ContainerStorage
) {
FileSource::Podman(Mount::new_for_image(image)?)
} else {
FileSource::OciOrRegistry
}
};
let rootfs = Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())
.with_context(|| format!("Opening {}", rootfs))?;
let rootfs = rootfs_source.open_dir()?;

// These must be set to exactly this; the CLI parser requires it.
assert!(self.bootc);
assert_eq!(self.format_version, 1);

// Allocate a working temporary directory
let td = tempfile::tempdir_in("/var/tmp")?;

// Note: In a format v2, we'd likely not use ostree.
let repo_path: Utf8PathBuf = td.path().join("repo").try_into()?;
let repo = ostree::Repo::create_at(
libc::AT_FDCWD,
repo_path.as_str(),
ostree::RepoMode::BareUser,
None,
gio::Cancellable::NONE,
)?;
// This repo is temporary
repo.set_disable_fsync(true);

// In the case that we're dealing with a remote image, we directly import it into ostree
let mut imported_commit = None;
// If we're deriving from an existing image, be sure to preserve its metadata (labels, creation time, etc.)
// by default.
let image_config: oci_spec::image::ImageConfiguration =
if let Some(image) = self.from.as_deref() {
let img_transport = format!("containers-storage:{image}");
Command::new("skopeo")
.args(["inspect", "--config", img_transport.as_str()])
.run_and_parse_json()
.context("Invoking skopeo to inspect config")?
} else {
if matches!(rootfs_source, FileSource::OciOrRegistry) {
let imgref = ostree_container::ImageReference {
transport: self.from_transport,
name: image.into(),
};
println!("Fetching {imgref}");
let imgref = ostree_container::OstreeImageReference {
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
imgref,
};
let handle = tokio::runtime::Handle::current();
handle.block_on(async {
use ostree_container::store::PrepareResult;
let mut imp = ostree_container::store::ImageImporter::new(
&repo,
&imgref,
Default::default(),
)
.await?;
let (commit, config) = match imp.prepare().await? {
PrepareResult::AlreadyPresent(r) => (r.merge_commit, r.configuration),
PrepareResult::Ready(r) => {
let r = imp.import(r).await?;
(r.merge_commit, r.configuration)
}
};
tracing::debug!("Wrote {commit}");
imported_commit = Some(commit);
anyhow::Ok(config)
})?
} else {
let img_transport = format!("containers-storage:{image}");
Command::new("skopeo")
.args(["inspect", "--config", img_transport.as_str()])
.run_and_parse_json()
.context("Invoking skopeo to inspect config")?
}
} else if let Some(rootfs) = rootfs.as_ref() {
// If we're not deriving, then we take the timestamp of the root
// directory as a creation timestamp.
let toplevel_ts = rootfs.dir_metadata()?.modified()?.into_std();
Expand All @@ -297,6 +372,8 @@ impl BuildChunkedOCIOpts {
let mut config = ImageConfiguration::default();
config.set_created(Some(toplevel_ts));
config
} else {
unreachable!()
};
let arch = image_config.architecture();
let creation_timestamp = image_config
Expand All @@ -305,26 +382,16 @@ impl BuildChunkedOCIOpts {
.map(chrono::DateTime::parse_from_rfc3339)
.transpose()?;

// Allocate a working temporary directory
let td = tempfile::tempdir_in("/var/tmp")?;

// Note: In a format v2, we'd likely not use ostree.
let repo_path: Utf8PathBuf = td.path().join("repo").try_into()?;
let repo = ostree::Repo::create_at(
libc::AT_FDCWD,
repo_path.as_str(),
ostree::RepoMode::BareUser,
None,
gio::Cancellable::NONE,
)?;

println!("Generating commit...");
// It's only the tests that override
let modifier =
ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::empty(), None);
// Process the filesystem, generating an ostree commit
let commitid =
generate_commit_from_rootfs(&repo, &rootfs, modifier, creation_timestamp.as_ref())?;
let commitid = if let Some(rootfs) = rootfs.as_ref() {
println!("Generating commit...");
// It's only the tests that override
let modifier =
ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::empty(), None);
generate_commit_from_rootfs(&repo, &rootfs, modifier, creation_timestamp.as_ref())?
} else {
imported_commit.expect("Should have imported a commit")
};

let label_arg = self
.bootc
Expand Down Expand Up @@ -368,6 +435,7 @@ impl BuildChunkedOCIOpts {
FileSource::Podman(mnt) => {
mnt.unmount().context("Final mount cleanup")?;
}
FileSource::OciOrRegistry => {}
}

Ok(())
Expand Down
5 changes: 5 additions & 0 deletions rust/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ use crate::fsutil::{self, FileHelpers, ResolvedOstreePaths};
use crate::progress::progress_task;
use crate::CxxResult;

/// Parse a transport from a CLI argument.
pub(crate) fn parse_transport(s: &str) -> Result<ostree_ext::container::Transport> {
ostree_ext::container::Transport::try_from(s)
}

#[derive(Debug, Parser)]
struct ContainerEncapsulateOpts {
#[clap(long)]
Expand Down
Loading