Skip to content

Commit

Permalink
WIP host system state
Browse files Browse the repository at this point in the history
  • Loading branch information
cgwalters committed Jul 4, 2023
1 parent c6243d4 commit 317c4be
Show file tree
Hide file tree
Showing 6 changed files with 389 additions and 229 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
212 changes: 138 additions & 74 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ pub(crate) struct UpgradeOpts {
pub(crate) touch_if_changed: Option<Utf8PathBuf>,
}

/// Interactively change desired system state
#[derive(Debug, Parser)]
pub(crate) struct EditOpts {
/// Input path to new d
pub(crate) filename: String,
}

/// Perform an upgrade operation
#[derive(Debug, Parser)]
pub(crate) struct SwitchOpts {
Expand Down Expand Up @@ -102,6 +109,8 @@ pub(crate) enum Opt {
/// Look for updates to the booted container image.
#[clap(alias = "update")]
Upgrade(UpgradeOpts),
/// Change desired host system state
Edit(EditOpts),
/// Target a new container image reference to boot.
Switch(SwitchOpts),
/// Display status
Expand Down Expand Up @@ -263,36 +272,37 @@ 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 {
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 fetched = pull(repo, &imgref, opts.quiet).await?;

if fetched.merge_commit.as_str() == commit.as_str() {
println!("Already queued: {digest}");
return Ok(());
}

stage(sysroot, &osname, &imgref, fetched, &origin).await?;

if let Some(path) = opts.touch_if_changed {
std::fs::write(&path, "").with_context(|| format!("Writing {path}"))?;
}
todo!();
// 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 {
// 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 fetched = pull(repo, &imgref, opts.quiet).await?;

// if fetched.merge_commit.as_str() == commit.as_str() {
// println!("Already queued: {digest}");
// return Ok(());
// }

// stage(sysroot, &osname, &imgref, fetched, &origin).await?;

// if let Some(path) = opts.touch_if_changed {
// std::fs::write(&path, "").with_context(|| format!("Writing {path}"))?;
// }

Ok(())
}
Expand All @@ -302,51 +312,104 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
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 repo = &sysroot.repo();

let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
let imgref = ostree_container::ImageReference {
transport,
name: opts.target.to_string(),
};
let sigverify = if opts.no_signature_verification {
SignatureSource::ContainerPolicyAllowInsecure
} else if let Some(remote) = opts.ostree_remote.as_ref() {
SignatureSource::OstreeRemote(remote.to_string())
} else {
SignatureSource::ContainerPolicy
};
let target = ostree_container::OstreeImageReference { sigverify, imgref };

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)?;
}
}
todo!()
// 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().unwrap();
// let repo = &sysroot.repo().unwrap();

// let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
// let imgref = ostree_container::ImageReference {
// transport,
// name: opts.target.to_string(),
// };
// let sigverify = if opts.no_signature_verification {
// SignatureSource::ContainerPolicyAllowInsecure
// } else if let Some(remote) = opts.ostree_remote.as_ref() {
// SignatureSource::OstreeRemote(remote.to_string())
// } else {
// SignatureSource::ContainerPolicy
// };
// let target = ostree_container::OstreeImageReference { sigverify, imgref };

// 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)?;
// }
// }

// // 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?;

// Ok(())
}

// 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?;
/// Implementation of the `bootc edit` CLI command.
#[context("Editing state")]
async fn edit(_opts: EditOpts) -> Result<()> {
prepare_for_write().await?;
todo!();
// 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 repo = &sysroot.repo();

// let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
// let imgref = ostree_container::ImageReference {
// transport,
// name: opts.target.to_string(),
// };
// let sigverify = if opts.no_signature_verification {
// SignatureSource::ContainerPolicyAllowInsecure
// } else if let Some(remote) = opts.ostree_remote.as_ref() {
// SignatureSource::OstreeRemote(remote.to_string())
// } else {
// SignatureSource::ContainerPolicy
// };
// let target = ostree_container::OstreeImageReference { sigverify, imgref };

// 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)?;
// }
// }

// // 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?;

Ok(())
}
Expand All @@ -371,6 +434,7 @@ where
let opt = Opt::parse_from(args);
match opt {
Opt::Upgrade(opts) => upgrade(opts).await,
Opt::Edit(opts) => edit(opts).await,
Opt::Switch(opts) => switch(opts).await,
Opt::UsrOverlay => usroverlay().await,
#[cfg(feature = "install")]
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
103 changes: 103 additions & 0 deletions lib/src/spec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//! The definition for host system state.

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

fn default_transport() -> String {
"registry".into()
}

/// 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
#[serde(default = "default_transport")]
pub transport: String,
/// Disable signature verification
pub signature: ImageSignature,
}

/// The status of the booted image
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, 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, 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, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BootEntry {
/// The image reference
pub image: Option<ImageStatus>,
/// Whether this boot entry supports changes via bootc
pub compatible: 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, Deserialize, PartialEq, 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 317c4be

Please sign in to comment.