8
8
import re
9
9
import signal
10
10
import sys
11
+ import subprocess
11
12
12
- split_version_re = re .compile ("[.+-]" )
13
+ # requires docker be available on the CLI
14
+
15
+ # constants
16
+ SPLIT_VERSION_REGEX = re .compile ("[.+-]" )
17
+ NEEDS_MANIFEST = "NEEDS_MANIFEST"
18
+ PENDING = "PENDING"
19
+ SUCCESS = "SUCCESS"
20
+ FAILED = "FAILED"
13
21
14
22
15
23
# distutils.version.LooseVersion is deprecated. packaging.version is the recommended replacement but I'd like to
16
24
# keep this script vanilla. This is an attempt at re-implementation good enough for our purposes.
17
25
class LooseVersion :
18
26
def __init__ (self , s ):
19
- self .vs = split_version_re .split (s )
27
+ self .vs = SPLIT_VERSION_REGEX .split (s )
20
28
self .n = max (len (x ) for x in self .vs )
21
29
22
30
def cmp (self , other , op ):
@@ -81,6 +89,10 @@ def read_buildlog_file(buildlog_file_path: Path) -> dict:
81
89
print_error (f"Buildlog not found at { parsed_args .buildlog_file_path } " )
82
90
83
91
92
+ def semver_sorted_dict (d : dict ) -> dict :
93
+ return dict (sorted (d .items (), key = lambda i : LooseVersion (i [0 ])))
94
+
95
+
84
96
def build_tag_state (buildlog_data : list ) -> dict :
85
97
tag_state = {}
86
98
for buildlog_entry in buildlog_data :
@@ -90,26 +102,51 @@ def build_tag_state(buildlog_data: list) -> dict:
90
102
major , minor , patch , arch = pad_none (
91
103
version .replace ("." , "-" ).split ("-" ), 4
92
104
)
105
+ manifest_semver = f"{ major } .{ minor } .{ patch } "
106
+ medium_tag = f"{ major } .{ minor } "
93
107
if arch :
94
- tag_state [version ] = digest
95
- # TODO: need to create a manifest that gets all the else tags of both digests
108
+ tag_state [version ] = {"digest" : digest , "status" : PENDING }
109
+ tag_state [major ] = {"digest" : NEEDS_MANIFEST , "status" : PENDING }
110
+ tag_state [medium_tag ] = {
111
+ "digest" : NEEDS_MANIFEST ,
112
+ "status" : PENDING ,
113
+ }
114
+ tag_state [manifest_semver ] = {
115
+ "digest" : NEEDS_MANIFEST ,
116
+ "status" : PENDING ,
117
+ }
96
118
else :
97
- if f"{ major } .{ minor } .{ patch } " in tag_state :
98
- tag_state [f"{ major } .{ minor } .{ patch } " ] = digest
119
+ if manifest_semver in tag_state :
120
+ # if the current digest for our major or minor tag is the previous version of this container, update it
121
+ if (
122
+ tag_state [major ]["digest" ]
123
+ is tag_state [manifest_semver ]["digest" ]
124
+ ):
125
+ tag_state [major ]["digest" ] = digest
126
+ if (
127
+ tag_state [medium_tag ]["digest" ]
128
+ is tag_state [manifest_semver ]["digest" ]
129
+ ):
130
+ tag_state [medium_tag ]["digest" ] = digest
131
+ # update the full tag to the new version of the container always
132
+ tag_state [manifest_semver ] = {"digest" : digest , "status" : PENDING }
99
133
else :
100
- tag_state [major ] = digest
101
- tag_state [f"{ major } .{ minor } " ] = digest
102
- tag_state [f"{ major } .{ minor } .{ patch } " ] = digest
134
+ tag_state [major ] = {"digest" : digest , "status" : PENDING }
135
+ tag_state [medium_tag ] = {
136
+ "digest" : digest ,
137
+ "status" : PENDING ,
138
+ }
139
+ tag_state [manifest_semver ] = {"digest" : digest , "status" : PENDING }
103
140
else :
104
141
print_error (
105
142
f"Buildlog entry found without associated container hash: { buildlog_entry } "
106
143
)
107
- return tag_state
144
+ return semver_sorted_dict ( tag_state )
108
145
109
146
110
- def semver_sorted_dict (d : dict ) -> str :
147
+ def pretty_printable_dict (d : dict ) -> str :
111
148
return json .dumps (
112
- dict ( sorted ( tag_state . items (), key = lambda i : LooseVersion ( i [ 0 ]))) ,
149
+ d ,
113
150
indent = 4 ,
114
151
sort_keys = False ,
115
152
)
@@ -121,4 +158,122 @@ def semver_sorted_dict(d: dict) -> str:
121
158
parsed_args = arg_parser .parse_args (sys .argv [1 :])
122
159
buildlog_data = read_buildlog_file (parsed_args .buildlog_file_path )
123
160
tag_state = build_tag_state (buildlog_data )
124
- print (semver_sorted_dict (tag_state ))
161
+ print (pretty_printable_dict (tag_state ))
162
+ # tag and push everything that doesn't need a manifest
163
+ image_name = parsed_args .buildlog_file_path .stem
164
+ for key , value in tag_state .items ():
165
+ if value ["digest" ] is not NEEDS_MANIFEST :
166
+ # Errors will show up here where containers/versions have been removed either because of our retention
167
+ # policy or ones that were yanked.
168
+ image_name_by_digest = (
169
+ f"gcr.io/ironcore-images/{ image_name } @sha256:{ value ['digest' ]} "
170
+ )
171
+ pull = subprocess .run (["docker" , "image" , "pull" , image_name_by_digest ])
172
+ # tag with the new value
173
+ if pull .returncode is 0 :
174
+ tag = subprocess .run (
175
+ [
176
+ "docker" ,
177
+ "image" ,
178
+ "tag" ,
179
+ image_name_by_digest ,
180
+ f"gcr.io/ironcore-images/{ image_name } :{ key } " ,
181
+ ]
182
+ )
183
+ if tag .returncode is 0 :
184
+ push = subprocess .run (
185
+ ["docker" , "push" , f"gcr.io/ironcore-images/{ image_name } :{ key } " ]
186
+ )
187
+ if push .returncode is 0 :
188
+ value ["status" ] = SUCCESS
189
+ continue
190
+ value ["status" ] = FAILED
191
+ # print what was tagged
192
+ successfully_tagged = {
193
+ k : v ["digest" ] for k , v in tag_state .items () if v ["status" ] is SUCCESS
194
+ }
195
+ failed_tags = {
196
+ k : v ["digest" ] for k , v in tag_state .items () if v ["status" ] is FAILED
197
+ }
198
+ needs_manifest = {
199
+ k : v ["digest" ] for k , v in tag_state .items () if v ["digest" ] is NEEDS_MANIFEST
200
+ }
201
+ print (pretty_printable_dict (successfully_tagged ))
202
+ print_error (pretty_printable_dict (failed_tags ))
203
+ print (pretty_printable_dict (needs_manifest ))
204
+ # use the tags just pushed to create manifests
205
+ link_manifest = {}
206
+ for key in needs_manifest :
207
+ # we expect no arch tags at this point
208
+ major , minor , patch = pad_none (key .split ("." ), 3 )
209
+ # if there's a patch we need to create a manifest for it
210
+ if patch :
211
+ gcr_image = f"gcr.io/ironcore-images/{ image_name } :{ key } "
212
+ # all our images that need manifests have arm64/amd64 versions
213
+ create_manifest = subprocess .run (
214
+ [
215
+ "docker" ,
216
+ "manifest" ,
217
+ "create" ,
218
+ gcr_image ,
219
+ f"{ gcr_image } -arm64" ,
220
+ f"{ gcr_image } -amd64" ,
221
+ ]
222
+ )
223
+ if create_manifest .returncode is 0 :
224
+ annotate_manifest_arm = subprocess .run (
225
+ [
226
+ "docker" ,
227
+ "manifest" ,
228
+ "annotate" ,
229
+ gcr_image ,
230
+ f"{ gcr_image } -arm64" ,
231
+ "--arch" ,
232
+ "arm64" ,
233
+ ]
234
+ )
235
+ annotate_manifest_amd = subprocess .run (
236
+ [
237
+ "docker" ,
238
+ "manifest" ,
239
+ "annotate" ,
240
+ gcr_image ,
241
+ f"{ gcr_image } -amd64" ,
242
+ "--arch" ,
243
+ "amd64" ,
244
+ ]
245
+ )
246
+ # if creation and annotation worked, push the new manifest
247
+ if (
248
+ annotate_manifest_arm .returncode is 0
249
+ and annotate_manifest_amd .returncode is 0
250
+ ):
251
+ # rebuilds aren't a factor now and the tags are semver sorted, so the last writer is the right one
252
+ major_manifest_name = f"gcr.io/ironcore-images/{ image_name } :{ major } "
253
+ minor_manifest_name = (
254
+ f"gcr.io/ironcore-images/{ image_name } :{ major } .{ minor } "
255
+ )
256
+ subprocess .run (
257
+ [
258
+ "docker" ,
259
+ "manifest" ,
260
+ "create" ,
261
+ major_manifest_name ,
262
+ f"{ gcr_image } -arm64" ,
263
+ f"{ gcr_image } -amd64" ,
264
+ ]
265
+ )
266
+ subprocess .run (
267
+ [
268
+ "docker" ,
269
+ "manifest" ,
270
+ "create" ,
271
+ minor_manifest_name ,
272
+ f"{ gcr_image } -arm64" ,
273
+ f"{ gcr_image } -amd64" ,
274
+ ]
275
+ )
276
+ # the more specific annotation of the first manifest seems to carry through to the others
277
+ subprocess .run (["docker" , "manifest" , "push" , gcr_image ])
278
+ subprocess .run (["docker" , "manifest" , "push" , major_manifest_name ])
279
+ subprocess .run (["docker" , "manifest" , "push" , minor_manifest_name ])
0 commit comments