Skip to content

Commit

Permalink
Adds external key-value string data storage to runtime via Annoations…
Browse files Browse the repository at this point in the history
… objects in Layers. (#815)

Adds --annotation argument to spfs run/shell, --get and --get-all
arguments to spfs info, spfs runtime info, and adds support for
reading the value from an Annotation using spfs read.

Signed-off-by: David Gilligan-Cook <dcook@imageworks.com>
  • Loading branch information
dcookspi authored Mar 14, 2024
1 parent db25168 commit 3da6292
Show file tree
Hide file tree
Showing 40 changed files with 2,288 additions and 110 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/spfs-cli/cmd-enter/src/cmd_enter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ impl CmdEnter {
// For example, the monitor may need to run fusermount to clean up
// fuse filesystems within the runtime before shutting down.
tracing::debug!("initializing runtime {owned:#?}");

let start_time = Instant::now();
let render_summary = spfs::initialize_runtime(&mut owned).await?;
self.report_render_summary(render_summary, start_time.elapsed().as_secs_f64());
Expand Down
6 changes: 5 additions & 1 deletion crates/spfs-cli/cmd-render/src/cmd_render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,11 @@ impl CmdRender {
&render_summary_reporter as &dyn spfs::storage::fs::RenderReporter,
]),
);
let stack = layers.into_iter().map(|l| *l.manifest()).collect();
let stack = layers
.into_iter()
.filter_map(|l| l.manifest().copied())
.collect();
tracing::trace!("stack: {:?}", stack);
renderer
.render(&stack, self.strategy)
.await
Expand Down
1 change: 1 addition & 0 deletions crates/spfs-cli/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ sentry = { workspace = true, optional = true }
sentry-miette = { workspace = true, optional = true }
sentry-tracing = { workspace = true, optional = true }
serde_json = { version = "1.0.57", optional = true }
serde_yaml = { workspace = true }
spfs = { workspace = true }
strip-ansi-escapes = { workspace = true, optional = true }
syslog-tracing = "0.2.0"
Expand Down
59 changes: 58 additions & 1 deletion crates/spfs-cli/common/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ use std::panic::catch_unwind;
#[cfg(feature = "sentry")]
use std::sync::Mutex;

use miette::Error;
use miette::{Error, IntoDiagnostic, Result, WrapErr};
#[cfg(feature = "sentry")]
use once_cell::sync::OnceCell;
use spfs::io::Pluralize;
use spfs::storage::LocalRepository;
use tracing_subscriber::prelude::*;

Expand Down Expand Up @@ -415,6 +416,62 @@ impl Logging {
}
}

/// Command line flags for viewing annotations in a runtime
#[derive(Debug, Clone, clap::Args)]
pub struct AnnotationViewing {
/// Output the data value for the given annotation key(s) from
/// the active runtime. Each value is printed on its own line
/// without its key.
#[clap(long, alias = "annotation")]
pub get: Option<Vec<String>>,

/// Output all the annotation keys and values from the active
/// runtime as a yaml dictionary
#[clap(long, alias = "all-annotations")]
pub get_all: bool,
}

impl AnnotationViewing {
/// Display annotation values based on the command line arguments
pub async fn print_data(&self, runtime: &spfs::runtime::Runtime) -> Result<()> {
if self.get_all {
let data = runtime.all_annotations().await?;
let keys = data
.keys()
.map(ToString::to_string)
.collect::<Vec<String>>();
let num_keys = keys.len();
tracing::debug!(
"{num_keys} annotation {}: {}",
"key".pluralize(num_keys),
keys.join(", ")
);
println!(
"{}",
serde_yaml::to_string(&data)
.into_diagnostic()
.wrap_err("Failed to generate yaml output")?
);
} else if let Some(keys) = &self.get {
tracing::debug!("--get these keys: {}", keys.join(", "));
for key in keys.iter() {
match runtime.annotation(key).await? {
Some(value) => {
tracing::debug!("{key} = {value}");
println!("{value}");
}
None => {
tracing::warn!("No annotation stored under: {key}");
println!();
}
}
}
}

Ok(())
}
}

