Skip to content

Commit a100269

Browse files
authored
Merge pull request #107 from cgwalters/edit
cli: Add an `edit` verb
2 parents e851fbb + c78b7ef commit a100269

File tree

2 files changed

+77
-1
lines changed

2 files changed

+77
-1
lines changed

lib/src/cli.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ use ostree_ext::keyfileext::KeyFileExt;
1616
use ostree_ext::ostree;
1717
use ostree_ext::sysroot::SysrootLock;
1818
use std::ffi::OsString;
19+
use std::io::Seek;
1920
use std::os::unix::process::CommandExt;
2021
use std::process::Command;
2122

23+
use crate::spec::Host;
2224
use crate::spec::HostSpec;
2325
use crate::spec::ImageReference;
2426

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

67-
/// Perform a status operation
69+
/// Perform an edit operation
70+
#[derive(Debug, Parser)]
71+
pub(crate) struct EditOpts {
72+
/// Path to new system specification; use `-` for stdin
73+
pub(crate) filename: String,
74+
75+
/// Don't display progress
76+
#[clap(long)]
77+
pub(crate) quiet: bool,
78+
}
79+
80+
/// Perform an status operation
6881
#[derive(Debug, Parser)]
6982
pub(crate) struct StatusOpts {
7083
/// Output in JSON format.
@@ -111,6 +124,8 @@ pub(crate) enum Opt {
111124
Upgrade(UpgradeOpts),
112125
/// Target a new container image reference to boot.
113126
Switch(SwitchOpts),
127+
/// Change host specification
128+
Edit(EditOpts),
114129
/// Display status
115130
Status(StatusOpts),
116131
/// Add a transient writable overlayfs on `/usr` that will be discarded on reboot.
@@ -405,6 +420,44 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
405420
Ok(())
406421
}
407422

423+
/// Implementation of the `bootc edit` CLI command.
424+
#[context("Editing spec")]
425+
async fn edit(opts: EditOpts) -> Result<()> {
426+
prepare_for_write().await?;
427+
let sysroot = &get_locked_sysroot().await?;
428+
let repo = &sysroot.repo();
429+
let booted_deployment = &sysroot.require_booted_deployment()?;
430+
let (_deployments, host) = crate::status::get_status(sysroot, Some(booted_deployment))?;
431+
432+
let new_host: Host = if opts.filename == "-" {
433+
let tmpf = tempfile::NamedTempFile::new()?;
434+
serde_yaml::to_writer(std::io::BufWriter::new(tmpf.as_file()), &host)?;
435+
crate::utils::spawn_editor(&tmpf)?;
436+
tmpf.as_file().seek(std::io::SeekFrom::Start(0))?;
437+
serde_yaml::from_reader(&mut tmpf.as_file())?
438+
} else {
439+
let mut r = std::io::BufReader::new(std::fs::File::open(opts.filename)?);
440+
serde_yaml::from_reader(&mut r)?
441+
};
442+
443+
if new_host.spec == host.spec {
444+
anyhow::bail!("No changes in current host spec");
445+
}
446+
let new_image = new_host
447+
.spec
448+
.image
449+
.as_ref()
450+
.ok_or_else(|| anyhow::anyhow!("Unable to transition to unset image"))?;
451+
let fetched = pull(repo, new_image, opts.quiet).await?;
452+
453+
// TODO gc old layers here
454+
455+
let stateroot = booted_deployment.osname();
456+
stage(sysroot, &stateroot, fetched, &new_host.spec).await?;
457+
458+
Ok(())
459+
}
460+
408461
/// Implementation of `bootc usroverlay`
409462
async fn usroverlay() -> Result<()> {
410463
// This is just a pass-through today. At some point we may make this a libostree API
@@ -426,6 +479,7 @@ where
426479
match opt {
427480
Opt::Upgrade(opts) => upgrade(opts).await,
428481
Opt::Switch(opts) => switch(opts).await,
482+
Opt::Edit(opts) => edit(opts).await,
429483
Opt::UsrOverlay => usroverlay().await,
430484
#[cfg(feature = "install")]
431485
Opt::Install(opts) => crate::install::install(opts).await,

lib/src/utils.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
use std::os::unix::prelude::OsStringExt;
12
use std::process::Command;
23

4+
use anyhow::{Context, Result};
35
use ostree::glib;
46
use ostree_ext::ostree;
57

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

29+
pub(crate) fn spawn_editor(tmpf: &tempfile::NamedTempFile) -> Result<()> {
30+
let v = "EDITOR";
31+
let editor = std::env::var_os(v)
32+
.ok_or_else(|| anyhow::anyhow!("{v} is unset"))?
33+
.into_vec();
34+
let editor = String::from_utf8(editor).with_context(|| format!("{v} is invalid UTF-8"))?;
35+
let mut editor_args = editor.split_ascii_whitespace();
36+
let argv0 = editor_args
37+
.next()
38+
.ok_or_else(|| anyhow::anyhow!("Invalid {v}: {editor}"))?;
39+
let status = Command::new(argv0)
40+
.args(editor_args)
41+
.arg(tmpf.path())
42+
.status()?;
43+
if !status.success() {
44+
anyhow::bail!("Invoking {v}: {editor} failed: {status:?}");
45+
}
46+
Ok(())
47+
}
48+
2749
/// Given a possibly tagged image like quay.io/foo/bar:latest and a digest 0ab32..., return
2850
/// the digested form quay.io/foo/bar:latest@sha256:0ab32...
2951
/// If the image already has a digest, it will be replaced.

0 commit comments

Comments
 (0)