Skip to content

Rework API to use a Kubernetes CRD #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 6, 2023
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
4 changes: 4 additions & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@ hex = "^0.4"
fn-error-context = "0.2.0"
gvariant = "0.4.0"
indicatif = "0.17.0"
k8s-openapi = { version = "0.18.0", features = ["v1_25"] }
kube = { version = "0.83.0", features = ["runtime", "derive"] }
libc = "^0.2"
liboverdrop = "0.1.0"
once_cell = "1.9"
openssl = "^0.10"
nix = ">= 0.24, < 0.26"
regex = "1.7.1"
rustix = { "version" = "0.37", features = ["thread", "process"] }
schemars = "0.8.6"
serde = { features = ["derive"], version = "1.0.125" }
serde_json = "1.0.64"
serde_yaml = "0.9.17"
serde_with = ">= 1.9.4, < 2"
tokio = { features = ["io-std", "time", "process", "rt", "net"], version = ">= 1.13.0" }
tokio-util = { features = ["io-util"], version = "0.7" }
Expand Down
134 changes: 81 additions & 53 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ use std::ffi::OsString;
use std::os::unix::process::CommandExt;
use std::process::Command;

use crate::spec::HostSpec;
use crate::spec::ImageReference;

/// Perform an upgrade operation
#[derive(Debug, Parser)]
pub(crate) struct UpgradeOpts {
Expand Down Expand Up @@ -174,9 +177,10 @@ pub(crate) async fn get_locked_sysroot() -> Result<ostree_ext::sysroot::SysrootL
#[context("Pulling")]
async fn pull(
repo: &ostree::Repo,
imgref: &OstreeImageReference,
imgref: &ImageReference,
quiet: bool,
) -> Result<Box<LayeredImageState>> {
let imgref = &OstreeImageReference::from(imgref.clone());
let config = Default::default();
let mut imp = ostree_container::store::ImageImporter::new(repo, imgref, config).await?;
let prep = match imp.prepare().await? {
Expand Down Expand Up @@ -215,22 +219,35 @@ async fn pull(
async fn stage(
sysroot: &SysrootLock,
stateroot: &str,
imgref: &ostree_container::OstreeImageReference,
image: Box<LayeredImageState>,
origin: &glib::KeyFile,
spec: &HostSpec,
) -> Result<()> {
let cancellable = gio::Cancellable::NONE;
let stateroot = Some(stateroot);
let merge_deployment = sysroot.merge_deployment(stateroot);
let origin = glib::KeyFile::new();
let ostree_imgref = spec
.image
.as_ref()
.map(|imgref| OstreeImageReference::from(imgref.clone()));
if let Some(imgref) = ostree_imgref.as_ref() {
origin.set_string(
"origin",
ostree_container::deploy::ORIGIN_CONTAINER,
imgref.to_string().as_str(),
);
}
let _new_deployment = sysroot.stage_tree_with_options(
stateroot,
image.merge_commit.as_str(),
Some(origin),
Some(&origin),
merge_deployment.as_ref(),
&Default::default(),
cancellable,
)?;
println!("Queued for next boot: {imgref}");
if let Some(imgref) = ostree_imgref.as_ref() {
println!("Queued for next boot: {imgref}");
}
Ok(())
}

Expand Down Expand Up @@ -266,30 +283,30 @@ async fn prepare_for_write() -> Result<()> {
async fn upgrade(opts: UpgradeOpts) -> Result<()> {
prepare_for_write().await?;
let sysroot = &get_locked_sysroot().await?;
let repo = &sysroot.repo();
let booted_deployment = &sysroot.require_booted_deployment()?;
let status = crate::status::DeploymentStatus::from_deployment(booted_deployment, true)?;
let osname = booted_deployment.osname();
let origin = booted_deployment
.origin()
.ok_or_else(|| anyhow::anyhow!("Deployment is missing an origin"))?;
let imgref = status
.image
.ok_or_else(|| anyhow::anyhow!("Booted deployment is not container image based"))?;
let imgref: OstreeImageReference = imgref.into();
if !status.supported {
let (_deployments, host) = crate::status::get_status(sysroot, Some(booted_deployment))?;
// SAFETY: There must be a status if we have a booted deployment
let status = host.status.unwrap();
let imgref = host.spec.image.as_ref();
// If there's no specified image, let's be nice and check if the booted system is using rpm-ostree
if imgref.is_none() && status.booted.map_or(false, |b| b.incompatible) {
return Err(anyhow::anyhow!(
"Booted deployment contains local rpm-ostree modifications; cannot upgrade via bootc"
));
}
let commit = booted_deployment.csum();
let state = ostree_container::store::query_image_commit(repo, &commit)?;
let digest = state.manifest_digest.as_str();

let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
// Find the currently queued digest, if any before we pull
let queued_digest = status
.staged
.as_ref()
.and_then(|e| e.image.as_ref())
.map(|img| img.image_digest.as_str());
if opts.check {
// pull the image manifest without the layers
let config = Default::default();
let mut imp = ostree_container::store::ImageImporter::new(repo, &imgref, config).await?;
let imgref = &OstreeImageReference::from(imgref.clone());
let mut imp =
ostree_container::store::ImageImporter::new(&sysroot.repo(), imgref, config).await?;
match imp.prepare().await? {
PrepareResult::AlreadyPresent(c) => {
println!(
Expand All @@ -298,24 +315,27 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
);
return Ok(());
}
PrepareResult::Ready(p) => {
PrepareResult::Ready(r) => {
// TODO show a diff
println!(
"New manifest available for {}. Digest {}",
imgref, p.manifest_digest
"New image available for {imgref}. Digest {}",
r.manifest_digest
);
// Note here we'll fall through to handling the --touch-if-changed below
}
}
} else {
let fetched = pull(repo, &imgref, opts.quiet).await?;

if fetched.merge_commit.as_str() == commit.as_str() {
println!("Already queued: {digest}");
return Ok(());
let fetched = pull(&sysroot.repo(), imgref, opts.quiet).await?;
if let Some(queued_digest) = queued_digest {
if fetched.merge_commit.as_str() == queued_digest {
println!("Already queued: {queued_digest}");
return Ok(());
}
}

stage(sysroot, &osname, &imgref, fetched, &origin).await?;
let osname = booted_deployment.osname();
stage(sysroot, &osname, fetched, &host.spec).await?;
}

if let Some(path) = opts.touch_if_changed {
std::fs::write(&path, "").with_context(|| format!("Writing {path}"))?;
}
Expand All @@ -327,14 +347,14 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
#[context("Switching")]
async fn switch(opts: SwitchOpts) -> Result<()> {
prepare_for_write().await?;

let cancellable = gio::Cancellable::NONE;
let sysroot = get_locked_sysroot().await?;
let booted_deployment = &sysroot.require_booted_deployment()?;
let (origin, booted_image) = crate::utils::get_image_origin(booted_deployment)?;
let booted_refspec = origin.optional_string("origin", "refspec")?;
let osname = booted_deployment.osname();

let sysroot = &get_locked_sysroot().await?;
let repo = &sysroot.repo();
let booted_deployment = &sysroot.require_booted_deployment()?;
let (_deployments, host) = crate::status::get_status(sysroot, Some(booted_deployment))?;
// SAFETY: There must be a status if we have a booted deployment
let status = host.status.unwrap();

let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
let imgref = ostree_container::ImageReference {
Expand All @@ -349,30 +369,38 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
SignatureSource::ContainerPolicy
};
let target = ostree_container::OstreeImageReference { sigverify, imgref };
let target = ImageReference::from(target);

let new_spec = {
let mut new_spec = host.spec.clone();
new_spec.image = Some(target.clone());
new_spec
};

if new_spec == host.spec {
anyhow::bail!("No changes in current host spec");
}

let fetched = pull(repo, &target, opts.quiet).await?;

if !opts.retain {
// By default, we prune the previous ostree ref or container image
if let Some(ostree_ref) = booted_refspec {
let (remote, ostree_ref) =
ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
origin.remove_key("origin", "refspec")?;
} else if let Some(booted_image) = booted_image.as_ref() {
ostree_container::store::remove_image(repo, &booted_image.imgref)?;
let _nlayers: u32 = ostree_container::store::gc_image_layers(repo)?;
if let Some(booted_origin) = booted_deployment.origin() {
if let Some(ostree_ref) = booted_origin.optional_string("origin", "refspec")? {
let (remote, ostree_ref) =
ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
} else if let Some(booted_image) = status.booted.as_ref().and_then(|b| b.image.as_ref())
{
let imgref = OstreeImageReference::from(booted_image.image.clone());
ostree_container::store::remove_image(repo, &imgref.imgref)?;
let _nlayers: u32 = ostree_container::store::gc_image_layers(repo)?;
}
}
}

// We always make a fresh origin to toss out old state.
let origin = glib::KeyFile::new();
origin.set_string(
"origin",
ostree_container::deploy::ORIGIN_CONTAINER,
target.to_string().as_str(),
);
stage(&sysroot, &osname, &target, fetched, &origin).await?;
let stateroot = booted_deployment.osname();
stage(sysroot, &stateroot, fetched, &new_spec).await?;

Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod install;
pub(crate) mod mount;
#[cfg(feature = "install")]
mod podman;
pub mod spec;
#[cfg(feature = "install")]
mod task;

Expand Down
7 changes: 4 additions & 3 deletions lib/src/privtests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use rustix::fd::AsFd;
use xshell::{cmd, Shell};

use super::cli::TestingOpts;
use super::spec::Host;

const IMGSIZE: u64 = 20 * 1024 * 1024 * 1024;

Expand Down Expand Up @@ -101,9 +102,9 @@ pub(crate) fn impl_run_host() -> Result<()> {
pub(crate) fn impl_run_container() -> Result<()> {
assert!(ostree_ext::container_utils::is_ostree_container()?);
let sh = Shell::new()?;
let stout = cmd!(sh, "bootc status").read()?;
assert!(stout.contains("Running in a container (ostree base)."));
drop(stout);
let host: Host = serde_yaml::from_str(&cmd!(sh, "bootc status").read()?)?;
let status = host.status.unwrap();
assert!(status.is_container);
for c in ["upgrade", "update"] {
let o = Command::new("bootc").arg(c).output()?;
let st = o.status;
Expand Down
98 changes: 98 additions & 0 deletions lib/src/spec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//! The definition for host system state.

use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Representation of a bootc host system
#[derive(
CustomResource, Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, JsonSchema,
)]
#[kube(
group = "org.containers.bootc",
version = "v1alpha1",
kind = "BootcHost",
struct = "Host",
namespaced,
status = "HostStatus",
derive = "PartialEq",
derive = "Default"
)]
#[serde(rename_all = "camelCase")]
pub struct HostSpec {
/// The host image
pub image: Option<ImageReference>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
/// An image signature
#[serde(rename_all = "camelCase")]
pub enum ImageSignature {
/// Fetches will use the named ostree remote for signature verification of the ostree commit.
OstreeRemote(String),
/// Fetches will defer to the `containers-policy.json`, but we make a best effort to reject `default: insecureAcceptAnything` policy.
ContainerPolicy,
/// No signature verification will be performed
Insecure,
}

/// A container image reference with attached transport and signature verification
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImageReference {
/// The container image reference
pub image: String,
/// The container image transport
pub transport: String,
/// Disable signature verification
pub signature: ImageSignature,
}

/// The status of the booted image
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImageStatus {
/// The currently booted image
pub image: ImageReference,
/// The digest of the fetched image (e.g. sha256:a0...);
pub image_digest: String,
}

/// A bootable entry
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BootEntryOstree {
/// The ostree commit checksum
pub checksum: String,
/// The deployment serial
pub deploy_serial: u32,
}

/// A bootable entry
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BootEntry {
/// The image reference
pub image: Option<ImageStatus>,
/// Whether this boot entry is not compatible (has origin changes bootc does not understand)
pub incompatible: bool,
/// Whether this entry will be subject to garbage collection
pub pinned: bool,
/// If this boot entry is ostree based, the corresponding state
pub ostree: Option<BootEntryOstree>,
}

/// The status of the host system
#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct HostStatus {
/// The staged image for the next boot
pub staged: Option<BootEntry>,
/// The booted image; this will be unset if the host is not bootc compatible.
pub booted: Option<BootEntry>,
/// The previously booted image
pub rollback: Option<BootEntry>,

/// Whether or not the current system state is an ostree-based container
pub is_container: bool,
}
Loading