/// Trait all spfs cli command parsers must implement to provide the
/// name of the spfs command that has been parsed. This method will be
/// called when configuring sentry.
Expand Down
2 changes: 1 addition & 1 deletion crates/spfs-cli/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ pub mod __private {
pub use {libc, spfs};
}

pub use args::{capture_if_relevant, CommandName, Logging, Render, Sync};
pub use args::{capture_if_relevant, AnnotationViewing, CommandName, Logging, Render, Sync};
#[cfg(feature = "sentry")]
pub use args::{configure_sentry, shutdown_sentry};
5 changes: 5 additions & 0 deletions crates/spfs-cli/main/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ nix = { workspace = true, features = ["signal"] }
number_prefix = "*" # we hope to match versions with indicatif
relative-path = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
spfs = { workspace = true }
spfs-cli-common = { workspace = true }
strum = { workspace = true, features = ["derive"] }
Expand All @@ -52,3 +53,7 @@ features = [
"Win32_System_SystemInformation",
"Win32_System_Threading",
]

[dev-dependencies]
rstest = { workspace = true }
tempfile = { workspace = true }
30 changes: 28 additions & 2 deletions crates/spfs-cli/main/src/cmd_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use colored::*;
use miette::Result;
use spfs::env::SPFS_DIR;
use spfs::find_path::ObjectPathEntry;
use spfs::graph::Annotation;
use spfs::io::{self, DigestFormat, Pluralize};
use spfs::prelude::*;
use spfs::{self};
Expand All @@ -20,6 +21,9 @@ pub struct CmdInfo {
#[clap(flatten)]
logging: cli::Logging,

#[clap(flatten)]
annotation: cli::AnnotationViewing,

/// Lists file sizes in human readable format
#[clap(long, short = 'H')]
human_readable: bool,
Expand Down Expand Up @@ -139,10 +143,28 @@ impl CmdInfo {
println!(
" {} {}",
"manifest:".bright_blue(),
self.format_digest(*obj.manifest(), repo).await?
match obj.manifest() {
None => "None".to_string(),
Some(manifest_digest) => self.format_digest(*manifest_digest, repo).await?,
}
);

let annotations = obj.annotations();
if annotations.is_empty() {
println!(" {} none", "annotations:".bright_blue());
} else {
for data in annotations {
let annotation: Annotation = data.into();
println!(" {}", "annotations:".bright_blue());
println!(" {} {}", "key:".bright_blue(), annotation.key());
println!(" {} {}", "value:".bright_blue(), annotation.value());
}
}

if self.follow {
self.to_process.push_back(obj.manifest().to_string());
if let Some(manifest_digest) = obj.manifest() {
self.to_process.push_back(manifest_digest.to_string());
}
}
}

Expand Down Expand Up @@ -199,6 +221,10 @@ impl CmdInfo {
async fn print_global_info(&self, repo: &spfs::storage::RepositoryHandle) -> Result<()> {
let runtime = spfs::active_runtime().await?;

if self.annotation.get_all || self.annotation.get.is_some() {
return self.annotation.print_data(&runtime).await;
}

println!("{}:", "Active Runtime".green());
println!(" {}: {}", "id".bright_blue(), runtime.name());
println!(" {}: {}", "editable".bright_blue(), runtime.status.editable);
Expand Down
120 changes: 119 additions & 1 deletion crates/spfs-cli/main/src/cmd_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,110 @@
// SPDX-License-Identifier: Apache-2.0
// https://github.com/imageworks/spk

use std::collections::BTreeMap;
use std::ffi::OsString;
use std::io;
use std::time::Instant;

use clap::{ArgGroup, Args};
use miette::{Context, Result};
use miette::{miette, Context, IntoDiagnostic, Result};
use spfs::graph::object::EncodingFormat;
use spfs::prelude::*;
use spfs::runtime::KeyValuePair;
use spfs::storage::FromConfig;
use spfs::tracking::EnvSpec;
use spfs_cli_common as cli;

#[cfg(test)]
#[path = "./cmd_run_test.rs"]
mod cmd_run_test;
#[cfg(test)]
#[path = "./fixtures.rs"]
mod fixtures;

#[derive(Args, Clone, Debug)]
pub struct Annotation {
/// Adds annotation key-value string data to the new runtime.
///
/// This allows external processes to store arbitrary data in the
/// runtimes they create. This is most useful with durable runtimes.
/// The data can be retrieved by running `spfs runtime info` or
/// `spfs info` and using the `--get <KEY>` or `--get-all` options
///
/// Annotation data is specified as key-value string pairs
/// separated by either an equals sign or colon (--annotation
/// name=value --annotation other:value). Multiple pairs of
/// annotation data can also be specified at once in yaml or json
/// format (--annotation '{name: value, other: value}').
///
/// Annotation data can also be given in a json or yaml file, by
/// using the `--annotation-file <FILE>` argument. If given,
/// `--annotation` arguments will supersede anything given in
/// annotation files.
///
/// If the same key is used more than once, the last key-value pair
/// will override the earlier values for the same key.
#[clap(long, value_name = "KEY:VALUE")]
pub annotation: Vec<String>,

/// Specify annotation key-value data from a json or yaml file
/// (see --annotation)
#[clap(long)]
pub annotation_file: Vec<std::path::PathBuf>,
}

impl Annotation {
/// Returns a list of annotation key-value pairs gathered from all
/// the annotation related command line arguments. The same keys,
/// and values, can appear multiple times in the list if specified
/// multiple times in various command line arguments.
pub fn get_data(&self) -> Result<Vec<KeyValuePair>> {
let mut data: Vec<KeyValuePair> = Vec::new();

for filename in self.annotation_file.iter() {
let reader: Box<dyn io::Read> =
if Ok("-".to_string()) == filename.clone().into_os_string().into_string() {
// Treat '-' as "read from stdin"
Box::new(io::stdin())
} else {
Box::new(
std::fs::File::open(filename)
.into_diagnostic()
.wrap_err(format!("Failed to open annotation file: {filename:?}"))?,
)
};
let annotation: BTreeMap<String, String> = serde_yaml::from_reader(reader)
.into_diagnostic()
.wrap_err(format!(
"Failed to parse as annotation data key-value pairs: {filename:?}"
))?;
data.extend(annotation);
}

for pair in self.annotation.iter() {
let pair = pair.trim();
if pair.starts_with('{') {
let given: BTreeMap<String, String> = serde_yaml::from_str(pair)
.into_diagnostic()
.wrap_err("--annotation value looked like yaml, but could not be parsed")?;
data.extend(given);
continue;
}

let (name, value) = pair
.split_once('=')
.or_else(|| pair.split_once(':'))
.ok_or_else(|| {
miette!("Invalid option: -annotation {pair} (should be in the form name=value)")
})?;

data.push((name.to_string(), value.to_string()));
}

Ok(data)
}
}

/// Run a program in a configured spfs environment
#[derive(Debug, Args)]
#[clap(group(
Expand Down Expand Up @@ -51,6 +146,9 @@ pub struct CmdRun {
#[clap(long, value_name = "RUNTIME_NAME")]
pub rerun: Option<String>,

#[clap(flatten)]
pub annotation: Annotation,

/// The tag or id of the desired runtime
///
/// Use '-' to request an empty environment
Expand Down Expand Up @@ -146,6 +244,26 @@ impl CmdRun {
);
}

let data = self.annotation.get_data()?;
if !data.is_empty() {
if config.storage.encoding_format == EncodingFormat::Legacy {
return Err(spfs::Error::String(
"Cannot use '--annotation' when spfs is configured to use the 'Legacy' encoding format".to_string(),
)
.into());
}

// These are added in reverse order so that the ones
// specified later on the command line will take precedence.
for (key, value) in data.into_iter().rev() {
tracing::trace!("annotation being added: {key}: {value}");
runtime
.add_annotation(key, value, config.filesystem.annotation_size_limit)
.await?;
}
tracing::trace!(" with annotation: {:?}", runtime);
}

let start_time = Instant::now();
runtime.config.mount_backend = config.filesystem.backend;
runtime.config.secondary_repositories = config.get_secondary_runtime_repositories();
Expand Down
Loading

0 comments on commit 3da6292

Please sign in to comment.