Skip to content

Commit

Permalink
Merge pull request containers#106 from cgwalters/k8s-prep
Browse files Browse the repository at this point in the history
Rework API to use a Kubernetes CRD
  • Loading branch information
jmarrero authored Jul 6, 2023
2 parents 41ebc7f + 9f19235 commit e851fbb
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 223 deletions.
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

0 comments on commit e851fbb

Please sign in to comment.