Skip to content
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

Add extra/external annotation data to spfs runtimes #815

Merged
merged 1 commit into from
Mar 14, 2024
Merged
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
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 {
dcookspi marked this conversation as resolved.
Show resolved Hide resolved
/// 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
Loading