Skip to content
Open
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
200 changes: 200 additions & 0 deletions src/aleph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//! Aleph version file support.
//!
//! The "aleph" file records the original OS version at install time.
//! This is written by CoreOS (`.coreos-aleph-version.json`) and
//! bootc (`.bootc-aleph.json`).

/*
* Copyright (C) 2020 Red Hat, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/

use anyhow::{Context, Result};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::path::Path;

#[derive(Serialize, Deserialize, Clone, Debug, Hash, Ord, PartialOrd, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
/// See https://github.com/coreos/fedora-coreos-tracker/blob/66d7d00bedd9d5eabc7287b9577f443dcefb7c04/internals/README-internals.md#aleph-version
pub(crate) struct Aleph {
#[serde(alias = "build")]
pub(crate) version: String,
}

pub(crate) struct AlephWithTimestamp {
pub(crate) aleph: Aleph,
#[allow(dead_code)]
pub(crate) ts: chrono::DateTime<Utc>,
}

/// Known aleph file paths, checked in order of priority.
const ALEPH_PATHS: &[&str] = &[
"sysroot/.coreos-aleph-version.json",
"sysroot/.bootc-aleph.json",
];

pub(crate) fn get_aleph_version(root: &Path) -> Result<Option<AlephWithTimestamp>> {
for aleph_path in ALEPH_PATHS {
let path = &root.join(aleph_path);
if !path.exists() {
continue;
}
let statusf = File::open(path).with_context(|| format!("Opening {path:?}"))?;
let meta = statusf.metadata()?;
let bufr = std::io::BufReader::new(statusf);
let aleph: Aleph = serde_json::from_reader(bufr)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the JSON file is malformed or empty, serde_json::from_reader will fail. Adding context to the error helps with troubleshooting by identifying which file failed to parse.

Suggested change
let aleph: Aleph = serde_json::from_reader(bufr)?;
let aleph: Aleph = serde_json::from_reader(bufr).with_context(|| format!("Parsing {path:?}"))?;

log::debug!("Found aleph version in {aleph_path}");
return Ok(Some(AlephWithTimestamp {
aleph,
ts: meta.created()?.into(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using metadata.created() can fail on filesystems or operating systems that do not support or expose creation times (returning std::io::ErrorKind::Unsupported or similar). Since this error is propagated with ?, it will cause get_aleph_version to fail entirely, which in turn breaks bootupd status and updates on those systems.

Falling back to metadata.modified() when created() fails makes this much more robust.

Suggested change
ts: meta.created()?.into(),
ts: meta.created().or_else(|_| meta.modified())?.into(),

}));
}
Ok(None)
}

#[cfg(test)]
mod test {
use super::*;
use anyhow::Result;

const V1_ALEPH_DATA: &str = r##"
{
"version": "32.20201002.dev.2",
"ref": "fedora/x86_64/coreos/testing-devel",
"ostree-commit": "b2ea6159d6274e1bbbb49aa0ef093eda5d53a75c8a793dbe184f760ed64dc862"
}"##;

// Waiting on https://github.com/rust-lang/rust/pull/125692
#[cfg(not(target_env = "musl"))]
#[test]
fn test_parse_from_root_empty() -> Result<()> {
// Verify we're a no-op in an empty root
let root: &tempfile::TempDir = &tempfile::tempdir()?;
let root = root.path();
assert!(get_aleph_version(root).unwrap().is_none());
Ok(())
}

// Waiting on https://github.com/rust-lang/rust/pull/125692
#[cfg(not(target_env = "musl"))]
#[test]
fn test_parse_from_root_coreos() -> Result<()> {
let root: &tempfile::TempDir = &tempfile::tempdir()?;
let root = root.path();
let sysroot = &root.join("sysroot");
std::fs::create_dir(sysroot).context("Creating sysroot")?;
std::fs::write(root.join(ALEPH_PATHS[0]), V1_ALEPH_DATA).context("Writing aleph")?;
let aleph = get_aleph_version(root).unwrap().unwrap();
assert_eq!(aleph.aleph.version, "32.20201002.dev.2");
Ok(())
}

// Waiting on https://github.com/rust-lang/rust/pull/125692
#[cfg(not(target_env = "musl"))]
#[test]
fn test_parse_from_root_bootc() -> Result<()> {
let root: &tempfile::TempDir = &tempfile::tempdir()?;
let root = root.path();
let sysroot = &root.join("sysroot");
std::fs::create_dir(sysroot).context("Creating sysroot")?;
std::fs::write(root.join(ALEPH_PATHS[1]), V1_ALEPH_DATA).context("Writing aleph")?;
let aleph = get_aleph_version(root).unwrap().unwrap();
assert_eq!(aleph.aleph.version, "32.20201002.dev.2");
Ok(())
}

// Waiting on https://github.com/rust-lang/rust/pull/125692
#[cfg(not(target_env = "musl"))]
#[test]
fn test_parse_coreos_preferred_over_bootc() -> Result<()> {
// When both files exist, the CoreOS aleph should be preferred
let root: &tempfile::TempDir = &tempfile::tempdir()?;
let root = root.path();
let sysroot = &root.join("sysroot");
std::fs::create_dir(sysroot).context("Creating sysroot")?;
let coreos_data = r##"{"version": "coreos-version"}"##;
let bootc_data = r##"{"version": "bootc-version"}"##;
std::fs::write(root.join(ALEPH_PATHS[0]), coreos_data).context("Writing coreos aleph")?;
std::fs::write(root.join(ALEPH_PATHS[1]), bootc_data).context("Writing bootc aleph")?;
let aleph = get_aleph_version(root).unwrap().unwrap();
assert_eq!(aleph.aleph.version, "coreos-version");
Ok(())
}

// Waiting on https://github.com/rust-lang/rust/pull/125692
#[cfg(not(target_env = "musl"))]
#[test]
fn test_parse_from_root_linked() -> Result<()> {
let root: &tempfile::TempDir = &tempfile::tempdir()?;
let root = root.path();
let sysroot = &root.join("sysroot");
std::fs::create_dir(sysroot).context("Creating sysroot")?;
let target_name = ".new-ostree-aleph.json";
let target = &sysroot.join(target_name);
std::fs::write(root.join(target), V1_ALEPH_DATA).context("Writing aleph")?;
std::os::unix::fs::symlink(target_name, root.join(ALEPH_PATHS[0]))
.context("Symlinking")?;
let aleph = get_aleph_version(root).unwrap().unwrap();
assert_eq!(aleph.aleph.version, "32.20201002.dev.2");
Ok(())
}

#[test]
fn test_parse_old_aleph() -> Result<()> {
// What the aleph file looked like before we changed it in
// https://github.com/osbuild/osbuild/pull/1475
let alephdata = r##"
{
"build": "32.20201002.dev.2",
"ref": "fedora/x86_64/coreos/testing-devel",
"ostree-commit": "b2ea6159d6274e1bbbb49aa0ef093eda5d53a75c8a793dbe184f760ed64dc862",
"imgid": "fedora-coreos-32.20201002.dev.2-qemu.x86_64.qcow2"
}"##;
let aleph: Aleph = serde_json::from_str(alephdata)?;
assert_eq!(aleph.version, "32.20201002.dev.2");
Ok(())
}

#[test]
fn test_parse_aleph() -> Result<()> {
let aleph: Aleph = serde_json::from_str(V1_ALEPH_DATA)?;
assert_eq!(aleph.version, "32.20201002.dev.2");
Ok(())
}

#[test]
fn test_parse_bootc_aleph() -> Result<()> {
// A realistic bootc aleph as written by `bootc install`.
// See https://github.com/bootc-dev/bootc/commit/c6112d2d2fa3b785c858741f1585b0578088eda2
let alephdata = r##"
{
"digest": "sha256:07bf537cc4e4d208eb0b978f76e5046e55529ce6192b982d8c1a41fa1d61b95a",
"kernel": "6.18.13-200.fc43.x86_64",
"labels": {
"com.coreos.inputhash": "fe9883169714c593d98058606e886b9747710ed15ab1b9cdbd7fa538fb435b3c",
"com.coreos.osname": "fedora-coreos",
"com.coreos.stream": "testing-devel",
"containers.bootc": "1",
"io.buildah.version": "1.42.2",
"org.opencontainers.image.description": "Fedora CoreOS testing-devel",
"org.opencontainers.image.revision": "233fe18749c7d2749581e4307c4cac60967acde4",
"org.opencontainers.image.source": "git@github.com:jbtrystram/fedora-coreos-config.git",
"org.opencontainers.image.title": "Fedora CoreOS testing-devel",
"org.opencontainers.image.version": "43.20260301.20.dev1",
"ostree.bootable": "1",
"ostree.commit": "89635f7cba9de932fc60d71a6bded65ad0db06a35c9d016da03ca7ade9ba4736",
"ostree.final-diffid": "sha256:12787d84fa137cd5649a9005efe98ec9d05ea46245fdc50aecb7dd007f2035b1"
},
"selinux": "disabled",
"target-image": "ostree-image-signed:docker://quay.io/fedora/fedora-coreos:testing-devel",
"timestamp": null,
"version": "43.20260301.20.dev1"
}"##;
let aleph: Aleph = serde_json::from_str(alephdata)?;
assert_eq!(aleph.version, "43.20260301.20.dev1");
Ok(())
}
}
8 changes: 4 additions & 4 deletions src/bootupd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::bootloader::{get_bootloader, Bootloader};
use crate::cli::bootupd::InstallOpts;
use crate::component::{self, ComponentType};
use crate::component::{Component, ValidationResult};
use crate::coreos;
use crate::aleph;
#[cfg(any(
target_arch = "x86_64",
target_arch = "aarch64",
Expand Down Expand Up @@ -598,7 +598,7 @@ pub(crate) fn status() -> Result<Status> {
// To determine if not-installed components can be adopted:
//
// `query_adopt_state()` checks for existing installation state,
// such as a `version` in `/sysroot/.coreos-aleph-version.json`,
// such as a `version` in an aleph file (e.g. `.coreos-aleph-version.json` or `.bootc-aleph.json`),
// or the presence of `/ostree/deploy`.
//
// `component.query_adopt()` performs additional checks,
Expand Down Expand Up @@ -681,8 +681,8 @@ pub(crate) fn print_status(status: &Status) -> Result<()> {
}
}

if let Some(coreos_aleph) = coreos::get_aleph_version(Path::new("/"))? {
println!("CoreOS aleph version: {}", coreos_aleph.aleph.version);
if let Some(aleph) = aleph::get_aleph_version(Path::new("/"))? {
println!("Aleph version: {}", aleph.aleph.version);
}

#[cfg(any(
Expand Down
8 changes: 4 additions & 4 deletions src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,10 @@ pub(crate) fn write_update_metadata(
#[context("Querying adoptable state")]
pub(crate) fn query_adopt_state() -> Result<Option<Adoptable>> {
// This would be extended with support for other operating systems later
if let Some(coreos_aleph) = crate::coreos::get_aleph_version(Path::new("/"))? {
if let Some(aleph) = crate::aleph::get_aleph_version(Path::new("/"))? {
let meta = ContentMetadata {
timestamp: coreos_aleph.ts,
version: coreos_aleph.aleph.version,
timestamp: aleph.ts,
version: aleph.aleph.version,
versions: None,
#[cfg(efi_arch)]
default_bootloader: None,
Expand All @@ -279,7 +279,7 @@ pub(crate) fn query_adopt_state() -> Result<Option<Adoptable>> {
confident: true,
}));
} else {
log::trace!("No CoreOS aleph detected");
log::trace!("No aleph version file detected");
}
let ostree_deploy_dir = Path::new("/ostree/deploy");
if ostree_deploy_dir.exists() {
Expand Down
123 changes: 0 additions & 123 deletions src/coreos.rs

This file was deleted.

2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ mod bootloader;
mod bootupd;
mod cli;
mod component;
mod coreos;
mod aleph;
#[cfg(any(
target_arch = "x86_64",
target_arch = "aarch64",
Expand Down
Loading