@@ -43,6 +43,7 @@ class PromoteHelper {
4343 private final String token ;
4444 private final String imageTag ;
4545 private final @ Nullable String additionalTag ;
46+ private final @ Nullable String replacementTag ;
4647
4748 /**
4849 * Promote the staged flex template image using MOSS promote API.
@@ -52,16 +53,26 @@ class PromoteHelper {
5253 * @param imageTag - image tag
5354 * @param additionalTag - additional destination tag, used by repo managements, e.g.
5455 * public-image-latest
56+ * @param replacementTag - tag to put on original holder of additionalTag, used by repo
57+ * managements, e.g. no-new-use-public-image-
5558 * @param sourceDigest - source image digest, e.g. sha256:xxxxx
5659 */
5760 public PromoteHelper (
5861 String sourcePath ,
5962 String targetPath ,
6063 String imageTag ,
6164 @ Nullable String additionalTag ,
65+ @ Nullable String replacementTag ,
6266 String sourceDigest )
6367 throws IOException , InterruptedException {
64- this (sourcePath , targetPath , imageTag , additionalTag , sourceDigest , accessToken ());
68+ this (
69+ sourcePath ,
70+ targetPath ,
71+ imageTag ,
72+ additionalTag ,
73+ replacementTag ,
74+ sourceDigest ,
75+ accessToken ());
6576 }
6677
6778 @ VisibleForTesting
@@ -70,30 +81,39 @@ public PromoteHelper(
7081 String targetPath ,
7182 String imageTag ,
7283 @ Nullable String additionalTag ,
84+ @ Nullable String replacementTag ,
7385 String sourceDigest ,
7486 String token ) {
7587 this .sourceSpec = new ArtifactRegImageSpec (sourcePath );
7688 this .targetSpec = new ArtifactRegImageSpec (targetPath );
7789 this .imageTag = imageTag ;
7890 this .additionalTag = additionalTag ;
91+ this .replacementTag = replacementTag ;
7992 this .sourceDigest = sourceDigest ;
8093 this .token = token ;
8194 this .targetPath = targetPath ;
8295 }
8396
8497 /** Promote the artifact. */
8598 public void promote () throws IOException , InterruptedException {
99+ String originalDigest = null ;
100+ if (additionalTag != null ) {
101+ originalDigest = getDigestFromTag (additionalTag );
102+ }
86103 String [] promoteArtifactCmd = getPromoteFlexTemplateImageCmd ();
87104 // promote API returns a long-running-operation
88105 String responseRLO = TemplatesStageMojo .runCommandCapturesOutput (promoteArtifactCmd , null );
89106 JsonElement parsed = JsonParser .parseString (responseRLO );
90107 String operation = parsed .getAsJsonObject ().get ("name" ).getAsString ();
91108 waitForComplete (operation );
92- addTag (imageTag );
109+ addTag (imageTag , sourceDigest );
93110 // override latest (for pull default tag) and additionalTag (for vul scan, if present)
94- addTag ("latest" );
111+ addTag ("latest" , sourceDigest );
95112 if (additionalTag != null ) {
96- addTag (additionalTag );
113+ addTag (additionalTag , sourceDigest );
114+ }
115+ if (originalDigest != null && !originalDigest .isEmpty ()) {
116+ addTag (replacementTag , originalDigest );
97117 }
98118 }
99119
@@ -157,7 +177,7 @@ private void waitForComplete(String operation) {
157177 }
158178
159179 /** Add tag after promotion. */
160- private void addTag (String tag ) throws IOException , InterruptedException {
180+ private void addTag (String tag , String digest ) throws IOException , InterruptedException {
161181 // TODO: remove this once copy tag is supported by promote API
162182 String [] command ;
163183 if (targetSpec .repository .endsWith ("gcr.io" )) {
@@ -169,7 +189,7 @@ private void addTag(String tag) throws IOException, InterruptedException {
169189 "images" ,
170190 "add-tag" ,
171191 "-q" ,
172- String .format ("%s@%s" , targetPath , sourceDigest ),
192+ String .format ("%s@%s" , targetPath , digest ),
173193 String .format ("%s:%s" , targetPath , tag )
174194 };
175195 } else {
@@ -180,13 +200,65 @@ private void addTag(String tag) throws IOException, InterruptedException {
180200 "docker" ,
181201 "tags" ,
182202 "add" ,
183- String .format ("%s@%s" , targetPath , sourceDigest ),
203+ String .format ("%s@%s" , targetPath , digest ),
184204 String .format ("%s:%s" , targetPath , tag )
185205 };
186206 }
187207 TemplatesStageMojo .runCommandCapturesOutput (command , null );
188208 }
189209
210+ /**
211+ * Get the digest of an image with a specific tag.
212+ *
213+ * @param tag - The tag of the image to retrieve.
214+ * @return The digest of the image.
215+ */
216+ @ VisibleForTesting
217+ String getDigestFromTag (String tag ) throws IOException , InterruptedException {
218+ String [] command ;
219+ String imageReference = String .format ("%s:%s" , targetPath , tag );
220+
221+ if (targetSpec .repository .endsWith ("gcr.io" )) {
222+ // gcr.io repository needs to use `gcloud container` to list tags
223+ command =
224+ new String [] {
225+ "gcloud" ,
226+ "container" ,
227+ "images" ,
228+ "list-tags" ,
229+ targetPath , // This is the image name, e.g., us.gcr.io/my-project/my-image
230+ "--filter=tags:" + tag ,
231+ "--format" ,
232+ "get(digest)"
233+ };
234+ } else {
235+ // Artifact Registry repository needs to use `gcloud artifacts docker images describe`
236+ command =
237+ new String [] {
238+ "gcloud" ,
239+ "artifacts" ,
240+ "docker" ,
241+ "images" ,
242+ "describe" ,
243+ imageReference , // This is the full image reference including tag, e.g.,
244+ // us-central1-docker.pkg.dev/my-project/my-repo/my-image:tag
245+ "--format" ,
246+ "get(image_summary.digest)"
247+ };
248+ }
249+
250+ String response = "" ;
251+ try {
252+ response = TemplatesStageMojo .runCommandCapturesOutput (command , null );
253+ } catch (Exception e ) {
254+ // Swallow exceptions here - usually this means that the image does not exist with the tag
255+ return "" ;
256+ }
257+ // The response is expected to be just the digest, e.g., "sha256:..."
258+ // Trim any leading/trailing whitespace.
259+ return response .trim ();
260+ }
261+
190262 private static class QueryOperationRunnable implements dev .failsafe .function .CheckedRunnable {
191263 String [] command ;
192264
0 commit comments