diff --git a/jib-cli/CHANGELOG.md b/jib-cli/CHANGELOG.md index 19633e00e0..4fab259020 100644 --- a/jib-cli/CHANGELOG.md +++ b/jib-cli/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Added `--image-metadata-out` option to specify JSON output file that should contain image metadata (image ID, digest, and tags) after build is complete. ([#3187](https://github.com/GoogleContainerTools/jib/pull/3187)) + ### Changed ### Fixed diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Build.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Build.java index 417778e478..cf3cab6042 100644 --- a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Build.java +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Build.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.cli; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainer; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.cli.buildfile.BuildFiles; @@ -131,7 +132,8 @@ public Integer call() { GlobalConfig globalConfig = GlobalConfig.readConfig(); Multimaps.asMap(globalConfig.getRegistryMirrors()).forEach(containerizer::addRegistryMirrors); - containerBuilder.containerize(containerizer); + JibContainer jibContainer = containerBuilder.containerize(containerizer); + JibCli.writeImageJson(commonCliOptions.getImageJsonPath(), jibContainer); } catch (Exception ex) { JibCli.logTerminatingException(logger, ex, commonCliOptions.isStacktrace()); return 1; diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonCliOptions.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonCliOptions.java index 94335f5f7e..afd0113905 100644 --- a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonCliOptions.java +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonCliOptions.java @@ -244,6 +244,15 @@ private static class FromUsernamePassword { @SuppressWarnings("NullAway.Init") // initialized by picocli private boolean serialize; + @CommandLine.Option( + names = "--image-metadata-out", + paramLabel = "", + description = + "path to the json file that should contain image metadata (for example, digest, id and tags) after build is" + + "complete") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private Path imageJsonPath; + public Verbosity getVerbosity() { return Verify.verifyNotNull(verbosity); } @@ -400,6 +409,15 @@ public List getAdditionalTags() { return additionalTags; } + /** + * Returns the full path to the image metadata json file, if provided. + * + * @return optional value of path to image json file + */ + public Optional getImageJsonPath() { + return Optional.ofNullable(imageJsonPath); + } + /** Validates parameters defined in this class that could not be done declaratively. */ public void validate() { if (targetImage.startsWith(TAR_IMAGE_PREFIX) && name == null) { diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Jar.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Jar.java index eb2f9d89f0..48ade4fa2b 100644 --- a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Jar.java +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Jar.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.cli; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainer; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.Ports; @@ -202,7 +203,8 @@ public Integer call() { GlobalConfig globalConfig = GlobalConfig.readConfig(); Multimaps.asMap(globalConfig.getRegistryMirrors()).forEach(containerizer::addRegistryMirrors); - containerBuilder.containerize(containerizer); + JibContainer jibContainer = containerBuilder.containerize(containerizer); + JibCli.writeImageJson(commonCliOptions.getImageJsonPath(), jibContainer); } catch (Exception ex) { JibCli.logTerminatingException(logger, ex, commonCliOptions.isStacktrace()); return 1; diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/JibCli.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/JibCli.java index 37ef16f8a6..4c913816d9 100644 --- a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/JibCli.java +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/JibCli.java @@ -18,10 +18,17 @@ import com.google.api.client.http.HttpTransport; import com.google.api.client.http.apache.v2.ApacheHttpTransport; +import com.google.cloud.tools.jib.api.JibContainer; import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.plugins.common.ImageMetadataOutput; import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; import java.util.logging.ConsoleHandler; import java.util.logging.Level; import java.util.logging.Logger; @@ -67,6 +74,24 @@ static void logTerminatingException( + "\u001B[0m"); } + /** + * Writes image details (imageId, digest, tags, etc.) to a json file, if the path to the json is + * provided. + * + * @param imageJsonOutputPath optional path to json file (for example, + * path/to/json/jib-image.json) + * @param jibContainer the {@link JibContainer} to derive image details from + * @throws IOException if error occurs when writing to the json file. + */ + static void writeImageJson(Optional imageJsonOutputPath, JibContainer jibContainer) + throws IOException { + if (imageJsonOutputPath.isPresent()) { + ImageMetadataOutput metadataOutput = ImageMetadataOutput.fromJibContainer(jibContainer); + Files.write( + imageJsonOutputPath.get(), metadataOutput.toJson().getBytes(StandardCharsets.UTF_8)); + } + } + /** * The magic starts here. * diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/BuildTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/BuildTest.java index 3d050c7d35..cb6b78305a 100644 --- a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/BuildTest.java +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/BuildTest.java @@ -68,6 +68,7 @@ public void testParse_defaults() { assertThat(commonCliOptions.isStacktrace()).isFalse(); assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.off); assertThat(commonCliOptions.isSerialize()).isFalse(); + assertThat(commonCliOptions.getImageJsonPath()).isEmpty(); } @Test @@ -103,6 +104,7 @@ public void testParse_shortFormParams() { assertThat(commonCliOptions.isStacktrace()).isFalse(); assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.off); assertThat(commonCliOptions.isSerialize()).isFalse(); + assertThat(commonCliOptions.getImageJsonPath()).isEmpty(); } @Test @@ -125,7 +127,8 @@ public void testParse_longFormParams() { "--verbosity=info", "--stacktrace", "--http-trace", - "--serialize"); + "--serialize", + "--image-metadata-out=path/to/json/jib-image.json"); CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; assertThat(commonCliOptions.getTargetImage()).isEqualTo("test-image-ref"); assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); @@ -149,6 +152,8 @@ public void testParse_longFormParams() { assertThat(commonCliOptions.isStacktrace()).isTrue(); assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.config); assertThat(commonCliOptions.isSerialize()).isTrue(); + assertThat(commonCliOptions.getImageJsonPath()) + .hasValue(Paths.get("path/to/json/jib-image.json")); } @Test diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JarTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JarTest.java index f58b7756f9..babd022ac2 100644 --- a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JarTest.java +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JarTest.java @@ -84,6 +84,7 @@ public void testParse_defaults() { assertThat(commonCliOptions.isStacktrace()).isFalse(); assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.off); assertThat(commonCliOptions.isSerialize()).isFalse(); + assertThat(commonCliOptions.getImageJsonPath()).isEmpty(); assertThat(jarCommand.getFrom()).isEmpty(); assertThat(jarCommand.getJvmFlags()).isEmpty(); assertThat(jarCommand.getExposedPorts()).isEmpty(); @@ -119,6 +120,7 @@ public void testParse_shortFormParams() { assertThat(commonCliOptions.isStacktrace()).isFalse(); assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.off); assertThat(commonCliOptions.isSerialize()).isFalse(); + assertThat(commonCliOptions.getImageJsonPath()).isEmpty(); } @Test @@ -138,6 +140,7 @@ public void testParse_longFormParams() { "--stacktrace", "--http-trace", "--serialize", + "--image-metadata-out=path/to/json/jib-image.json", "my-app.jar"); CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; assertThat(commonCliOptions.getTargetImage()).isEqualTo("test-image-ref"); @@ -157,6 +160,8 @@ public void testParse_longFormParams() { assertThat(commonCliOptions.isStacktrace()).isTrue(); assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.config); assertThat(commonCliOptions.isSerialize()).isTrue(); + assertThat(commonCliOptions.getImageJsonPath()) + .hasValue(Paths.get("path/to/json/jib-image.json")); } @Test diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JibCliTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JibCliTest.java index 131e28bbb4..025a3c24af 100644 --- a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JibCliTest.java +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JibCliTest.java @@ -22,18 +22,41 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JibContainer; import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.plugins.common.ImageMetadataOutput; import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.common.collect.ImmutableSet; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.DigestException; +import java.util.Optional; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +@RunWith(MockitoJUnitRunner.class) public class JibCliTest { + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock private JibContainer mockJibContainer; + @Test public void testConfigureHttpLogging() { Logger logger = JibCli.configureHttpLogging(Level.ALL); @@ -68,4 +91,26 @@ public void testLogTerminatingException_stackTrace() { .log(LogEvent.Level.ERROR, "\u001B[31;1mjava.io.IOException: test error message\u001B[0m"); verifyNoMoreInteractions(logger); } + + @Test + public void testWriteImageJson() + throws InvalidImageReferenceException, IOException, DigestException { + String imageId = "sha256:61bb3ec31a47cb730eb58a38bbfa813761a51dca69d10e39c24c3d00a7b2c7a9"; + String digest = "sha256:3f1be7e19129edb202c071a659a4db35280ab2bb1a16f223bfd5d1948657b6fc"; + when(mockJibContainer.getTargetImage()).thenReturn(ImageReference.parse("adoptopenjdk:8-jre")); + when(mockJibContainer.getImageId()).thenReturn(DescriptorDigest.fromDigest(imageId)); + when(mockJibContainer.getDigest()).thenReturn(DescriptorDigest.fromDigest(digest)); + when(mockJibContainer.getTags()).thenReturn(ImmutableSet.of("latest", "tag-2")); + + Path outputPath = temporaryFolder.getRoot().toPath().resolve("jib-image.json"); + JibCli.writeImageJson(Optional.of(outputPath), mockJibContainer); + + String outputJson = new String(Files.readAllBytes(outputPath), StandardCharsets.UTF_8); + ImageMetadataOutput metadataOutput = + JsonTemplateMapper.readJson(outputJson, ImageMetadataOutput.class); + assertThat(metadataOutput.getImage()).isEqualTo("adoptopenjdk:8-jre"); + assertThat(metadataOutput.getImageId()).isEqualTo(imageId); + assertThat(metadataOutput.getImageDigest()).isEqualTo(digest); + assertThat(metadataOutput.getTags()).containsExactly("latest", "tag-2"); + } }