Skip to content

Commit

Permalink
Rework API to use a Kubernetes CRD
Browse files Browse the repository at this point in the history
I was working on the configmap support slowly, but a big issue
it raised was how we expose the ability to mutate things.

Our current "API" is just an imperative CLI.  This would require
higher level tooling to manage Kubernetes style declarative system state.

Instead, let's use the really nice Rust `kube` crate to define a CRD;
this is what is output (in YAML form) via `bootc status` now.

We translate the imperative CLI verbs into changes to the `spec`
field.

However, things become more compelling when we offer a `bootc edit`
CLI verb that allows arbitrary changes to the spec.

I think this will become the *only* way to manage attached configmaps,
instead of having imperative CLI verbs like `bootc configmap add` etc.
At least to start.

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed Jul 6, 2023
1 parent 41ebc7f commit 9f19235
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 9f19235

Please sign in to comment.