-
Notifications
You must be signed in to change notification settings - Fork 33
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
rpm-ostree: Use upstream Rust bindings #491
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,15 +2,16 @@ | |
|
||
use super::actor::{RpmOstreeClient, StatusCache}; | ||
use super::Release; | ||
use failure::{bail, ensure, format_err, Fallible, ResultExt}; | ||
use failure::{format_err, Fallible, ResultExt}; | ||
use filetime::FileTime; | ||
use log::trace; | ||
use prometheus::IntCounter; | ||
use serde::Deserialize; | ||
use std::collections::BTreeSet; | ||
use std::fs; | ||
use std::rc::Rc; | ||
|
||
use super::CLI_CLIENT; | ||
|
||
/// Path to local OSTree deployments. We use its mtime to check for modifications (e.g. new deployments) | ||
/// to local deployments that might warrant querying `rpm-ostree status` again to update our knowledge | ||
/// of the current state of deployments. | ||
|
@@ -37,83 +38,31 @@ lazy_static::lazy_static! { | |
)).unwrap(); | ||
} | ||
|
||
/// JSON output from `rpm-ostree status --json` | ||
#[derive(Clone, Debug, Deserialize)] | ||
pub struct StatusJSON { | ||
deployments: Vec<DeploymentJSON>, | ||
} | ||
|
||
/// Partial deployment object (only fields relevant to zincati). | ||
#[derive(Clone, Debug, Deserialize)] | ||
#[serde(rename_all = "kebab-case")] | ||
pub struct DeploymentJSON { | ||
booted: bool, | ||
base_checksum: Option<String>, | ||
#[serde(rename = "base-commit-meta")] | ||
base_metadata: BaseCommitMetaJSON, | ||
checksum: String, | ||
// NOTE(lucab): missing field means "not staged". | ||
#[serde(default)] | ||
staged: bool, | ||
version: String, | ||
} | ||
|
||
/// Metadata from base commit (only fields relevant to zincati). | ||
#[derive(Clone, Debug, Deserialize)] | ||
struct BaseCommitMetaJSON { | ||
#[serde(rename = "coreos-assembler.basearch")] | ||
basearch: String, | ||
#[serde(rename = "fedora-coreos.stream")] | ||
stream: String, | ||
} | ||
|
||
impl DeploymentJSON { | ||
/// Convert into `Release`. | ||
pub fn into_release(self) -> Release { | ||
impl From<&rpmostree_client::Deployment> for Release { | ||
fn from(d: &rpmostree_client::Deployment) -> Self { | ||
Release { | ||
checksum: self.base_revision(), | ||
version: self.version, | ||
checksum: d | ||
.base_checksum | ||
.clone() | ||
.unwrap_or_else(|| d.checksum.clone()), | ||
version: d.version.clone().unwrap_or_default(), | ||
age_index: None, | ||
} | ||
} | ||
|
||
/// Return the deployment base revision. | ||
pub fn base_revision(&self) -> String { | ||
cgwalters marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.base_checksum | ||
.clone() | ||
.unwrap_or_else(|| self.checksum.clone()) | ||
} | ||
} | ||
|
||
/// Parse base architecture for booted deployment from status object. | ||
pub fn parse_basearch(status: &StatusJSON) -> Fallible<String> { | ||
let json = booted_json(status)?; | ||
Ok(json.base_metadata.basearch) | ||
} | ||
|
||
/// Parse the booted deployment from status object. | ||
pub fn parse_booted(status: &StatusJSON) -> Fallible<Release> { | ||
let json = booted_json(status)?; | ||
Ok(json.into_release()) | ||
} | ||
|
||
/// Parse updates stream for booted deployment from status object. | ||
pub fn parse_updates_stream(status: &StatusJSON) -> Fallible<String> { | ||
let json = booted_json(status)?; | ||
ensure!(!json.base_metadata.stream.is_empty(), "empty stream value"); | ||
Ok(json.base_metadata.stream) | ||
} | ||
|
||
/// Parse local deployments from a status object. | ||
fn parse_local_deployments(status: &StatusJSON, omit_staged: bool) -> Fallible<BTreeSet<Release>> { | ||
fn parse_local_deployments( | ||
status: &rpmostree_client::Status, | ||
omit_staged: bool, | ||
) -> Fallible<BTreeSet<Release>> { | ||
let mut deployments = BTreeSet::<Release>::new(); | ||
for entry in &status.deployments { | ||
if omit_staged && entry.staged { | ||
if omit_staged && entry.staged.unwrap_or_default() { | ||
continue; | ||
} | ||
|
||
let release = entry.clone().into_release(); | ||
deployments.insert(release); | ||
deployments.insert(entry.into()); | ||
} | ||
Ok(deployments) | ||
} | ||
|
@@ -123,29 +72,14 @@ pub fn local_deployments( | |
client: &mut RpmOstreeClient, | ||
omit_staged: bool, | ||
) -> Fallible<BTreeSet<Release>> { | ||
let status = status_json(client)?; | ||
let status = query_status(client)?; | ||
let local_depls = parse_local_deployments(&status, omit_staged)?; | ||
|
||
Ok(local_depls) | ||
} | ||
|
||
/// Return JSON object for booted deployment. | ||
fn booted_json(status: &StatusJSON) -> Fallible<DeploymentJSON> { | ||
let booted = status | ||
.clone() | ||
.deployments | ||
.into_iter() | ||
.find(|d| d.booted) | ||
.ok_or_else(|| format_err!("no booted deployment found"))?; | ||
|
||
ensure!(!booted.base_revision().is_empty(), "empty base revision"); | ||
ensure!(!booted.version.is_empty(), "empty version"); | ||
ensure!(!booted.base_metadata.basearch.is_empty(), "empty basearch"); | ||
Ok(booted) | ||
} | ||
|
||
/// Ensure our status cache is up to date; if empty or out of date, run `rpm-ostree status` to populate it. | ||
fn status_json(client: &mut RpmOstreeClient) -> Fallible<Rc<StatusJSON>> { | ||
fn query_status_inner(client: &mut RpmOstreeClient) -> Fallible<Rc<rpmostree_client::Status>> { | ||
STATUS_CACHE_ATTEMPTS.inc(); | ||
let ostree_depls_data = fs::metadata(OSTREE_DEPLS_PATH) | ||
.with_context(|e| format_err!("failed to query directory {}: {}", OSTREE_DEPLS_PATH, e))?; | ||
|
@@ -160,7 +94,9 @@ fn status_json(client: &mut RpmOstreeClient) -> Fallible<Rc<StatusJSON>> { | |
|
||
STATUS_CACHE_MISSES.inc(); | ||
trace!("cache stale, invoking rpm-ostree to retrieve local deployments"); | ||
let status = Rc::new(invoke_cli_status(false)?); | ||
let status = Rc::new( | ||
rpmostree_client::query_status(&*CLI_CLIENT).map_err(failure::Error::from_boxed_compat)?, | ||
); | ||
client.status_cache = Some(StatusCache { | ||
status: Rc::clone(&status), | ||
mtime: ostree_depls_data_mtime, | ||
|
@@ -170,41 +106,26 @@ fn status_json(client: &mut RpmOstreeClient) -> Fallible<Rc<StatusJSON>> { | |
} | ||
|
||
/// CLI executor for `rpm-ostree status --json`. | ||
pub fn invoke_cli_status(booted_only: bool) -> Fallible<StatusJSON> { | ||
pub fn query_status(client: &mut RpmOstreeClient) -> Fallible<Rc<rpmostree_client::Status>> { | ||
RPM_OSTREE_STATUS_ATTEMPTS.inc(); | ||
|
||
let mut cmd = std::process::Command::new("rpm-ostree"); | ||
cmd.arg("status").env("RPMOSTREE_CLIENT_ID", "zincati"); | ||
cgwalters marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Try to request the minimum scope we need. | ||
if booted_only { | ||
cmd.arg("--booted"); | ||
} | ||
|
||
let cmdrun = cmd | ||
.arg("--json") | ||
.output() | ||
.with_context(|_| "failed to run 'rpm-ostree' binary")?; | ||
|
||
if !cmdrun.status.success() { | ||
RPM_OSTREE_STATUS_FAILURES.inc(); | ||
bail!( | ||
"rpm-ostree status failed:\n{}", | ||
String::from_utf8_lossy(&cmdrun.stderr) | ||
); | ||
match query_status_inner(client) { | ||
Ok(s) => Ok(s), | ||
Err(e) => { | ||
RPM_OSTREE_STATUS_FAILURES.inc(); | ||
Err(e) | ||
} | ||
} | ||
let status: StatusJSON = serde_json::from_slice(&cmdrun.stdout)?; | ||
Ok(status) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
fn mock_status(path: &str) -> Fallible<StatusJSON> { | ||
fn mock_status(path: &str) -> Fallible<rpmostree_client::Status> { | ||
cgwalters marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let fp = std::fs::File::open(path).unwrap(); | ||
let bufrd = std::io::BufReader::new(fp); | ||
let status: StatusJSON = serde_json::from_reader(bufrd)?; | ||
let status = serde_json::from_reader(bufrd)?; | ||
Ok(status) | ||
} | ||
|
||
|
@@ -226,18 +147,4 @@ mod tests { | |
assert_eq!(deployments.len(), 1); | ||
} | ||
} | ||
|
||
#[test] | ||
fn mock_booted_basearch() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These two are some basic unit tests to ensure we don't regress on parsing FCOS-specific metadata, I'd rather not drop it. |
||
let status = mock_status("tests/fixtures/rpm-ostree-status.json").unwrap(); | ||
let booted = booted_json(&status).unwrap(); | ||
assert_eq!(booted.base_metadata.basearch, "x86_64"); | ||
} | ||
|
||
#[test] | ||
fn mock_booted_updates_stream() { | ||
let status = mock_status("tests/fixtures/rpm-ostree-status.json").unwrap(); | ||
let booted = booted_json(&status).unwrap(); | ||
assert_eq!(booted.base_metadata.stream, "testing-devel"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,7 +1,8 @@ | ||||||
mod cli_deploy; | ||||||
mod cli_finalize; | ||||||
mod cli_status; | ||||||
pub use cli_status::{invoke_cli_status, parse_basearch, parse_booted, parse_updates_stream}; | ||||||
pub use cli_status::query_status; | ||||||
use lazy_static::lazy_static; | ||||||
|
||||||
mod actor; | ||||||
pub use actor::{ | ||||||
|
@@ -16,6 +17,16 @@ use failure::{ensure, format_err, Fallible, ResultExt}; | |||||
use serde::Serialize; | ||||||
use std::cmp::Ordering; | ||||||
|
||||||
/// Metadata key the base (RPM) architecture; injected by coreos-assembler | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
pub(crate) const COSA_BASEARCH: &str = "coreos-assembler.basearch"; | ||||||
/// Metadata key for the Fedora CoreOS stream, injected by FCOS tooling via cosa. | ||||||
pub(crate) const FCOS_STREAM: &str = "fedora-coreos.stream"; | ||||||
|
||||||
lazy_static! { | ||||||
pub(crate) static ref CLI_CLIENT: rpmostree_client::CliClient = | ||||||
rpmostree_client::CliClient::new("zincati"); | ||||||
} | ||||||
|
||||||
/// An OS release. | ||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)] | ||||||
pub struct Release { | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks sketchy, and may break other logic in the app.
I don't know in which cases we can end up with a version-less commit, but it may be good to log a warning and filter it out.