Skip to content

Commit 3b54e14

Browse files
committed
Infer predefined annotations when pushing to registry
Signed-off-by: itowlson <[email protected]>
1 parent 436ad58 commit 3b54e14

File tree

4 files changed

+247
-4
lines changed

4 files changed

+247
-4
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oci/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ async-compression = "0.4.3"
1010
# Fork with nested async-std dependency bumped to satisfy Windows build; branch/revision is protected
1111
async-tar = { git = "https://github.com/vdice/async-tar", rev = "71e037f9652971e7a55b412a8e47a37b06f9c29d" }
1212
base64 = "0.21"
13+
chrono = "0.4"
1314
# Fork with updated auth to support ACR login
1415
# Ref https://github.com/camallo/dkregistry-rs/pull/263
1516
dkregistry = { git = "https://github.com/fermyon/dkregistry-rs", rev = "161cf2b66996ed97c7abaf046e38244484814de3" }

crates/oci/src/client.rs

Lines changed: 236 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ pub struct ClientOpts {
8484
pub content_ref_inline_max_size: usize,
8585
}
8686

87+
/// Controls whether predefined annotations are generated when pushing an application.
88+
/// If an explicit annotation has the same name as a predefined one, the explicit
89+
/// one takes precedence.
90+
#[derive(Debug, PartialEq)]
91+
pub enum InferPredefinedAnnotations {
92+
/// Infer annotations for created, authors, version, name and description.
93+
All,
94+
/// Do not generate any annotations; use only explicitly supplied annotations.
95+
None,
96+
}
97+
8798
impl Client {
8899
/// Create a new instance of an OCI client for distributing Spin applications.
89100
pub async fn new(insecure: bool, cache_root: Option<PathBuf>) -> Result<Self> {
@@ -107,6 +118,7 @@ impl Client {
107118
manifest_path: &Path,
108119
reference: impl AsRef<str>,
109120
annotations: Option<BTreeMap<String, String>>,
121+
infer_annotations: InferPredefinedAnnotations,
110122
) -> Result<Option<String>> {
111123
let reference: Reference = reference
112124
.as_ref()
@@ -125,7 +137,7 @@ impl Client {
125137
)
126138
.await?;
127139

128-
self.push_locked_core(locked, auth, reference, annotations)
140+
self.push_locked_core(locked, auth, reference, annotations, infer_annotations)
129141
.await
130142
}
131143

@@ -136,14 +148,15 @@ impl Client {
136148
locked: LockedApp,
137149
reference: impl AsRef<str>,
138150
annotations: Option<BTreeMap<String, String>>,
151+
infer_annotations: InferPredefinedAnnotations,
139152
) -> Result<Option<String>> {
140153
let reference: Reference = reference
141154
.as_ref()
142155
.parse()
143156
.with_context(|| format!("cannot parse reference {}", reference.as_ref()))?;
144157
let auth = Self::auth(&reference).await?;
145158

146-
self.push_locked_core(locked, auth, reference, annotations)
159+
self.push_locked_core(locked, auth, reference, annotations, infer_annotations)
147160
.await
148161
}
149162

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

191+
let annotations = all_annotations(&locked_app, annotations, infer_annotations);
192+
177193
// Push layer for locked spin application config
178194
let locked_config_layer = ImageLayer::new(
179195
serde_json::to_vec(&locked_app).context("could not serialize locked config")?,
@@ -688,6 +704,76 @@ fn registry_from_input(server: impl AsRef<str>) -> String {
688704
}
689705
}
690706

707+
fn all_annotations(
708+
locked_app: &LockedApp,
709+
explicit: Option<BTreeMap<String, String>>,
710+
predefined: InferPredefinedAnnotations,
711+
) -> Option<BTreeMap<String, String>> {
712+
use spin_locked_app::{MetadataKey, APP_DESCRIPTION_KEY, APP_NAME_KEY, APP_VERSION_KEY};
713+
const APP_AUTHORS_KEY: MetadataKey<Vec<String>> = MetadataKey::new("authors");
714+
715+
if predefined == InferPredefinedAnnotations::None {
716+
return explicit;
717+
}
718+
719+
// We will always, at minimum, have a `created` annotation, so if we don't already have an
720+
// anootations collection then we may as well create one now...
721+
let mut current = explicit.unwrap_or_default();
722+
723+
let authors = locked_app
724+
.get_metadata(APP_AUTHORS_KEY)
725+
.unwrap_or_default()
726+
.unwrap_or_default();
727+
if !authors.is_empty() {
728+
let authors = authors.join(", ");
729+
add_inferred(
730+
&mut current,
731+
oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_AUTHORS,
732+
Some(authors),
733+
);
734+
}
735+
736+
let name = locked_app.get_metadata(APP_NAME_KEY).unwrap_or_default();
737+
add_inferred(
738+
&mut current,
739+
oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_TITLE,
740+
name,
741+
);
742+
743+
let description = locked_app
744+
.get_metadata(APP_DESCRIPTION_KEY)
745+
.unwrap_or_default();
746+
add_inferred(
747+
&mut current,
748+
oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_DESCRIPTION,
749+
description,
750+
);
751+
752+
let version = locked_app.get_metadata(APP_VERSION_KEY).unwrap_or_default();
753+
add_inferred(
754+
&mut current,
755+
oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_VERSION,
756+
version,
757+
);
758+
759+
let created = chrono::Utc::now().to_rfc3339();
760+
add_inferred(
761+
&mut current,
762+
oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_CREATED,
763+
Some(created),
764+
);
765+
766+
Some(current)
767+
}
768+
769+
fn add_inferred(map: &mut BTreeMap<String, String>, key: &str, value: Option<String>) {
770+
if let Some(value) = value {
771+
if let std::collections::btree_map::Entry::Vacant(e) = map.entry(key.to_string()) {
772+
e.insert(value);
773+
}
774+
}
775+
}
776+
691777
#[cfg(test)]
692778
mod test {
693779
use super::*;
@@ -976,4 +1062,152 @@ mod test {
9761062
}
9771063
}
9781064
}
1065+
1066+
fn annotatable_app() -> LockedApp {
1067+
let mut meta_builder = spin_locked_app::values::ValuesMapBuilder::new();
1068+
meta_builder
1069+
.string("name", "this-is-spinal-tap")
1070+
.string("version", "11.11.11")
1071+
.string("description", "")
1072+
.string_array("authors", vec!["Marty DiBergi", "Artie Fufkin"]);
1073+
let metadata = meta_builder.build();
1074+
LockedApp {
1075+
spin_lock_version: Default::default(),
1076+
must_understand: vec![],
1077+
metadata,
1078+
host_requirements: Default::default(),
1079+
variables: Default::default(),
1080+
triggers: Default::default(),
1081+
components: Default::default(),
1082+
}
1083+
}
1084+
1085+
fn as_annotations(annotations: &[(&str, &str)]) -> Option<BTreeMap<String, String>> {
1086+
Some(
1087+
annotations
1088+
.iter()
1089+
.map(|(k, v)| (k.to_string(), v.to_string()))
1090+
.collect(),
1091+
)
1092+
}
1093+
1094+
#[test]
1095+
fn no_annotations_no_infer_result_is_no_annotations() {
1096+
let locked_app = annotatable_app();
1097+
let explicit = None;
1098+
let infer = InferPredefinedAnnotations::None;
1099+
1100+
assert!(all_annotations(&locked_app, explicit, infer).is_none());
1101+
}
1102+
1103+
#[test]
1104+
fn explicit_annotations_no_infer_result_is_explicit_annotations() {
1105+
let locked_app = annotatable_app();
1106+
let explicit = as_annotations(&[("volume", "11"), ("dimensions", "feet")]);
1107+
let infer = InferPredefinedAnnotations::None;
1108+
1109+
let annotations =
1110+
all_annotations(&locked_app, explicit, infer).expect("should still have annotations");
1111+
assert_eq!(2, annotations.len());
1112+
assert_eq!("11", annotations.get("volume").unwrap());
1113+
assert_eq!("feet", annotations.get("dimensions").unwrap());
1114+
}
1115+
1116+
#[test]
1117+
fn no_annotations_infer_all_result_is_auto_annotations() {
1118+
let locked_app = annotatable_app();
1119+
let explicit = None;
1120+
let infer = InferPredefinedAnnotations::All;
1121+
1122+
let annotations =
1123+
all_annotations(&locked_app, explicit, infer).expect("should now have annotations");
1124+
assert_eq!(4, annotations.len());
1125+
assert_eq!(
1126+
"Marty DiBergi, Artie Fufkin",
1127+
annotations
1128+
.get(oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_AUTHORS)
1129+
.expect("should have authors annotation")
1130+
);
1131+
assert_eq!(
1132+
"this-is-spinal-tap",
1133+
annotations
1134+
.get(oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_TITLE)
1135+
.expect("should have title annotation")
1136+
);
1137+
assert_eq!(
1138+
"11.11.11",
1139+
annotations
1140+
.get(oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_VERSION)
1141+
.expect("should have version annotation")
1142+
);
1143+
assert!(
1144+
annotations
1145+
.get(oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_DESCRIPTION)
1146+
.is_none(),
1147+
"empty description should not have generated annotation"
1148+
);
1149+
assert!(
1150+
annotations
1151+
.get(oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_CREATED)
1152+
.is_some(),
1153+
"creation annotation should have been generated"
1154+
);
1155+
}
1156+
1157+
#[test]
1158+
fn explicit_annotations_infer_all_gets_both_sets() {
1159+
let locked_app = annotatable_app();
1160+
let explicit = as_annotations(&[("volume", "11"), ("dimensions", "feet")]);
1161+
let infer = InferPredefinedAnnotations::All;
1162+
1163+
let annotations =
1164+
all_annotations(&locked_app, explicit, infer).expect("should still have annotations");
1165+
assert_eq!(6, annotations.len());
1166+
assert_eq!(
1167+
"11",
1168+
annotations
1169+
.get("volume")
1170+
.expect("should have retained explicit annotation")
1171+
);
1172+
assert_eq!(
1173+
"Marty DiBergi, Artie Fufkin",
1174+
annotations
1175+
.get(oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_AUTHORS)
1176+
.expect("should have authors annotation")
1177+
);
1178+
}
1179+
1180+
#[test]
1181+
fn explicit_annotations_take_precedence_over_inferred() {
1182+
let locked_app = annotatable_app();
1183+
let explicit = as_annotations(&[
1184+
("volume", "11"),
1185+
(
1186+
oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_AUTHORS,
1187+
"David St Hubbins, Nigel Tufnel",
1188+
),
1189+
]);
1190+
let infer = InferPredefinedAnnotations::All;
1191+
1192+
let annotations =
1193+
all_annotations(&locked_app, explicit, infer).expect("should still have annotations");
1194+
assert_eq!(
1195+
5,
1196+
annotations.len(),
1197+
"should have one custom, one predefined explicit, and three inferred"
1198+
);
1199+
assert_eq!(
1200+
"11",
1201+
annotations
1202+
.get("volume")
1203+
.expect("should have retained explicit annotation")
1204+
);
1205+
assert_eq!(
1206+
"David St Hubbins, Nigel Tufnel",
1207+
annotations
1208+
.get(oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_AUTHORS)
1209+
.expect("should have authors annotation"),
1210+
"explicit authors should have taken precedence"
1211+
);
1212+
}
9791213
}

src/commands/registry.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use anyhow::{Context, Result};
33
use clap::{Parser, Subcommand};
44
use indicatif::{ProgressBar, ProgressStyle};
55
use spin_common::arg_parser::parse_kv;
6-
use spin_oci::Client;
6+
use spin_oci::{client::InferPredefinedAnnotations, Client};
77
use std::{io::Read, path::PathBuf, time::Duration};
88

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

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

89-
let digest = client.push(&app_file, &self.reference, annotations).await?;
89+
let digest = client
90+
.push(
91+
&app_file,
92+
&self.reference,
93+
annotations,
94+
InferPredefinedAnnotations::All,
95+
)
96+
.await?;
9097
match digest {
9198
Some(digest) => println!("Pushed with digest {digest}"),
9299
None => println!("Pushed; the registry did not return the digest"),

0 commit comments

Comments
 (0)