Skip to content

cli: Add an edit verb #107

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

Merged
merged 1 commit into from
Jul 12, 2023
Merged
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
56 changes: 55 additions & 1 deletion lib/src/cli.rs
Original file line number Diff line number Diff line change
@@ -16,9 +16,11 @@ use ostree_ext::keyfileext::KeyFileExt;
use ostree_ext::ostree;
use ostree_ext::sysroot::SysrootLock;
use std::ffi::OsString;
use std::io::Seek;
use std::os::unix::process::CommandExt;
use std::process::Command;

use crate::spec::Host;
use crate::spec::HostSpec;
use crate::spec::ImageReference;

@@ -64,7 +66,18 @@ pub(crate) struct SwitchOpts {
pub(crate) target: String,
}

/// Perform a status operation
/// Perform an edit operation
#[derive(Debug, Parser)]
pub(crate) struct EditOpts {
/// Path to new system specification; use `-` for stdin
pub(crate) filename: String,

/// Don't display progress
#[clap(long)]
pub(crate) quiet: bool,
}

/// Perform an status operation
#[derive(Debug, Parser)]
pub(crate) struct StatusOpts {
/// Output in JSON format.
@@ -111,6 +124,8 @@ pub(crate) enum Opt {
Upgrade(UpgradeOpts),
/// Target a new container image reference to boot.
Switch(SwitchOpts),
/// Change host specification
Edit(EditOpts),
/// Display status
Status(StatusOpts),
/// Add a transient writable overlayfs on `/usr` that will be discarded on reboot.
@@ -405,6 +420,44 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
Ok(())
}

/// Implementation of the `bootc edit` CLI command.
#[context("Editing spec")]
async fn edit(opts: EditOpts) -> Result<()> {
prepare_for_write().await?;
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))?;

let new_host: Host = if opts.filename == "-" {
let tmpf = tempfile::NamedTempFile::new()?;
serde_yaml::to_writer(std::io::BufWriter::new(tmpf.as_file()), &host)?;
crate::utils::spawn_editor(&tmpf)?;
tmpf.as_file().seek(std::io::SeekFrom::Start(0))?;
serde_yaml::from_reader(&mut tmpf.as_file())?
} else {
let mut r = std::io::BufReader::new(std::fs::File::open(opts.filename)?);
serde_yaml::from_reader(&mut r)?
};

if new_host.spec == host.spec {
anyhow::bail!("No changes in current host spec");
}
let new_image = new_host
.spec
.image
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Unable to transition to unset image"))?;
let fetched = pull(repo, new_image, opts.quiet).await?;

// TODO gc old layers here

let stateroot = booted_deployment.osname();
stage(sysroot, &stateroot, fetched, &new_host.spec).await?;

Ok(())
}

/// Implementation of `bootc usroverlay`
async fn usroverlay() -> Result<()> {
// This is just a pass-through today. At some point we may make this a libostree API
@@ -426,6 +479,7 @@ where
match opt {
Opt::Upgrade(opts) => upgrade(opts).await,
Opt::Switch(opts) => switch(opts).await,
Opt::Edit(opts) => edit(opts).await,
Opt::UsrOverlay => usroverlay().await,
#[cfg(feature = "install")]
Opt::Install(opts) => crate::install::install(opts).await,
22 changes: 22 additions & 0 deletions lib/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::os::unix::prelude::OsStringExt;
use std::process::Command;

use anyhow::{Context, Result};
use ostree::glib;
use ostree_ext::ostree;

@@ -24,6 +26,26 @@ pub(crate) fn run_in_host_mountns(cmd: &str) -> Command {
c
}

pub(crate) fn spawn_editor(tmpf: &tempfile::NamedTempFile) -> Result<()> {
let v = "EDITOR";
let editor = std::env::var_os(v)
.ok_or_else(|| anyhow::anyhow!("{v} is unset"))?
.into_vec();
let editor = String::from_utf8(editor).with_context(|| format!("{v} is invalid UTF-8"))?;
let mut editor_args = editor.split_ascii_whitespace();
let argv0 = editor_args
.next()
.ok_or_else(|| anyhow::anyhow!("Invalid {v}: {editor}"))?;
let status = Command::new(argv0)
.args(editor_args)
.arg(tmpf.path())
.status()?;
if !status.success() {
anyhow::bail!("Invoking {v}: {editor} failed: {status:?}");
}
Ok(())
}

/// Given a possibly tagged image like quay.io/foo/bar:latest and a digest 0ab32..., return
/// the digested form quay.io/foo/bar:latest@sha256:0ab32...
/// If the image already has a digest, it will be replaced.