@@ -84,6 +84,17 @@ pub struct ClientOpts {
84
84
pub content_ref_inline_max_size : usize ,
85
85
}
86
86
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
+
87
98
impl Client {
88
99
/// Create a new instance of an OCI client for distributing Spin applications.
89
100
pub async fn new ( insecure : bool , cache_root : Option < PathBuf > ) -> Result < Self > {
@@ -107,6 +118,7 @@ impl Client {
107
118
manifest_path : & Path ,
108
119
reference : impl AsRef < str > ,
109
120
annotations : Option < BTreeMap < String , String > > ,
121
+ infer_annotations : InferPredefinedAnnotations ,
110
122
) -> Result < Option < String > > {
111
123
let reference: Reference = reference
112
124
. as_ref ( )
@@ -125,7 +137,7 @@ impl Client {
125
137
)
126
138
. await ?;
127
139
128
- self . push_locked_core ( locked, auth, reference, annotations)
140
+ self . push_locked_core ( locked, auth, reference, annotations, infer_annotations )
129
141
. await
130
142
}
131
143
@@ -136,14 +148,15 @@ impl Client {
136
148
locked : LockedApp ,
137
149
reference : impl AsRef < str > ,
138
150
annotations : Option < BTreeMap < String , String > > ,
151
+ infer_annotations : InferPredefinedAnnotations ,
139
152
) -> Result < Option < String > > {
140
153
let reference: Reference = reference
141
154
. as_ref ( )
142
155
. parse ( )
143
156
. with_context ( || format ! ( "cannot parse reference {}" , reference. as_ref( ) ) ) ?;
144
157
let auth = Self :: auth ( & reference) . await ?;
145
158
146
- self . push_locked_core ( locked, auth, reference, annotations)
159
+ self . push_locked_core ( locked, auth, reference, annotations, infer_annotations )
147
160
. await
148
161
}
149
162
@@ -155,6 +168,7 @@ impl Client {
155
168
auth : RegistryAuth ,
156
169
reference : Reference ,
157
170
annotations : Option < BTreeMap < String , String > > ,
171
+ infer_annotations : InferPredefinedAnnotations ,
158
172
) -> Result < Option < String > > {
159
173
let mut locked_app = locked. clone ( ) ;
160
174
let mut layers = self
@@ -174,6 +188,8 @@ impl Client {
174
188
. context ( "could not assemble archive layers for locked application" ) ?;
175
189
}
176
190
191
+ let annotations = all_annotations ( & locked_app, annotations, infer_annotations) ;
192
+
177
193
// Push layer for locked spin application config
178
194
let locked_config_layer = ImageLayer :: new (
179
195
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 {
688
704
}
689
705
}
690
706
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
+
691
777
#[ cfg( test) ]
692
778
mod test {
693
779
use super :: * ;
@@ -976,4 +1062,152 @@ mod test {
976
1062
}
977
1063
}
978
1064
}
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
+ }
979
1213
}
0 commit comments