Skip to content

Commit

Permalink
Infer predefined annotations when pushing to registry
Browse files Browse the repository at this point in the history
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
  • Loading branch information
itowlson committed Jul 10, 2024
1 parent 436ad58 commit f1bc4d5
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 4 deletions.
1 change: 1 addition & 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/oci/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ async-compression = "0.4.3"
# Fork with nested async-std dependency bumped to satisfy Windows build; branch/revision is protected
async-tar = { git = "https://github.com/vdice/async-tar", rev = "71e037f9652971e7a55b412a8e47a37b06f9c29d" }
base64 = "0.21"
chrono = "0.4"
# Fork with updated auth to support ACR login
# Ref https://github.com/camallo/dkregistry-rs/pull/263
dkregistry = { git = "https://github.com/fermyon/dkregistry-rs", rev = "161cf2b66996ed97c7abaf046e38244484814de3" }
Expand Down
90 changes: 88 additions & 2 deletions crates/oci/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ pub struct ClientOpts {
pub content_ref_inline_max_size: usize,
}

/// Controls whether predefined annotations are generated when pushing an application.
/// If an explicit annotation has the same name as a predefined one, the explicit
/// one takes precedence.
#[derive(Debug, PartialEq)]
pub enum InferPredefinedAnnotations {
/// Infer annotations for created, authors, version, name and description.
All,
/// Do not generate any annotations; use only explicitly supplied annotations.
None,
}

impl Client {
/// Create a new instance of an OCI client for distributing Spin applications.
pub async fn new(insecure: bool, cache_root: Option<PathBuf>) -> Result<Self> {
Expand All @@ -107,6 +118,7 @@ impl Client {
manifest_path: &Path,
reference: impl AsRef<str>,
annotations: Option<BTreeMap<String, String>>,
infer_annotations: InferPredefinedAnnotations,
) -> Result<Option<String>> {
let reference: Reference = reference
.as_ref()
Expand All @@ -125,7 +137,7 @@ impl Client {
)
.await?;

self.push_locked_core(locked, auth, reference, annotations)
self.push_locked_core(locked, auth, reference, annotations, infer_annotations)
.await
}

Expand All @@ -136,14 +148,15 @@ impl Client {
locked: LockedApp,
reference: impl AsRef<str>,
annotations: Option<BTreeMap<String, String>>,
infer_annotations: InferPredefinedAnnotations,
) -> Result<Option<String>> {
let reference: Reference = reference
.as_ref()
.parse()
.with_context(|| format!("cannot parse reference {}", reference.as_ref()))?;
let auth = Self::auth(&reference).await?;

self.push_locked_core(locked, auth, reference, annotations)
self.push_locked_core(locked, auth, reference, annotations, infer_annotations)
.await
}

Expand All @@ -155,6 +168,7 @@ impl Client {
auth: RegistryAuth,
reference: Reference,
annotations: Option<BTreeMap<String, String>>,
infer_annotations: InferPredefinedAnnotations,
) -> Result<Option<String>> {
let mut locked_app = locked.clone();
let mut layers = self
Expand All @@ -174,6 +188,8 @@ impl Client {
.context("could not assemble archive layers for locked application")?;
}

let annotations = all_annotations(&locked_app, annotations, infer_annotations);

// Push layer for locked spin application config
let locked_config_layer = ImageLayer::new(
serde_json::to_vec(&locked_app).context("could not serialize locked config")?,
Expand Down Expand Up @@ -688,6 +704,76 @@ fn registry_from_input(server: impl AsRef<str>) -> String {
}
}

fn all_annotations(
locked_app: &LockedApp,
explicit: Option<BTreeMap<String, String>>,
predefined: InferPredefinedAnnotations,
) -> Option<BTreeMap<String, String>> {
use spin_locked_app::{MetadataKey, APP_DESCRIPTION_KEY, APP_NAME_KEY, APP_VERSION_KEY};
const APP_AUTHORS_KEY: MetadataKey<Vec<String>> = MetadataKey::new("authors");

if predefined == InferPredefinedAnnotations::None {
return explicit;
}

// We will always, at minimum, have a `created` annotation, so if we don't already have an
// anootations collection then we may as well create one now...
let mut current = explicit.unwrap_or_default();

let authors = locked_app
.get_metadata(APP_AUTHORS_KEY)
.unwrap_or_default()
.unwrap_or_default();
if !authors.is_empty() {
let authors = authors.join(", ");
add_inferred(
&mut current,
oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_AUTHORS,
Some(authors),
);
}

let name = locked_app.get_metadata(APP_NAME_KEY).unwrap_or_default();
add_inferred(
&mut current,
oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_TITLE,
name,
);

let description = locked_app
.get_metadata(APP_DESCRIPTION_KEY)
.unwrap_or_default();
add_inferred(
&mut current,
oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_DESCRIPTION,
description,
);

let version = locked_app.get_metadata(APP_VERSION_KEY).unwrap_or_default();
add_inferred(
&mut current,
oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_VERSION,
version,
);

let created = chrono::Utc::now().to_rfc3339();
add_inferred(
&mut current,
oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_CREATED,
Some(created),
);

Some(current)
}

fn add_inferred(map: &mut BTreeMap<String, String>, key: &str, value: Option<String>) {
if let Some(value) = value {
if let std::collections::btree_map::Entry::Vacant(e) = map.entry(key.to_string()) {
e.insert(value);
}
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
11 changes: 9 additions & 2 deletions src/commands/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use indicatif::{ProgressBar, ProgressStyle};
use spin_common::arg_parser::parse_kv;
use spin_oci::Client;
use spin_oci::{client::InferPredefinedAnnotations, Client};
use std::{io::Read, path::PathBuf, time::Duration};

/// Commands for working with OCI registries to distribute applications.
Expand Down Expand Up @@ -86,7 +86,14 @@ impl Push {

let _spinner = create_dotted_spinner(2000, "Pushing app to the Registry".to_owned());

let digest = client.push(&app_file, &self.reference, annotations).await?;
let digest = client
.push(
&app_file,
&self.reference,
annotations,
InferPredefinedAnnotations::All,
)
.await?;
match digest {
Some(digest) => println!("Pushed with digest {digest}"),
None => println!("Pushed; the registry did not return the digest"),
Expand Down

0 comments on commit f1bc4d5

Please sign in to comment.